diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-03 14:05:38 -0400 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-03 14:05:38 -0400 |
| commit | 32b551d43781dc7b8ac4587ca2ba1554b0cbd5e5 (patch) | |
| tree | d1cab6bcc35e461610cd5dfcf3f51ba679a576fc | |
| parent | fa71f1b53b524e5f6c6c1fbff1088d610b9f3236 (diff) | |
| download | open-iov-32b551d43781dc7b8ac4587ca2ba1554b0cbd5e5.tar.gz open-iov-32b551d43781dc7b8ac4587ca2ba1554b0cbd5e5.zip | |
vgsp: host CUDA bridge + sema release recovery; greeneye simulator wave
- kernel: gsp_cuda_bridge (QMD launch capture, debugfs job/out/done, param+output resolution cached from inline writes), sema drain fixes (never hold/starve a channel), GR ring autopin + counter autoreg, per-page push reads, counter-derive gate; module builds clean
- tools/cuda-bridge: CUDA driver-API daemon executing mail/vadd kernels on the physical GPU, CUBB2/CBD2 v2 protocol, layout check + self-tests
- tools/guest-diag + tools/cuda-ab: scanner/pin fixes, victory push-only mode
- debug/risc-v-gsp-rm-vm: Phase S simulator wave (GSP device model, boot ladder, triage/re-demo harnesses, docs 30-41), GPGPU-sim scaffolding
- STATE-CURRENT/docs updates
100 files changed, 20310 insertions, 390 deletions
diff --git a/STATE-CURRENT.md b/STATE-CURRENT.md index 2b04c0c..d0e44d8 100644 --- a/STATE-CURRENT.md +++ b/STATE-CURRENT.md @@ -1,13 +1,83 @@ # Open-IOV Current State -> **Snapshot: 2026-10-02 ~22:12.** 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: +> **Snapshot: 2026-10-03 (reorganized; simulator status current 2026-10-03, +> physical campaign data as of 2026-10-02 ~22:12).** 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: > [`docs/04-implementation/01-known-knowns-and-unknowns.md`](docs/04-implementation/01-known-knowns-and-unknowns.md). > The former 2026-09-21 snapshot (`STATE.md`) is retired; its content remains > in the git history. + +## Objective, status and blockers (this snapshot) + +**Objective.** Land the first green execution of an unmodified guest CUDA +program against the physical TU104 backend (a `vectorAdd`-class kernel whose +results verify), then the remaining open gates (multi-VM, 3D, Windows, +isolation, performance, portability). + +**Status.** + +- **Emulation front: GREEN.** The driver-API smoke completes end-to-end + (`ALL OK`, payload 64/64); the runtime client initializes with `rc=0` and + queues a kernel launch, whose execution requires the backend. +- **Physical backend: live on the real TU104.** Supervised full boot lifecycle + (FWSEC -> Booter Load -> RISC-V active -> RM init-done `0x1001`, + `rpc_result=0`), static-config RPC served, RM objects + a real channel + allocated, and a real copy-engine 4 KiB copy executed (`REAL COPY OK`, + semaphore released, destination equals source). Guest CE copies are + hardware-authored with engine-authored releases via the P6b/P6c forwarding + and relay paths (all parameters default off; one relayed copy's data + mismatch is under triage), and the host GR ladder allocates a GR0 channel + + compute object. +- **Guest frontier (2026-10-02 ~22:01).** The first real guest CUDA program + against the backend enumerated device properties (`cc=7.5 sm=48 + mem=134152192`) and then stalled in its first device allocation / UVM + channel path before any kernel executed; teardown was clean. +- **Simulator workstream:** see the section below (separate, development-only). + +**Blockers (current walls).** + +1. **GR context commit/execution.** The promote-side wall is root-caused + (the ladder VA space was externally owned, so the engine-context commit was + skipped and a promote VA was never booked). A default-off shared-management + address space gets past it, but the next RM call (`TURING_COMPUTE_A` + compute-object alloc) wedges the server; three further cycles closed the + client-promote shape space on the default external VAS with a soft + `NV_ERR_INVALID_STATE` (0x40) each. The V5 commit assert `0x52a8b04` is a + secondary failure. Next routes: name the soft-0x40 check, root-cause the + V5 first failure, or rehost the promote flow in risc-v-gsp-rm-debugger. +2. **First device allocation / UVM channel path in the guest** (the frontier + program's stall) - the first green guest execution is the immediate goal. +3. **Relay data-mismatch triage** (one relayed copy of 68 failed the host-side + data compare; handled fail-closed, parameter default-off). +4. **Known hazards, mitigated, never unattended:** P1 async FWSEC boot defect + (`staging not visible before kick`; fail-open, quarantined off the critical + path) and the D0/D3cold power-gating freeze hazard (default-latched locks, + exactly 5 power-domain locks per release). + +## Simulator workstream (risc-v-gsp-rm-debugger) + +Development-only tooling for simulating and inspecting GSP firmware runtime +memory; never shipped, never a guest dependency, no GPU/mdev/kernel-module +touch. Status 2026-10-03: + +- **S1 device model + boot ladder:** the real 570.144 image boots natively to + rung **`B2`** (relocation copy; 14,682 instructions, 0 unknown MMIO, 2-run + determinism), two rungs past the QEMU first fault; `B3/B4` (message loop) + are the targets. Docs: `debug/risc-v-gsp-rm-vm/docs/35` (design, Phase S/G) + and `36` (device model, `B0`-`B2`). +- **Evaluation harness:** `harness/sim_qemu_line.py` runs the simulator-vs-QEMU + A/B on frozen thresholds; verdict **`above`** on the S1 path (docs/37). +- **One-command triage:** `./risc-v-gsp-rm-debugger.sh triage` (or + `python3 harness/triage.py --firmware <new> --out dumps/triage`) chains + identity/probe/native-boot/A-B/version-diff into one report (docs/40). +- **Tests:** **290 collected on 2026-10-03** (`make test` in the tool + directory; audit baseline 155; docs/00 §Status, docs/27). +- **Docs index:** `debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md` + README + document map (docs 30-40, one line each). + ## Mission Run **unmodified NVIDIA guest drivers** in VMs with full GPU functionality @@ -31,45 +101,7 @@ meant to execute on the physical GPU. | `cuCtxCreate` returns | ✅ (the whole smoke test now completes) | | `cuMemAlloc` + `cuMemcpy` roundtrip (`ALL OK` in the smoke test) | ✅ payload roundtrip verified 64/64 in the guest | | Full `nvidia-smi` output / kernel launch | 🟡 the static runtime now initializes end-to-end with **real releases** (`cudaGetDeviceCount`/`cudaSetDevice`/3×`cudaMalloc`/2×`cudaMemcpyHtoD`/`kernel launch`/`cudaGetLastError` all `rc=0`) and queues a kernel launch; execution awaits the backend | -| CUDA kernel execution (`vectorAdd`) | 🟡 execution backend: physical ownership + firmware staging verified; the unbind/rebind freeze hazard is mitigated by the supervised D0 power-domain lock (3/3 green claim cycles; the 2026-10-01 P1 mdev-owned release path initially ran outside that pre-lock window and re-armed D3cold, freezing the host at 22:32:53 - the G0 fix bundle now latches the D0/D3cold locks by default; see the frontier section), and the real FB size (8 GiB) and boot layout are verified live. The supervised live secure-verification run raised the protected region (WPR2 up) for the first time, then proved a secure-verification-only carve is not host-recoverable on this fused part, so the fail-safe held the device and an approved reboot restored it (distribution driver rebound, GPU healthy). That one-way stage is now retired: the only device-write path is a single full boot lifecycle (secure verification -> bootloader -> RISC-V start -> resource-manager initialization), whose code is implemented in the module sources and has now run live: the first real boot raised WPR2, passed Booter Load and went RISC-V active, and the module received the first real GSP-RM message (event 0x1002); init-done (0x1001) and a clean release are the next steps (see the 2026-10-01 section). A second live boot on 2026-10-01 proved the full boot sequence again (protected region up, bootloader halt, RISC-V active, first GSP-RM message) and reached the resource-manager's inline CPU-sequencer request; the host executor rejected the real request because of an over-strict internal size cap, which was found with a raw read of the message in host memory, fixed, and rebuilt. Cycles 2-3 then ran the real sequencer and received init-done with an RM init failure (`0x59`), which reproduces on a verified matching firmware image; the server's own shutdown supplied the last handoff bit, so each failed cycle - completed a **clean release without a reboot**. Cycles 4-10 then captured - the resource-manager's own error journal and root-caused the failure: the - boot queue was missing the two pre-boot initialization messages the matching - driver queues before the kernel kick (a system-information message and a - packed registry table). That handshake is now implemented and **verified - live**: the registry transport and packing are proven (a display-disable key - took effect in the server's journal), the GPU timer is programmed before - the kick, and the chipset/bridge block of the system information is - populated from live values. Init now reaches the server's **static-config - collection** and fails there (`0x59`); the device-id override key was - exercised with no change, and a display-disable experiment showed display is - not the first error. A source-level review of the failing collector has now - closed the host side: it reads none of the remaining host-supplied fields - (the one registry key on its call graph can only clear an error), so the - ACPI/workspace/zeroed-field leads are retired. The one hard dependency left - runtime is enabled only after the collector and the protected region blocks - host reads). **Cycles 11-13 resolved this wall**: the resource-manager log - ring had never actually been mapped (its libos init-argument id was - malformed), which is why the first error was invisible; with the ring live - the failure traced to the server's chip device-id reading as zero because - the pre-boot identity fields were sent unpacked instead of as raw PCI - config-space dwords. Fixing the payload made the server report init-done - with a zero result and serve the full static-config reply (GPU name, 8 GiB - FB, internal handles, BAR PDE bases, engine caps), and graceful shutdown - works through the unload RPC. Each of cycles 4-13 released the device - cleanly with no reboot. Since 2026-10-01 17:24 the next rung has both code - and live results: a host resource-manager client creates memory and - channel-group objects on the booted server, allocates and programs the - page-directory base of a virtual-address-space object, and **allocates a real - channel** on the live server. In cycles 18-25 it went all the way to - execution: the channel was bound and scheduled, a work-submit token was - obtained, a copy work item was placed in its GPFIFO and the doorbell rung, - and after the engine's fetch and method-path faults were diagnosed and fixed - the physical copy engine ran it - a 4 KiB copy with the semaphore released - and destination equal to source. Every cycle released the device cleanly - with no reboot; the next rung is wiring the live physical session into the - guest-facing mdev/vfio path - ([`docs/03-architecture/11-physical-backend-guest-wiring.md`](docs/03-architecture/11-physical-backend-guest-wiring.md)). The guest-wiring rung then advanced: an inert backend-session API was added, and the mediated-device-owned lifecycle (P1, see the frontier section) was implemented and exercised - deferring the physical claim/boot to the first device create, releasing it on the last remove, with fail-open behaviour. The first P1 acceptance cycle exposed two defects - an asynchronous boot marked READY after a failed FWSEC boot, and the P1 release path freezing the host (see the frontier section). The G0 fix bundle (default-latched D0/D3cold locks, post-rebind readiness gate, pre-claim D0 settle, READY gated on a proven boot, pre-kick staging readback) is implemented, built, smoke-tested and now **validated live**: two module-owned supervised cycles (2026-10-02 ~00:57-00:58) were 2/2 green through claim, FWSEC, booter, RISC-V active, init-done (`0x1001`, `rpc_result=0` in ~955 ms) and a latched release with WPR2 down, exactly 5 `power domain left LOCKED` lines, 0 in-window rail cycles, host alive and pstore empty. The retested P1 mdev-owned path - the exact path that hard-froze on 2026-10-01 - now fails open safely (the READY gate releases the claim when no live GSP-RM session exists; no freeze), while the async FWSEC boot defect (`staging not visible before kick` -> `FWSEC/FRTS execution failed: -5`) remains open, quarantined off the critical path; guest wiring proceeds on the module-owned live session. The bundle was later committed (`56766b2`). Guest-wiring P2 (static-config identity overlay) has since landed and verified live (2026-10-02 01:20-01:45): with `gsp_phys_static_live=1` the guest's `nvidia-smi -q` Product Name comes from the live session (`NVIDIA GeForce RTX 2080 Super with Max-Q Design`) versus the emulated `NVIDIA GeForce RTX 2080 SUPER` with it off (A/B; overlay applied 4x/0), while the guest smoke stays GREEN `ALL OK` 64/64; one overlay-OFF full run hit a pre-existing emulated-path flake, watched. P3 (read-only control forwarding) has since landed and verified live (2026-10-02 ~01:33-01:45): the `gsp_phys_ctrl_forward` whitelist served 22 controls live with 2 fallbacks and 0 timeouts while the guest smoke stayed GREEN `ALL OK` 64/64; **P5 wave 1 (guest-submission shadow execution) has since landed and verified live** (2026-10-02 ~02:00-02:20): with `gsp_phys_work_forward=1` the sema pump shadow-executes eligible CE pushes on the physical engine through a dedicated ordered workqueue - 514 real 4 KiB copies during a GREEN guest smoke (`ALL OK` 64/64), 194 requests coalesced/dropped while one was in flight, 0 refusals, 0 timeouts - while the software release path stays authoritative and no guest memory or guest VA is touched; P6 has since proven guest-page DMA live (P6a, 2026-10-02: the physical copy engine copied 4 KiB to and from a real guest RAM page with the semaphore released while the guest smoke stayed GREEN `ALL OK` 64/64 and release stayed latched with no reboot) and then **P6b wave 1** (2026-10-02, run p6run13): with `gsp_phys_work_forward=1 gsp_phys_guest_mem=1` the sema service resolved real guest CE-copy operands to host physical addresses and executed them on the physical copy engine through module-built GMMU scratch slots (`req=42 ok=42 verify_ok=42 verify_fail=0`, 503 ladder shadow copies also ran) while the software release path stayed authoritative - guest CE copies now execute on hardware and are **hardware-authored with verified data** while the software copy remains the fail-closed fallback for unqueued or failed copies: wave 1.5 added deferred, exactly-once release (`engaged=75 applied=75`), and wave 2 added `gsp_phys_work_hw_primary=1`, which skips the software copy for forwarded ops (`copy_skipped=119`, `req=100 ok=100 verify_ok=100`, smoke `matched=64/64` `ALL OK`, 19 inline software fallbacks `copy_soft=19 copy_fail=0`, and a 121-copy fallback-recovery run with guest-RAM ranges unregistered). The guest smoke stayed GREEN `ALL OK` 64/64 and release stayed latched with no reboot. Real page-directory shadowing and guest channels (P6c: completion/fault relay, guest page tables, `vectorAdd`) have since been reached: guest CE copies and their releases are forwarded with release and fault relay in place (P6c C0/C1/C3, default off); the host GR ladder (H1) brings up a GR0 channel and a `TURING_COMPUTE_A` object, but the first push faults `CTXNOTVALID` (the external-VAS context was never committed) and the promote-binding attempt wedged GSP-RM (host assert, fuse-locked WPR2), so the fail-safe held the device until an approved reboot restored it (see the frontier section). Real guest page tables/channels and GR execution remain, along with the P6a trigger-page pinning follow-up, and P4 (alloc/free handle adoption) remains deliberately deferred. | +| CUDA kernel execution (`vectorAdd`) | 🟡 **In progress - the physical backend is live** (full boot + RM objects + a real CE copy + guest CE forwarding); the current wall is the GR context/execution path and the guest's first device allocation. The first green guest CUDA execution is the immediate goal; the chronological detail is in the milestone log below and the pre-2026-10-02 mega-summary is preserved in the archive. | | Graphics (3D), video | ❌ | | Faults, multi-version, Windows, migration, multi-tenancy, performance | ❌ | @@ -79,9 +111,35 @@ Test status is tracked with the CUDA driver-API smoke test DtoH -> cuMemFree -> cuCtxDestroy` and prints `ALL OK` only when the whole chain succeeds. -## Right now (frontier) +## Milestone index (granular IDs; full detail in the milestone log below) + +| ID | Milestone | When | State | +|---|---|---|---| +| MS-20261002-01 | First real guest CUDA program vs the physical backend | 2026-10-02 ~22:01 | device props enumerated; stalls in first alloc/UVM; clean teardown | +| MS-20261002-02 | GR promote-side wall root-caused; current wall shared-mgmt GR alloc | 2026-10-02 ~17:35 | default-off change; server wedge on `TURING_COMPUTE_A`; fail-safe held | +| MS-20261002-03 | Guest-submission relay + fault relay (P6c) | 2026-10-02 07:43-08:07 | landed default-off; `relays=64`; one data-mismatch under triage | +| MS-20261002-04 | Host GR ladder (H1) opened | 2026-10-02 08:29 | GR0 channel + compute object; first submit `CTXNOTVALID` | +| MS-20261002-05 | risc-v-gsp-rm-debugger status (tool) | 2026-10-02 08:36 | superseded 2026-10-03: see the Simulator workstream section | +| MS-20261002-06 | P6a trigger-path fix | 2026-10-02 07:10 | guest G2H/W2G copy green; PTE self-test green | +| MS-20261002-07 | P6b wave 2 (hardware-primary copies) | 2026-10-02 | `copy_skipped=119`; queued guest copies 100/100 verified | +| MS-20261002-08 | P6b wave 1 (guest CE forwarding) | 2026-10-02 | `req=42 ok=42 verify_ok=42`; 512-pair scratch-pool rule | +| MS-20261002-09 | P6a guest-page DMA proven on the physical engine | 2026-10-02 | G2H + W2G 4 KiB with semaphore; guest observed the write | +| MS-20261002-10 | P6 guest-memory first attempts | 2026-10-02 ~02:42 | resolved by P6a | +| MS-20261002-11 | P5 wave 1 guest-submission shadow execution | 2026-10-02 ~02:00 | 514 real CE copies; 194 coalesced drops | +| MS-20261002-12 | P3 read-only control forwarding | 2026-10-02 01:33 | `ok=22 fallback=2 timeout=0`; smoke green | +| MS-20261002-13 | P2 guest-facing identity overlay | 2026-10-02 01:20 | live Product Name A/B (overlay 4x/0) | +| MS-20261002-14 | G0 freeze-fix bundle validated live | 2026-10-02 ~00:57 | 2/2 green cycles; committed `56766b2` | +| MS-20261002-15 | P1 mdev-owned lifecycle (fail-open) | 2026-10-02 ~00:58 | retested after G0; no freeze | +| MS-20261001-01 | Guest-wiring rung opened (P1) | 2026-10-01 22:00 | backend-session API inert; baseline re-verified | +| MS-20261001-02 | Submission rung: real CE copy on the physical engine | 2026-10-01 17:24-21:05 | `REAL COPY OK`; clean release each cycle | +| MS-20261001-03 | GSP-RM serves the static-config RPC | 2026-10-01 16:45 | `rpc_result=0`; 1656-byte config; graceful unload | +| MS-20261001-04 | Driver-API baseline re-verified green | 2026-10-01 14:20 | `ALL OK` 64/64; `faked=0 miss=0 stale=0` | +| MS-20261001-05 | First real GSP boot + live GSP-RM session | 2026-10-01 | WPR2 up; RISC-V active; first event `0x1002` | +| MS-LEGACY-01..06 | Emulation-campaign milestones (pre-2026-10-01) | 2026-09-26..09-30 | details in the milestone log/archive below | -- **First real guest CUDA program exercised against the physical backend +## Milestone log - current frontier and recent milestones (granular IDs in each entry) + +- `MS-20261002-01` **First real guest CUDA program exercised against the physical backend (2026-10-02 ~22:01).** A fresh supervised module-owned cycle (claim + full GSP-RM boot + guest RAM + hardware-primary work forwarding) booted the real server (`stage 3e complete`, init event `0x1001`, `rpc_result=0` in ~952 ms), @@ -95,7 +153,7 @@ chain succeeds. mdev removed, protected region down, host driver healthy, pstore empty). This is the first time a real CUDA program ran that far in the guest against the real backend; it is not yet a green execution. -- **GR context campaign: the promote-side wall is root-caused; the current +- `MS-20261002-02` **GR context campaign: the promote-side wall is root-caused; the current wall is the shared-management GR object alloc (2026-10-02 ~17:35).** The long-running context-promote wall was the ladder's virtual address space being allocated as *externally owned* (the client owns page tables, VA @@ -129,7 +187,7 @@ chain succeeds. enumerates and reads device properties, then stalls in its first device allocation / UVM channel path before any kernel executes - the first green guest execution remains the immediate goal. -- **Guest-submission relay and fault relay landed, default off (P6c +- `MS-20261002-03` **Guest-submission relay and fault relay landed, default off (P6c C0/C1/C3, 2026-10-02 07:43-08:07).** With `gsp_phys_relay=1` the physical engine authors the guest CE copy and the release semaphore: the engine-authored release was observed in the guest page (`relays=64`), @@ -143,7 +201,7 @@ chain succeeds. relays forwarded-engine faults to the guest channel notifier/event path; it delivered a real CE0 fault live (default off), and the auxiliary event post reported `no_event`. The software path is unchanged with all parameters off. -- **Host GR ladder (H1) opened and hit the GR context wall (2026-10-02 +- `MS-20261002-04` **Host GR ladder (H1) opened and hit the GR context wall (2026-10-02 08:29).** The host resource-manager ladder is now selectable to the compute engine (`gsp_phys_rmcall_engine=1`, default off) and brings up a GR0 channel with a `TURING_COMPUTE_A` compute object on the live GSP-RM @@ -157,7 +215,7 @@ chain succeeds. fuse-locked WPR2) - see the top bullet. Current wall: commit the GR context and make the engine execute the submitted compute block. Next: host `vectorAdd`, then guest GR forwarding. -- **Experimental tooling: risc-v-gsp-rm-debugger (2026-10-02 08:36, in development).** +- `MS-20261002-05` **Experimental tooling: risc-v-gsp-rm-debugger (2026-10-02 08:36, in development).** `debug/risc-v-gsp-rm-vm/` boots GSP/uproc firmware in a RISC-V QEMU VM with a GDB stub and adds Frida-like runtime inspection, a scripting/plugin API and a Ghidra bridge for GSP-RM runtime-memory analysis and @@ -165,13 +223,13 @@ chain succeeds. changes); the in-development notices were removed 2026-10-02 after the tool audit and value validation; see [`docs/05-operations/03-risc-v-gsp-rm-vm.md`](docs/05-operations/03-risc-v-gsp-rm-vm.md). -- **P6a trigger-path fix landed (2026-10-02 07:10).** The debug copy +- `MS-20261002-06` **P6a trigger-path fix landed (2026-10-02 07:10).** The debug copy triggers now resolve the guest page host-physical address at trigger time through the fresh-scratch-line path, retiring the stale-PTE data-check failure (live: `GUEST G2H COPY OK`, `GUEST W2G COPY OK verify_ok=1`, PTE self-test and ladder `REAL COPY OK`, clean release). Forwarding was never affected (HPAs are resolved at submit time). -- **Guest CE copies are hardware-authored with verified data, with deferred +- `MS-20261002-07` **Guest CE copies are hardware-authored with verified data, with deferred exactly-once release (P6b waves 1.5 and 2, 2026-10-02, run p6w2d; superseded as a frontier by the P6c relay and H1 bullets above).** Wave 1.5 (run p6w15), with `gsp_phys_work_defer_release=1` (plus @@ -209,7 +267,7 @@ chain succeeds. The trigger-path fix, the P6c relay/fault relay and the host GR ladder have since landed (bullets above); the relay data-mismatch triage and the GR execution wall are the open items. -- **Guest CE copies run on the physical engine with verified data (P6b wave 1, +- `MS-20261002-08` **Guest CE copies run on the physical engine with verified data (P6b wave 1, 2026-10-02, run p6run13; superseded by the P6b wave-2 result above).** With `gsp_phys_work_forward=1 gsp_phys_guest_mem=1` during a full CUDA driver-API smoke, the sema service captures the first eligible copy per push @@ -239,7 +297,7 @@ chain succeeds. can theoretically race guest reuse within the submit window. Module sha256 `9d444908...`. **Next: P6b wave 2** (hardware-primary release relay), then **P6c** (real guest page tables/channels). -- **Guest-page DMA proven on the physical engine (P6a, 2026-10-02; superseded +- `MS-20261002-09` **Guest-page DMA proven on the physical engine (P6a, 2026-10-02; superseded by the P6b wave-1 result above).** The IOMMU/DMA reachability known-unknown is closed on the reference host: the physical GPU's IOMMU group is an identity domain (all-RAM identity) and the @@ -268,7 +326,7 @@ chain succeeds. `e1bce651...`. **Next: P6b** - forwarding guest submissions (page-directory shadowing, real guest channels, push forwarding) - landed afterwards (superseded by the P6b/P6c bullets above). -- **Guest-memory path (P6) first attempts (2026-10-02 ~02:42 onward; resolved +- `MS-20261002-10` **Guest-memory path (P6) first attempts (2026-10-02 ~02:42 onward; resolved by the P6a result above).** The rung puts guest memory into the real execution path: guest RAM is now a QEMU file-backed `pc.ram` region (`/dev/shm/vgpu-guest-ram`, 4096M, @@ -286,7 +344,7 @@ chain succeeds. latter was root-caused in-run). The P6 code was uncommitted at the time of that snapshot; the follow-up attempt resolved both defects (P6a result above). -- **Guest-submission shadow execution (P5 wave 1) landed and verified live +- `MS-20261002-11` **Guest-submission shadow execution (P5 wave 1) landed and verified live (2026-10-02 ~02:00-02:20).** Under the new `gsp_phys_work_forward` parameter (bool, default off) the sema pump requests ONE shadow copy for each eligible CE push through a dedicated ordered workqueue (`gsp-phys-work`), @@ -312,7 +370,7 @@ chain succeeds. (hardware-primary release relay with a non-applying decode mode; **subsumed** by P6b wave 2 + the P6c relay above); **P4** remains deferred. -- **Read-only control forwarding (P3) landed and verified live (2026-10-02 +- `MS-20261002-12` **Read-only control forwarding (P3) landed and verified live (2026-10-02 01:33-01:45).** `GSP_RM_CONTROL` (fn76) forwards a whitelist of side-effect-free queries (`BUS_GET_PCI_BAR_INFO`, `BUS_GET_INFO_V2`, `FB_GET_INFO_V2`, `INTERNAL_GMMU_GET_STATIC_INFO`) from the live GSP-RM @@ -332,7 +390,7 @@ chain succeeds. would change guest-visible handles and risk the currently green guest path. The P2/P3/P5-wave-1 bundles and the G0 freeze fix are now committed (`56766b2`, `82fc7a8`; HEAD `82fc7a8`). -- **Guest-facing identity wiring (P2) landed and verified live (2026-10-02 +- `MS-20261002-13` **Guest-facing identity wiring (P2) landed and verified live (2026-10-02 01:20-01:45).** On a module-owned live GSP-RM session the emulation overlays the server's real static configuration identity into the guest-facing `GET_GSP_STATIC_INFO` reply under the new @@ -354,7 +412,7 @@ chain succeeds. above). At that point guest work was not yet forwarded to the physical engine (the P6b/P6c forwarding results are above). The P2 bundle and the G0 freeze fix have since been committed (`56766b2`). -- **G0 fix bundle built after the P1 release-path freeze (2026-10-02 00:55).** +- `MS-20261002-14` **G0 fix bundle built after the P1 release-path freeze (2026-10-02 00:55).** The first P1 mdev-owned acceptance cycle ended in the known hard freeze (journal stops, pstore empty): the P1 release path ran outside the supervised D0 pre-lock window and the teardown restored `power/control=auto`, re-arming @@ -374,7 +432,7 @@ chain succeeds. `released 0000:01:00.0; rebound to nvidia (rc=1)`, exactly 5 `power domain left LOCKED` lines, 0 in-window rail cycles, host survived, pstore empty; module sha256 `dde36d92...`. Committed in `56766b2`. -- **Guest-wiring lifecycle (P1) implemented and exercised; retested after G0 +- `MS-20261002-15` **Guest-wiring lifecycle (P1) implemented and exercised; retested after G0 (2026-10-02 ~00:58).** A module parameter selects a lifecycle where the physical-GPU claim and boot are deferred to the first mediated device create and released on the last remove, run asynchronously and **fail-open**: if the @@ -404,7 +462,7 @@ chain succeeds. (`gsp_phys_gpu=1` claim+boot at load, proven); the P1 async claim stays fail-open. At that point no guest work was forwarded to the physical engine yet (the P6b/P6c forwarding results are above). -- **Guest-wiring rung opened (2026-10-01 22:00): the first backend-session +- `MS-20261001-01` **Guest-wiring rung opened (2026-10-01 22:00): the first backend-session API code is in the tree, inert.** A skeleton that exposes the live physical session to the device layer (session request entry points plus a static-info snapshot helper) was added behind the existing gates and does not yet alter @@ -418,7 +476,7 @@ chain succeeds. 3D acceleration, and a native-vs-guest performance program; and a host-side acceleration feasibility study) were produced and are kept in the local evidence archive (untracked). -- **Submission-rung milestone (2026-10-01 17:24-21:05): real work now executes +- `MS-20261001-02` **Submission-rung milestone (2026-10-01 17:24-21:05): real work now executes on the physical engine.** The host resource-manager client allocated a virtual-address-space object with a committed page directory, a channel-group object and a real copy-engine channel, bound and scheduled it, obtained a @@ -436,7 +494,7 @@ chain succeeds. [`docs/03-architecture/11-physical-backend-guest-wiring.md`](docs/03-architecture/11-physical-backend-guest-wiring.md)). Evidence: the local evidence archive (cycle 18 through cycle 25 runs, with per-wall reports). -- **Physical-backend milestone (2026-10-01 16:45): the host-owned GSP-RM +- `MS-20261001-03` **Physical-backend milestone (2026-10-01 16:45): the host-owned GSP-RM completes initialization on the real TU104 and serves the static-config RPC.** Three supervised cycles, no reboot, clean release each time: 1. the resource-manager log ring is now registered correctly (its libos @@ -459,11 +517,11 @@ chain succeeds. send RM RPCs and receive full replies, and shut it down gracefully. The next rung is to drive the guest-facing emulation from this backend. Evidence: the local evidence archive (cycles 11-13). -- **Driver-API baseline re-verified green on the current build** (2026-10-01 +- `MS-20261001-04` **Driver-API baseline re-verified green on the current build** (2026-10-01 14:20): the smoke test passes end-to-end (`ALL OK`, payload 64/64) with real releases (`faked=0`, `miss=0`, `stale=0`); the source-level root-cause campaign (cycles 4-10) did not regress it. -- **Physical-backend milestone (2026-10-01)**: the module-owned GPU path +- `MS-20261001-05` **Physical-backend milestone (2026-10-01)**: the module-owned GPU path reached the first successful secure-verification (FWSEC) carve of the protected region on the real TU104 under the verified D0 power-domain harness. The teardown of a FWSEC-only carve is not host-recoverable on this @@ -517,17 +575,17 @@ chain succeeds. failure was a zero server-side device-id caused by our unpacked identity payload - the PMU path itself was fine. The raw config-space identity fix completed initialization (see the milestone bullet above). -- **The full CUDA driver-API smoke test passes with correct data**: the client +- `MS-LEGACY-01` **The full CUDA driver-API smoke test passes with correct data**: the client completes `cuCtxCreate -> cuMemAlloc -> cuMemcpyHtoD -> cuMemcpyDtoH -> cuMemFree -> cuCtxDestroy` and prints `ALL OK`, with the 64-byte payload roundtripping exactly (64/64 verified in the guest). -- **The last two walls**: the GPU page-table walk's address decode (child +- `MS-LEGACY-02` **The last two walls**: the GPU page-table walk's address decode (child pointers and leaves were 16x too low; fixed with the real entry encoding - walks now succeed at scale) and the device-to-host copy (a copy-engine class transfer bound on a compute channel whose work was denied by the engine-type gate; trusted host-injected pushes may now execute copy-engine work as well). -- **Baseline green and quiet**: the payload roundtrip is back to 64/64 +- `MS-LEGACY-03` **Baseline green and quiet**: the payload roundtrip is back to 64/64 (`ALL OK`), the staging buffer's translation stays resolvable via pins streamed from the guest, and high-frequency diagnostics are now gated behind a default-off debug switch - a normal boot+run produces a ~520-line @@ -542,17 +600,17 @@ chain succeeds. the copy-engine decoder consults pinned translations before the scanned map, so every push-data operation executes (`memset=19473`, `ringops=10551` live). -- **The runtime client now initializes and reaches kernel launch**: with a +- `MS-LEGACY-04` **The runtime client now initializes and reaches kernel launch**: with a static-runtime `vectorAdd` client, `cudaGetDeviceCount`, `cudaSetDevice`, `cudaFree`, three `cudaMalloc`, the HtoD copies and `cudaGetLastError` all return success. The client then parks in `cudaDeviceSynchronize` - the launch is queued but never **executed**, because real execution requires the physical-GPU backend (the next project). -- **What fixed it**: the failing call was never a kernel call - it was the +- `MS-LEGACY-05` **What fixed it**: the failing call was never a kernel call - it was the clock-domain probe inside libcuda (two device attributes) whose three legacy clock controls were answered with zeros; they are now populated with exactly the fields the helper checks. -- **Real-release mode now reaches the same rc=0 chain**: the earlier +- `MS-LEGACY-06` **Real-release mode now reaches the same rc=0 chain**: the earlier per-push 8 s stall was two pump bookkeeping bugs (release detection keyed on an unchanged semaphore VA; rewind replays re-entering the CE grace path) - both fixed, and the runtime init completes in ~1 min with real releases. @@ -642,6 +700,72 @@ chain succeeds. work does not touch the driver binding and is unaffected. See [`docs/03-architecture/08-physical-gpu-integration.md`](docs/03-architecture/08-physical-gpu-integration.md). + +--- + +## Evidence pointers + +- **Module sha256 (latest runs):** `e5318ee0...` (Addendum 30, 2026-10-03, + large-push parser fix); earlier: P6b `5fb5bfbe...`/`9d444908...`, P6a + `e1bce651...`, P3 `30dfd311...`, P2 `69eac176...`, G0 `dde36d92...`, P5 + wave 1 `83952506...`; boot `12cf3a41-...` unchanged across Addenda 28-30. +- **Campaign detail:** `Handoff-vGPU-IO.md` Part 93 Addenda 26-30 (the + authoritative live narrative; this page is the dated public snapshot). +- **Local evidence archive:** `open-iov/backups/evidence-20261002-victory/` + (`*-victory8/9`, `14-guest-dmesg-fail-*`, `20-gsp-phys-state-*`); earlier + cycle 11-25 runs with per-wall reports (local archive, untracked). +- **Simulator sessions:** `debug/risc-v-gsp-rm-vm/dumps/gsp-boot-b1-run1/2`, + `b2-run1/2`, and `debug/risc-v-gsp-rm-vm/harness/qemu_line/` (docs/36 §4, + docs/37 §4.3). +- **Definition of done:** `docs/04-implementation/01-known-knowns-and-unknowns.md` + §4 + the open-gates section (archived below). + +## Archive - historical narrative (pre-2026-10-02) + +The sections from here down are the dated historical record kept for evidence; the current state is in the top sections of this page. The 2026-10-02 milestone-ladder mega-summary is preserved verbatim below. + +### Preserved mega-summary (2026-10-02 milestone-ladder cell, verbatim) + +> | CUDA kernel execution (`vectorAdd`) | 🟡 execution backend: physical ownership + firmware staging verified; the unbind/rebind freeze hazard is mitigated by the supervised D0 power-domain lock (3/3 green claim cycles; the 2026-10-01 P1 mdev-owned release path initially ran outside that pre-lock window and re-armed D3cold, freezing the host at 22:32:53 - the G0 fix bundle now latches the D0/D3cold locks by default; see the frontier section), and the real FB size (8 GiB) and boot layout are verified live. The supervised live secure-verification run raised the protected region (WPR2 up) for the first time, then proved a secure-verification-only carve is not host-recoverable on this fused part, so the fail-safe held the device and an approved reboot restored it (distribution driver rebound, GPU healthy). That one-way stage is now retired: the only device-write path is a single full boot lifecycle (secure verification -> bootloader -> RISC-V start -> resource-manager initialization), whose code is implemented in the module sources and has now run live: the first real boot raised WPR2, passed Booter Load and went RISC-V active, and the module received the first real GSP-RM message (event 0x1002); init-done (0x1001) and a clean release are the next steps (see the 2026-10-01 section). A second live boot on 2026-10-01 proved the full boot sequence again (protected region up, bootloader halt, RISC-V active, first GSP-RM message) and reached the resource-manager's inline CPU-sequencer request; the host executor rejected the real request because of an over-strict internal size cap, which was found with a raw read of the message in host memory, fixed, and rebuilt. Cycles 2-3 then ran the real sequencer and received init-done with an RM init failure (`0x59`), which reproduces on a verified matching firmware image; the server's own shutdown supplied the last handoff bit, so each failed cycle +> completed a **clean release without a reboot**. Cycles 4-10 then captured +> the resource-manager's own error journal and root-caused the failure: the +> boot queue was missing the two pre-boot initialization messages the matching +> driver queues before the kernel kick (a system-information message and a +> packed registry table). That handshake is now implemented and **verified +> live**: the registry transport and packing are proven (a display-disable key +> took effect in the server's journal), the GPU timer is programmed before +> the kick, and the chipset/bridge block of the system information is +> populated from live values. Init now reaches the server's **static-config +> collection** and fails there (`0x59`); the device-id override key was +> exercised with no change, and a display-disable experiment showed display is +> not the first error. A source-level review of the failing collector has now +> closed the host side: it reads none of the remaining host-supplied fields +> (the one registry key on its call graph can only clear an error), so the +> ACPI/workspace/zeroed-field leads are retired. The one hard dependency left +> runtime is enabled only after the collector and the protected region blocks +> host reads). **Cycles 11-13 resolved this wall**: the resource-manager log +> ring had never actually been mapped (its libos init-argument id was +> malformed), which is why the first error was invisible; with the ring live +> the failure traced to the server's chip device-id reading as zero because +> the pre-boot identity fields were sent unpacked instead of as raw PCI +> config-space dwords. Fixing the payload made the server report init-done +> with a zero result and serve the full static-config reply (GPU name, 8 GiB +> FB, internal handles, BAR PDE bases, engine caps), and graceful shutdown +> works through the unload RPC. Each of cycles 4-13 released the device +> cleanly with no reboot. Since 2026-10-01 17:24 the next rung has both code +> and live results: a host resource-manager client creates memory and +> channel-group objects on the booted server, allocates and programs the +> page-directory base of a virtual-address-space object, and **allocates a real +> channel** on the live server. In cycles 18-25 it went all the way to +> execution: the channel was bound and scheduled, a work-submit token was +> obtained, a copy work item was placed in its GPFIFO and the doorbell rung, +> and after the engine's fetch and method-path faults were diagnosed and fixed +> the physical copy engine ran it - a 4 KiB copy with the semaphore released +> and destination equal to source. Every cycle released the device cleanly +> with no reboot; the next rung is wiring the live physical session into the +> guest-facing mdev/vfio path +> ([`docs/03-architecture/11-physical-backend-guest-wiring.md`](docs/03-architecture/11-physical-backend-guest-wiring.md)). The guest-wiring rung then advanced: an inert backend-session API was added, and the mediated-device-owned lifecycle (P1, see the frontier section) was implemented and exercised - deferring the physical claim/boot to the first device create, releasing it on the last remove, with fail-open behaviour. The first P1 acceptance cycle exposed two defects - an asynchronous boot marked READY after a failed FWSEC boot, and the P1 release path freezing the host (see the frontier section). The G0 fix bundle (default-latched D0/D3cold locks, post-rebind readiness gate, pre-claim D0 settle, READY gated on a proven boot, pre-kick staging readback) is implemented, built, smoke-tested and now **validated live**: two module-owned supervised cycles (2026-10-02 ~00:57-00:58) were 2/2 green through claim, FWSEC, booter, RISC-V active, init-done (`0x1001`, `rpc_result=0` in ~955 ms) and a latched release with WPR2 down, exactly 5 `power domain left LOCKED` lines, 0 in-window rail cycles, host alive and pstore empty. The retested P1 mdev-owned path - the exact path that hard-froze on 2026-10-01 - now fails open safely (the READY gate releases the claim when no live GSP-RM session exists; no freeze), while the async FWSEC boot defect (`staging not visible before kick` -> `FWSEC/FRTS execution failed: -5`) remains open, quarantined off the critical path; guest wiring proceeds on the module-owned live session. The bundle was later committed (`56766b2`). Guest-wiring P2 (static-config identity overlay) has since landed and verified live (2026-10-02 01:20-01:45): with `gsp_phys_static_live=1` the guest's `nvidia-smi -q` Product Name comes from the live session (`NVIDIA GeForce RTX 2080 Super with Max-Q Design`) versus the emulated `NVIDIA GeForce RTX 2080 SUPER` with it off (A/B; overlay applied 4x/0), while the guest smoke stays GREEN `ALL OK` 64/64; one overlay-OFF full run hit a pre-existing emulated-path flake, watched. P3 (read-only control forwarding) has since landed and verified live (2026-10-02 ~01:33-01:45): the `gsp_phys_ctrl_forward` whitelist served 22 controls live with 2 fallbacks and 0 timeouts while the guest smoke stayed GREEN `ALL OK` 64/64; **P5 wave 1 (guest-submission shadow execution) has since landed and verified live** (2026-10-02 ~02:00-02:20): with `gsp_phys_work_forward=1` the sema pump shadow-executes eligible CE pushes on the physical engine through a dedicated ordered workqueue - 514 real 4 KiB copies during a GREEN guest smoke (`ALL OK` 64/64), 194 requests coalesced/dropped while one was in flight, 0 refusals, 0 timeouts - while the software release path stays authoritative and no guest memory or guest VA is touched; P6 has since proven guest-page DMA live (P6a, 2026-10-02: the physical copy engine copied 4 KiB to and from a real guest RAM page with the semaphore released while the guest smoke stayed GREEN `ALL OK` 64/64 and release stayed latched with no reboot) and then **P6b wave 1** (2026-10-02, run p6run13): with `gsp_phys_work_forward=1 gsp_phys_guest_mem=1` the sema service resolved real guest CE-copy operands to host physical addresses and executed them on the physical copy engine through module-built GMMU scratch slots (`req=42 ok=42 verify_ok=42 verify_fail=0`, 503 ladder shadow copies also ran) while the software release path stayed authoritative - guest CE copies now execute on hardware and are **hardware-authored with verified data** while the software copy remains the fail-closed fallback for unqueued or failed copies: wave 1.5 added deferred, exactly-once release (`engaged=75 applied=75`), and wave 2 added `gsp_phys_work_hw_primary=1`, which skips the software copy for forwarded ops (`copy_skipped=119`, `req=100 ok=100 verify_ok=100`, smoke `matched=64/64` `ALL OK`, 19 inline software fallbacks `copy_soft=19 copy_fail=0`, and a 121-copy fallback-recovery run with guest-RAM ranges unregistered). The guest smoke stayed GREEN `ALL OK` 64/64 and release stayed latched with no reboot. Real page-directory shadowing and guest channels (P6c: completion/fault relay, guest page tables, `vectorAdd`) have since been reached: guest CE copies and their releases are forwarded with release and fault relay in place (P6c C0/C1/C3, default off); the host GR ladder (H1) brings up a GR0 channel and a `TURING_COMPUTE_A` object, but the first push faults `CTXNOTVALID` (the external-VAS context was never committed) and the promote-binding attempt wedged GSP-RM (host assert, fuse-locked WPR2), so the fail-safe held the device until an approved reboot restored it (see the frontier section). Real guest page tables/channels and GR execution remain, along with the P6a trigger-page pinning follow-up, and P4 (alloc/free handle adoption) remains deliberately deferred. | + ## First real GSP boot + live GSP-RM session (2026-10-01) Cycle 09:53:27–09:54:59 on the supervised D0 harness (`--boot --keep`), @@ -839,8 +963,10 @@ a GDB stub and adds Frida-like runtime inspection, a scripting/plugin API and a Ghidra bridge for GSP-RM runtime-memory analysis and emulation-accuracy measurement. Status is tracked by the tool audit (`debug/risc-v-gsp-rm-vm/docs/27-TOOL-AUDIT.md`); the in-development notices -were removed 2026-10-02 after the 194-test suite and end-to-end value -demonstrations (`debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md`); see +were removed 2026-10-02 after the then-194-test suite and end-to-end value +demonstrations (`debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md`); the +2026-10-03 Phase S wave (device model `B0`-`B2`, A/B line, triage; 290 tests) +is summarised in the "Simulator workstream" section above (docs 35-40). See [`docs/05-operations/03-risc-v-gsp-rm-vm.md`](docs/05-operations/03-risc-v-gsp-rm-vm.md). | Wall | Fix | diff --git a/debug/risc-v-gsp-rm-vm/.gitignore b/debug/risc-v-gsp-rm-vm/.gitignore index 9099b44..f400d55 100644 --- a/debug/risc-v-gsp-rm-vm/.gitignore +++ b/debug/risc-v-gsp-rm-vm/.gitignore @@ -8,3 +8,17 @@ __pycache__/ *.pyc .run/ sdk/build/ + +# Simulator program: local upstream checkout is local-only. +# (The handoff lives at the workspace root: ../../../../handoff-simulator.md; +# the entry below is a safety net in case a local copy is recreated.) +handoff-simulator.md +Handoff-Simulator-Plan-B.md +gpgpu-sim/upstream/ +gpgpu-sim/build/ +gpgpu-sim/**/*.o +gpgpu-sim/**/*.so + +# S4 evaluation evidence: generated sessions are never tracked. +harness/qemu_line/sessions/ +harness/re_demo/sessions/ diff --git a/debug/risc-v-gsp-rm-vm/Makefile b/debug/risc-v-gsp-rm-vm/Makefile index d75d8cc..bbbf32a 100644 --- a/debug/risc-v-gsp-rm-vm/Makefile +++ b/debug/risc-v-gsp-rm-vm/Makefile @@ -1,7 +1,10 @@ # risc-v-gsp-rm-debugger convenience targets. The real work lives in the package. PYTHON ?= python3 +FW ?= ../../firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin +OUT ?= dumps/triage +TRIAGE_ARGS ?= -.PHONY: all demo test demo-run plugin probe-real clean info +.PHONY: all demo test demo-run plugin probe-real clean info config-sanity triage all: demo sdk @@ -11,9 +14,13 @@ demo: sdk: $(MAKE) -C sdk -test: demo +test: demo config-sanity $(PYTHON) -m unittest discover -s tests -t . +# GreenEye-GPGPU-Sim TU104 scaffold config consistency (no build required). +config-sanity: + $(PYTHON) gpgpu-sim/scripts/config_sanity.py + demo-run: demo ./risc-v-gsp-rm-debugger.sh demo --no-build @@ -28,6 +35,10 @@ plugin: probe-real: ./risc-v-gsp-rm-debugger.sh probe +# One-command new-version triage (docs/40); override FW/OUT/TRIAGE_ARGS as needed. +triage: + $(PYTHON) harness/triage.py --firmware $(FW) --out $(OUT) $(TRIAGE_ARGS) + clean: $(MAKE) -C firmware/uproc_demo clean $(MAKE) -C sdk clean diff --git a/debug/risc-v-gsp-rm-vm/README.md b/debug/risc-v-gsp-rm-vm/README.md index 61204af..1868a58 100644 --- a/debug/risc-v-gsp-rm-vm/README.md +++ b/debug/risc-v-gsp-rm-vm/README.md @@ -1,6 +1,8 @@ # risc-v-gsp-rm-debugger — GSP runtime analysis in a RISC-V QEMU VM > Status: implemented, documented and tested (audit: docs/27-TOOL-AUDIT.md; value demos: docs/25-VALUE-VALIDATION.md). +> The 2026-10-03 Phase S simulator docs (30–40: cycle model, device model, +> A/B verdict, triage) remain **in development** and are labelled as such. > Experimental tooling for simulating and inspecting the GSP firmware's > runtime memory, for `vgsp` accuracy measurements and GSP/uproc > reverse engineering. It is a development-only, out-of-tree tool: never a @@ -64,12 +66,16 @@ ghidra/ Ghidra scripts (export symbols, annotate hits, control bridg plugins/ example plugin, MCP server, RM-harness plugin + authoring guide examples/ Python instrumentation scripts and declarative probe specs harness/ automation harness (mutation, portability, version diff, - control discovery/reachability, value-validation demos) + control discovery/reachability, value-validation demos, + S4 A/B + new-version triage: sim_qemu_line.py, triage.py) rmharness/ host RM oracle wrappers + read-only capture harness +gpgpu-sim/ Phase G overlay scaffold (configs, patches, scripts; docs/30) tests/ unit tests + QEMU integration smoke tests docs/ full documentation set (design, scripting, plugins, accuracy, - portability playbooks, roadmap, service/injection guides) + portability playbooks, roadmap, service/injection guides, + simulator device model/cycle/A-B/review docs 30-40) dumps/ recorded sessions (gitignored) +SIMULATOR-WORK-BOUNDARY.txt scope contract for the 2026-10-03 simulator wave ``` ## Documentation map @@ -106,8 +112,23 @@ dumps/ recorded sessions (gitignored) | [27-TOOL-AUDIT](docs/27-TOOL-AUDIT.md) | tool audit: purpose, tests, usage, status | | [28-PORTING-AUTOMATION](docs/28-PORTING-AUTOMATION.md) | driver-version sweep and new-device scaffolds (pilot, dry-run default) | | [29-PACKAGING-AND-TRIALING](docs/29-PACKAGING-AND-TRIALING.md) | trial simulation packages (pilot) | +| [30-GPGPU-SIM-INTEGRATION](docs/30-GPGPU-SIM-INTEGRATION.md) | GPGPU-Sim overlay: scaffold, build port, admission drafts (Phase G) | +| [31-TURING-TU104-PARAMETERS](docs/31-TURING-TU104-PARAMETERS.md) | Turing TU104 parameter dossier (public sources) | +| [32-GSP-CYCLE-MODEL](docs/32-GSP-CYCLE-MODEL.md) | modeled per-instruction cycle accounting (not silicon timing) | +| [33-AMPERE-PARAMETERS](docs/33-AMPERE-PARAMETERS.md) | Ampere parameter dossier (public sources) | +| [34-BLACKWELL-SOURCES](docs/34-BLACKWELL-SOURCES.md) | Blackwell source classification (public/private leads) | +| [35-GREENEYE-SIMULATOR-DESIGN](docs/35-GREENEYE-SIMULATOR-DESIGN.md) | standalone GSP simulator design; Phase S/G scope and gates | +| [36-GSP-DEVICE-MODEL](docs/36-GSP-DEVICE-MODEL.md) | S1 device model, versioned profiles, boot ladder B0–B2 | +| [37-SIM-QEMU-LINE](docs/37-SIM-QEMU-LINE.md) | simulator-vs-QEMU A/B harness and measured line verdict | +| [38-GSP-EM-API](docs/38-GSP-EM-API.md) | TLV boundary between the GSP emulator and a system simulator (design) | +| [39-RE-WORKFLOW-REVIEW](docs/39-RE-WORKFLOW-REVIEW.md) | reverse-engineering workflow review: time sinks and upgrades | +| [40-NEW-VERSION-TRIAGE](docs/40-NEW-VERSION-TRIAGE.md) | one-command new-version triage (`harness/triage.py`, `./risc-v-gsp-rm-debugger.sh triage`) | +| [41-STABILITY-AND-TIME-TO-VALUE](docs/41-STABILITY-AND-TIME-TO-VALUE.md) | formal stability/value report: phased improvements, proof, adversarial review, conditions | | [RISC-V-GSP-RM-DEBUGGER-ROADMAP](RISC-V-GSP-RM-DEBUGGER-ROADMAP.md) | the long-term program (H1–H7) | +Scope contract for the current simulator wave: +[`SIMULATOR-WORK-BOUNDARY.txt`](SIMULATOR-WORK-BOUNDARY.txt). + <!-- change (2026-10-02): add docs 28/29 to the documentation map; scope: doc map only; benefit: new tool-tree documents are linked from the tree front door. --> diff --git a/debug/risc-v-gsp-rm-vm/SIMULATOR-WORK-BOUNDARY.txt b/debug/risc-v-gsp-rm-vm/SIMULATOR-WORK-BOUNDARY.txt new file mode 100644 index 0000000..d79c7bf --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/SIMULATOR-WORK-BOUNDARY.txt @@ -0,0 +1,31 @@ +SIMULATOR WORK BOUNDARY — risc-v-gsp-rm-debugger + +This directory (open-iov/debug/risc-v-gsp-rm-vm/) is the ONLY place the +simulator workstream writes. Every file change, test, session and document +produced by this workstream stays inside it (the private handoffs at the +workspace root are pointer/canonical copies, not project changes). + +NOT TOUCHED, EVER, WITHOUT EXPLICIT HUMAN APPROVAL: + - kernel/ (including vgsp.ko and every kernel module) + - driver/, userspace/, firmware/ sources, config/, tools/ + - any file outside open-iov/debug/risc-v-gsp-rm-vm/ + +CONDITIONAL EXCEPTION (owner-granted 2026-10-03, session 4): + Backups of AGENTS.md, Handoff-vGPU-IO.md, STATE-CURRENT.md and + Handoff-gsp-accurate-emulation-tools.md exist under + HANDOFF-BACKUPS/simulator-wave-20261003/. Those four files may be edited + ONLY after: (a) measured time-to-value proof, (b) first and second + adversarial review pass, (c) changes are additive/trimming with solid + reasoning. Any other edit outside this directory remains deferred. + +DEFERRED UNTIL HUMAN APPROVAL (recorded in the simulator handoff): + The private live handoff lives at the workspace root (directory above + open-iov): /home/rmcore/Git/integdev_gpu_drv/handoff-simulator.md + (its §14.4 lists the deferrals). + - vgsp kernel-module changes for a simulator-backed VFIO/PCIe device + - any use of physical GPU / mdev / module load / guest / reboot + - promotion of any simulator result into product behavior + - edits to eng-ops, RUNBOOK, AGENTS.md or other project docs + +If a required change falls outside this directory, stop, record the deferral +in the handoff, and continue with everything that can be done inside. diff --git a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md index 0df8d08..50b4428 100644 --- a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md +++ b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md @@ -32,7 +32,7 @@ developer Frida-like instrumentation over its memory and execution: It is deliberately not a GDB wrapper: a pure-Python RSP client makes the whole stack scriptable, and QEMU is just the first execution backend. -## Status (v0.1, 2026-10-02) +## Status (v0.1 + 2026-10-03 simulator wave) | Capability | State | |---|---| @@ -44,24 +44,27 @@ whole stack scriptable, and QEMU is just the first execution backend. | Plugin loader + example plugin | working | | Native RV64IMAC simulator (M1, `--backend native`) | working; QEMU-parity differential test | | Native exec/access trace (M1.5, R4): `--trace`, analyzer coverage | working; trace blob + determinism test | -| Test suite | 194 tests green (live QEMU + native differential included) | +| Test suite | full suite green on the 2026-10-03 review host (298 tests incl. live QEMU + native differential; count varies with local prerequisites) | | Ghidra bridge | scripts + docs | -| Real GSP `.fwimage` probe (first-fault report) | working (honest: it does not boot without a TU104 model — see 06) | +| Real GSP `.fwimage` probe (first-fault report) | working (first fault + lenient continuation) | +| GSP device model + versioned profiles (S1) | in development: `sim/devices/`, `sim/profiles/`, `sim/gsp_boot.py`; B0–B2 on the real 570.144 image with 0 unknown MMIO (36) | +| Modeled cycle accounting | increment 0: `sim/cycles.py`; modeled defaults, not silicon timing (32) | +| Simulator-vs-QEMU A/B + new-version triage | working: frozen thresholds, verdict `above` on the S1 path per docs/37 §4.3; `harness/triage.py` (37, 40) | +| GPGPU-Sim overlay | Phase 0 scaffold + CUDA-12 build port record; Phase G gated (30) | | TCG full-memory tracing plugin | source + optional build (verified live) | | Automation harness (mutation/portability/version diff) + value demos | working (18/19/21/25) | | Host RM oracle harness (`rmharness`) | wrappers + capture working; device tables depend on driver permissions (17) | | First-touch device map + lenient native MMIO | working (24) | | MCP server (`plugins/mcp_server.py`) | working (20); stdlib-only, read-only by default | | Porting automation + trial packaging (28/29) | scaffolds + tests present; unadmitted pilots (dry-run default) | -| Native M2+ device model | roadmap only (08, 16) | The in-development notices were removed on 2026-10-02 after the tool audit ([27-TOOL-AUDIT](27-TOOL-AUDIT.md)), the 194-test green suite, and the end-to-end s01–s10 value demonstrations ([25-VALUE-VALIDATION](25-VALUE-VALIDATION.md)). Open items stay listed as -open: docs 28/29 are unadmitted pilots, the device model and custom simulator -M2+ remain roadmap, and nothing here is a stability guarantee beyond the -audited v0.1 surface. +open: docs 28/29 are unadmitted pilots, the device model is partial (S1, +B0–B2; docs/36) and custom-simulator M2+ remains roadmap, and nothing here is +a stability guarantee beyond the audited v0.1 surface. ## Quick start @@ -113,6 +116,17 @@ Interactively: | [27-TOOL-AUDIT](27-TOOL-AUDIT.md) | tool audit: purpose, tests, usage, status | | [28-PORTING-AUTOMATION](28-PORTING-AUTOMATION.md) | driver-version sweep and new-device scaffolds (pilot) | | [29-PACKAGING-AND-TRIALING](29-PACKAGING-AND-TRIALING.md) | trial simulation packages (pilot) | +| [30-GPGPU-SIM-INTEGRATION](30-GPGPU-SIM-INTEGRATION.md) | GPGPU-Sim overlay scaffold and build port (Phase G) | +| [31-TURING-TU104-PARAMETERS](31-TURING-TU104-PARAMETERS.md) | Turing TU104 parameter dossier | +| [32-GSP-CYCLE-MODEL](32-GSP-CYCLE-MODEL.md) | modeled cycle accounting (not silicon timing) | +| [33-AMPERE-PARAMETERS](33-AMPERE-PARAMETERS.md) | Ampere parameter dossier | +| [34-BLACKWELL-SOURCES](34-BLACKWELL-SOURCES.md) | Blackwell source classification | +| [35-GREENEYE-SIMULATOR-DESIGN](35-GREENEYE-SIMULATOR-DESIGN.md) | standalone GSP simulator design and gates | +| [36-GSP-DEVICE-MODEL](36-GSP-DEVICE-MODEL.md) | S1 device model, profiles, boot ladder | +| [37-SIM-QEMU-LINE](37-SIM-QEMU-LINE.md) | simulator-vs-QEMU A/B and measured verdict | +| [38-GSP-EM-API](38-GSP-EM-API.md) | GSP-emulator/system-simulator TLV boundary (design) | +| [39-RE-WORKFLOW-REVIEW](39-RE-WORKFLOW-REVIEW.md) | reverse-engineering workflow review | +| [40-NEW-VERSION-TRIAGE](40-NEW-VERSION-TRIAGE.md) | one-command new-version triage | | [RISC-V-GSP-RM-DEBUGGER-ROADMAP](../RISC-V-GSP-RM-DEBUGGER-ROADMAP.md) | long-term program H1–H7 | ## Where this fits in the Open-IOV workflow diff --git a/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md b/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md index ebc9226..a9a91aa 100644 --- a/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md +++ b/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md @@ -123,14 +123,17 @@ Before trusting a risc-v-gsp-rm-debugger session: ## 8. Known accuracy gaps (living list) -1. No TU104 device model: no boot beyond the first unmapped access. +1. Partial TU104 device model (S1, docs/36): the real 570.144 image is now + served past the first unmapped access to B2 (relocation copy, 0 unknown + MMIO) with a recorded boot shim; B3/B4 (CSB poll, message loop) are not + reached, and the model is uncalibrated and partial. 2. No time/concurrency model; multi-core and partition-switch behaviour is - approximated. + approximated. Cycles are modeled defaults, not measured (docs/32). 3. PMU/SEC2/ACR/Booter behaviour is message-level only. 4. Re-hosted uproc code is compiled for `qemu virt`, so compiler/layout differences with the shipped image must be accounted for when comparing control flow. 5. Interrupt delivery and MSI semantics differ; error-path M5 is the weakest - metric until the device model exists (08). + metric until interrupt delivery is modelled (docs/36 §6). Each gap maps to a roadmap item; do not silently ignore them in reports. diff --git a/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md b/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md index 46e28a4..0e5fede 100644 --- a/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md +++ b/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md @@ -127,7 +127,7 @@ the audit trail). | `initialize` | returns `protocolVersion` (echoes `2024-11-05`, `2025-03-26` or `2025-06-18`; otherwise the server default), `serverInfo`, `capabilities.tools` | | `notifications/initialized` | accepted silently (notifications never get a response) | | `ping` | `{}` | -| `tools/list` | the 24-tool catalogue with JSON input schemas | +| `tools/list` | the 31-tool catalogue with JSON input schemas | | `tools/call` | `{"name": ..., "arguments": {...}}` → `{"content":[{"type":"text","text":"<json>"}]}`, or `isError: true` for tool errors | JSON-RPC errors: `-32700` parse error, `-32600` invalid request, `-32601` @@ -144,6 +144,8 @@ them deterministically. | Tool | Arguments | Class | Notes | |---|---|---|---| | `session_info` | — | read | backend, PC, symbols, recording, breakpoints/watches, enabled capabilities | +| `backends_list` | — | read | QEMU/native availability and features; `active` backend; `launchable` (standalone only) | +| `session_launch` | `backend=native\|qemu`, `kernel`, `record=true`, `name`, `trace=false` | session | protocol-level backend selection; replaces the served session; **standalone mode only**; `trace` is native-only; allowed without `--allow-write` (it replaces the local served session and writes only its own recording; it never touches target memory) | | `symbols_resolve` | `target` | read | symbol, `symbol+0xNN` or hex address | | `bp_set` | `target`, `kind=sw\|hw` | observe | hits recorded as `bp_hit` | | `bp_clear` | `target` | observe | removes every breakpoint at that address | @@ -157,8 +159,13 @@ them deterministically. | `snapshot` | `addr`, `size` (max 16 MiB), `label=mcp` | read | zlib + sha256 into the session | | `call_function` | `target`, `args=[]`, `timeout=5.0` | **write** | requires `--allow-write`; Frida-like remote call | | `inject_service` | `service`, `slot=6`, `load_addr`, `cmd=1`, `arg0`, `arg1`, `path=remote\|firmware\|both`, `settle=0.25` | **inject** | requires `--allow-inject`; service ELF built with `make sdk` | +| `service_list` | — | read | services loaded into this session (injection registry) + the demo handler slots | | `trace_start` | `exec=true`, `start`, `end`, `max_events` | trace | native backend + recording session only | | `trace_stop` | — | trace | flushes `blobs/trace.jsonl`, returns counts | +| `sim_cycles` | — | read | native only: `insns`, modeled `cycles`, cycle-model table, fidelity label | +| `sim_device_state` | `limit=16` | read | native only: unknown-MMIO policy/log, device-map size, optional `sim/devices` model state | +| `sim_profile` | — | read | native only: firmware, bias, stop/profile metadata, trace config, model identity, session meta | +| `sim_regions` | `limit=16` | read | native only: RAM/UART windows, loaded firmware segments, device-map entries, observed unmodelled MMIO pages | | `analyze_current` | `max_rows=40` | read | `render_summary` for the active recording | | `compare_sessions` | `a`, `b`, `symbols=true`, `max_rows=40` | read | A/B recorded sessions | | `export_hits` | `session`, `out` | read | Ghidra `greeneye_hits.tsv` | @@ -231,7 +238,10 @@ recorded, and the session can be compared like any other run. 3. **Read-only by default.** `--allow-write` enables `write_mem` and `call_function`; `--allow-inject` enables `inject_service`. The agent's client config decides which manifest class it gets; a read/observe agent is - configured without those flags. + configured without those flags. Session-class tools (`session_info`, + `session_launch`) stay available without the flags: launching selects or + replaces the local served session (standalone mode) and never mutates + target memory. 4. **Sessions are the audit trail.** Writes, calls, injections and snapshots land in `events.jsonl`; run with `--record` (the trace tools require it). A finding without a session is not a finding. @@ -259,14 +269,16 @@ The tool list is stable regardless of what is present. ## 9. Testing ```bash -python3 -m unittest tests.test_mcp -v # 34 tests, no external deps +python3 -m unittest tests.test_mcp -v # 55 tests, no external deps ``` The suite drives `initialize`/`tools/list`/`tools/call` over in-memory streams (including unknown method/tool, parse errors, write/inject gating, no-stdout-pollution and structured `not_available`), plus a standalone subprocess test on the native backend that skips cleanly when the native -simulator is absent. +simulator is absent. Native-backend tools have additional end-to-end tests on +the bundled demo firmware (skipped when it is not built): `step`/`run` budgets, +`sim_cycles` accounting, `sim_regions` metadata and native backend detection. ## 10. Roadmap diff --git a/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md b/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md index fce7a96..0aefabd 100644 --- a/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md +++ b/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md @@ -254,7 +254,7 @@ flowchart LR All results are one JSON text block; absent modules return a structured `not_available` error; stdout carries protocol frames only (20 §4, §8). Full -argument schemas for all 24 tools are in 20 §5. +argument schemas for all 31 tools are in 20 §5. ## 9. Automation harness diff --git a/debug/risc-v-gsp-rm-vm/docs/30-GPGPU-SIM-INTEGRATION.md b/debug/risc-v-gsp-rm-vm/docs/30-GPGPU-SIM-INTEGRATION.md new file mode 100644 index 0000000..74830f2 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/30-GPGPU-SIM-INTEGRATION.md @@ -0,0 +1,104 @@ +# 30 — GPGPU-Sim integration (in development) + +> **Status: in development (Phase 0 scaffold).** This page describes a +> development-only overlay that is being prepared to give the debugger an +> off-hardware GPU execution substrate. It makes **no accuracy, timing, +> performance or support claim**, is **not calibrated to silicon**, and is +> not a product or guest component. Nothing here authorises GPU hardware, +> kernel-module, mdev or guest access. Thresholds for the capabilities below +> exist only as unfiled drafts in +> [`../gpgpu-sim/ADMISSION.md`](../gpgpu-sim/ADMISSION.md). + +## 1. Purpose + +QEMU (with the `vgsp` host module) and the native reference core under `sim/` +cover the debugger's instruction-level needs today. The next step is an +execution substrate that can host, on one deterministic clock, both a +GPU microarchitecture model and the GSP/microcontroller models the program +needs, while reusing the debugger's session format, hooks/watchpoints, Ghidra +bridge and MCP surface. + +GPGPU-Sim is the chosen base: it is open source (BSD-3-Clause, University of +British Columbia), data-driven, and already models Turing SM75 in its tested +configurations. The overlay in +[`../gpgpu-sim/`](../gpgpu-sim/README.md) tracks the project's changes to +upstream without forking its history: a pinned clone plus an ordered patch +set. + +## 2. Status (Phase 0) + +What exists today: + +| Item | State | +|---|---| +| Overlay layout + pinned upstream policy | done — [`../gpgpu-sim/UPSTREAM.md`](../gpgpu-sim/UPSTREAM.md) (pin `90ec339`, tag v4.0.1) | +| Idempotent setup/build script | done — `../gpgpu-sim/setup.sh` (clone/checkout, ordered patches, CUDA-`CUDA_INSTALL_PATH` build; `--no-build` supported) | +| Turing TU104 (RTX 2080 SUPER) config scaffold | done — `../gpgpu-sim/configs/turing-tu104-2080super/` | +| Config consistency test | done — `../gpgpu-sim/scripts/config_sanity.py`, wired into `make test` | +| Run wrapper for CUDA binaries | done — `../gpgpu-sim/scripts/run_sim.sh` (LD_LIBRARY_PATH substitution per upstream) | +| Admission records | drafts only — [`../gpgpu-sim/ADMISSION.md`](../gpgpu-sim/ADMISSION.md), pending maintainer filing | +| CUDA 12.x build/runtime port | **done (WP0.1)** — patches `0001`/`0002`; `../gpgpu-sim/BUILD-PORT.md` records the pinned CUDA 12.0 / gcc 13 build and `vectorAdd` functional+perf green | +| Calibrated TU104 model | **not done** (Phase 1) | +| GSP core + Turing MCU models | **not done** (Phase 1.5, behind device-map and model gates) | + +## 3. What the TU104 scaffold encodes, and how + +The config is derived from the upstream SM75_RTX2060 Turing config, with only +the device-level parameters public specifications distinguish. Values carried +from public sources: + +* 48 SMs (3072 CUDA cores at 64 FP32/SM) → `-gpgpu_n_clusters 48`; +* 8 GB GDDR6, 256-bit, 15.5 Gbps effective → recorded as machine-checkable + `# SPEC:` markers plus the DRAM model (burst 16, QDR ratio 4, DRAM command + clock 3875 MHz); **memory capacity itself is not modelled by GPGPU-Sim 4.x** + and is documented, not asserted; +* ~496 GB/s → the modeled module count/geometry is chosen so + `buswidth × ratio × DRAM clock × modules` equals 496 GB/s, and + `config_sanity.py` recomputes it; +* 4 MB L2 → cache geometry × modules × sub-partitions equals 4 MiB, also + recomputed by the test; +* 64 ROPs / 192 TMUs → recorded as `# SPEC:` markers (GPGPU-Sim 4.x has no + count knobs for these); +* base ~1650 MHz / boost ~1815 MHz → base clock active, boost mode commented; +* compute capability 7.5, PCIe 3.0 x16 → CC asserted; PCIe is not modelled. + +Anything not tied to a published number keeps an explicit `# UNKNOWN:` +marker (latencies, L1/L2/DRAM timing, address mapping, Booksim topology +size). See the config header for the full list. + +## 4. How to build and run + +```bash +cd open-iov/debug/risc-v-gsp-rm-vm/gpgpu-sim + +./setup.sh # clone pin + apply patches + build +python3 scripts/config_sanity.py +./scripts/run_sim.sh --help +./scripts/run_sim.sh ~/path/to/vectorAdd +``` + +`setup.sh` is idempotent, never uses `sudo`, and caps compile jobs at 4 by +default (`JOBS` overrides). The CUDA toolkit is selected with +`CUDA_INSTALL_PATH` (default `/usr/local/cuda`). On hosts whose CUDA is not +yet ported, the build step stops with a clear error; `--no-build` still +materialises the checkout and patches. + +## 5. What is NOT claimed + +* No accuracy, cycle, latency, bandwidth or correlation claim: the config is + a documented scaffold, and config-only modeling is treated as a failure + mode, not a result. +* No calibrated model, no GSP/MCU/fabric/guest simulation yet; the real GSP + image is not loaded by this overlay. +* No support or product claim of any kind; development-only tooling, never + shipped, never in a guest. +* No hardware access from any script in this overlay. + +## 6. Admission drafts + +The five draft records (build/run, TU104 calibration, GSP-in-sim, MCU +cross-controller, sim debugger backend) with scenario, measurable outcome, +baseline, threshold and failure signal live in +[`../gpgpu-sim/ADMISSION.md`](../gpgpu-sim/ADMISSION.md). They are drafts and +require maintainer filing under the project admission rule; until then these +capabilities are pilots at most. diff --git a/debug/risc-v-gsp-rm-vm/docs/31-TURING-TU104-PARAMETERS.md b/debug/risc-v-gsp-rm-vm/docs/31-TURING-TU104-PARAMETERS.md new file mode 100644 index 0000000..a68fc32 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/31-TURING-TU104-PARAMETERS.md @@ -0,0 +1,260 @@ +# 31 — Turing TU104 parameter dossier (Phase 1 model inputs) + +> **Status:** research dossier for the GreenEye-GPGPU-Sim Phase 1 model +> (WP1.1, `handoff-simulator.md` §5). Public-source only; every row carries a +> citation. Values that are **scaled** from a measured board to a different +> board or clock are marked `[scaled]`; values that are **inferred** rather +> than directly measured/published are marked `[inferred]`. Nothing in this +> document is a silicon-accuracy claim — measure before claiming (docs/06). +> +> **Scope:** modelling inputs for the full TU104 (RTX 2080 Super class, +> 48 SM) and the T4 board variant (40 SM) measured by the microbenchmarking +> literature. Not in scope: GSP/MCU parameters (docs/32+, Phase 1.5). + +## 1. Sources and citation labels + +| Label | Source | What it provides | +|---|---|---| +| **[T4]** | Z. Jia, M. Maggioni, J. Smith, D. P. Scarpazza, *Dissecting the NVidia Turing T4 GPU via Microbenchmarking*, arXiv:1903.07486 (2019), 65 pp. | Microbenchmarked TU104 (T4 40-SM board) memory hierarchy geometry/latency/bandwidth, instruction latencies, atomics, tensor-core throughput, SM scheduling/encoding facts. | +| **[TWP]** | NVIDIA, *NVIDIA Turing GPU Architecture* whitepaper, WP-09183-001_v01 (2018), https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/technologies/turing-architecture/NVIDIA-Turing-Architecture-Whitepaper.pdf | Per-SM composition, TU102/TU104/TU106 product tables, clock/bandwidth/L2/register-file, NVLink 2, tensor/RT core counts. | +| **[AMP]** | H. Abdelkhalik, Y. Arafa, N. Santhi, A.-H. Badawy, *Demystifying the Nvidia Ampere Architecture through Microbenchmarking and Instruction-level Analysis*, arXiv:2208.11174 (2022) | Ampere A100 numbers, used here only for **cross-generation comparisons** to Turing (global-memory latency 434 cycles attributed to Turing via the paper's reference [13]). | +| **[2080S]** | NVIDIA RTX 20-series product table as published on Wikipedia *GeForce 20 series* (retrieved 2026-10-03), citing NVIDIA product pages and TechPowerUp GPU database: https://en.wikipedia.org/wiki/GeForce_RTX_20_series | Board-level RTX 2080 Super (TU104-450) clocks, memory rate, bandwidth, power; TU104 die data. | +| **[VOLTA]** | Z. Jia et al., *Dissecting the NVIDIA Volta GPU Architecture via Microbenchmarking*, arXiv:1804.06826 (2018) | Volta comparison points quoted inside [T4]. | + +> **Provenance rule.** This document cites public material and recorded +> observations; register-level details are cross-checked against the tracked +> field catalogue. + +## 2. Method and scaling caveats + +1. **[T4] is a 40-SM, 16 GiB GDDR6 (10 Gbps) TU104 variant running at + 1,590 MHz** [T4 §3, Table 3.1]. The Phase-1 target is the full + **TU104-450 / RTX 2080 Super: 48 SM, 8 GiB GDDR6 (15.5 Gbps), 1650/1815 + MHz** [2080S]. +2. Cache geometry **per SM** and latencies **in cycles** are + microarchitecture properties and apply to all TU104 variants; per-chip + aggregate bandwidth and absolute GFLOPS/GB/s **must be scaled** by SM + count and clock as noted. All scaled rows are marked `[scaled]`. +3. [T4] measured "actual" bandwidths under its own microbenchmarks (pointer + chase / streaming loops). These are *achievable* numbers on that setup; + they are not peak specifications. Keep both rows distinct in the model. +4. Instruction-latency measurements are dependent-issue latencies on a + statically scheduled core; independent-issue throughput may differ + (see §7). +5. The GDDR6 DRAM **timing parameters** (tCL/tRCD/tRP/tRAS/refresh) are not + published for TU104; the model must use a JEDEC-class GDDR6 timing set + and treat it as a calibrated unknown (see §10). + +## 3. Chip and device parameters + +### 3.1 TU104 die / board + +| Parameter | Value | Source | Notes | +|---|---|---|---| +| GPU chip | TU104 | [TWP], [2080S] | Full die supports 48 SMs (Quadro RTX 5000, RTX 2080 Super); 46-SM (RTX 2080/2070S) and 40-SM (Tesla T4) also ship. | +| Transistors | 13.6 B | [TWP] Table 4, [2080S] | | +| Die size | 545 mm² | [TWP] Table 4, [2080S] | | +| Process | TSMC 12 nm FFN | [TWP] | | +| GPC / TPC / SM (full) | 6 GPCs / 24 TPCs / 48 SMs | [TWP] (Quadro RTX 5000 column) | 2 SMs per TPC. | +| T4 board | 5 GPCs / 20 TPCs / 40 SMs | [TWP] Table 5, [T4] Table 3.1 | The measured board. | +| Max graphics clock | 1,590 MHz (T4) | [T4] Table 3.1, [TWP] Table 5 | | +| Base/boost clock (2080 Super) | 1,650 / 1,815 MHz | [2080S] | Board-dependent; FE is 1,650/1,815, reference differs. | +| Base/boost clock (Quadro RTX 5000) | 1,620 / 1,815 MHz | [TWP] Table 4 | | +| Clock throttle behavior | compute workloads can throttle below app clock | [T4] §4.5 | Model needs a clock envelope, not a fixed clock. | + +### 3.2 SM (per-SM parameters; applies to all TU104 variants) + +| Parameter | Value | Source | Notes | +|---|---|---|---| +| FP32 CUDA cores / SM | 64 | [TWP] §TU102, Table 1/4 | 4 processing blocks × 16 FP32. | +| INT32 cores / SM | 64 | [TWP] §Turing SM | 4 blocks × 16 INT32; concurrent execution with FP32 on a separate datapath. | +| Tensor cores / SM | 8 (2nd gen) | [TWP] Table 4 | 2 per processing block. | +| RT cores / SM | 1 | [TWP] §TU102 | | +| Texture units / SM | 4 | [TWP] §TU102 | | +| Register file / SM | 256 KB (64 Ki × 32-bit) | [TWP] Table 4, [T4] Table 3.1 | 16,384 32-bit registers per processing block [T4] §3.5.1 = 64 Ki per SM. | +| Register banks | 2 banks, 64-bit wide | [T4] Table 3.1 | Dual-ported; conflicts only when all 3 FFMA sources are in the same bank [T4] §3.5.1. | +| Warp schedulers / dispatcher | 4 blocks each 1 scheduler + 1 dispatch unit | [TWP] §Turing SM, [T4] §2.2 | No dual issue observed [T4] §2.1. | +| Warp→scheduler mapping | `scheduler = warp_id % 4` | [T4] §2.2, Table 2.1 | Same rule as Volta. | +| Threads / SM (max) | 1,024 (32 warps) | [T4] Table 3.1 | This is **half** of Volta's 2,048 [T4 §1]. | +| Independent FP32+INT32 issue | yes | [TWP] §Turing SM | The key Turing SM addition. | +| L1 data / shared memory (unified) | 96 KB configurable | [TWP] §TU102 | Whitepaper figure for the TU10x SM. | +| Shared memory (max user configuration) | 64 KB (T4 measured config) | [T4] Table 3.1, §3.1.2 | Whitepaper: 32 or 64 KB L1 data usage [T4 §3.1.2]; model as 0–64 KB shared partitioning of a 96 KB unified array. | +| L0 instruction cache | ~16 KB | [T4] Table 3.1 | | +| L1 instruction cache | ~46 KB | [T4] Table 3.1 | | + +## 4. Memory hierarchy (cycle latencies and geometry) + +All cycle numbers are measured on the T4 at 1,590 MHz; latencies in cycles are +treated as architecture-level (see caveat §2.2). "Hit latency" rows are +dependent-issue latencies. + +### 4.1 Latency table + +| Access | Latency (cycles) | Source | Notes | +|---|---|---|---| +| L1 data cache hit | 32 | [T4] Table 3.1, Fig. 3.5 | Volta 28, Kepler 35. | +| L1 data – same line secondary access | 32 | [T4] Fig. 3.5 | | +| L2 hit (L1 miss) | ~188 | [T4] Table 3.1, Fig. 3.5 | Volta ~193. | +| L2 miss, TLB hit (DRAM) | 296 | [T4] Fig. 3.5 | | +| L2 miss + TLB miss (first touch) | 616 | [T4] Fig. 3.5 | First access in the chase. | +| Global memory (Ampere paper cross-check) | 434 (Turing) vs 290 (Ampere A100) | [AMP] §V-B, Table IV, citing [13] | Independent corroboration of the Turing DRAM latency band. | +| L1 constant broadcast | ~26 | [T4] Table 3.1 | | +| L1.5 constant broadcast | 92 | [T4] Table 3.1 | ~46 KiB L1.5 const cache. | +| L2 constant broadcast | ~215 | [T4] Table 3.1 | | +| Shared memory no-conflict | 19 | [T4] Table 3.1 | Volta 19. | +| Atomic shared (no contention) | 8 | [T4] Table 4.2 | see §7.3. | +| Atomic global (no contention) | 76 | [T4] Table 4.2 | T4 is slower than V100 (36). | + +### 4.2 Geometry and bandwidth + +| Parameter | Value | Source | Notes | +|---|---|---|---| +| L1 data size (nominal) | 32 or 64 KiB | [T4] Table 3.1, §3.1.2 | | +| L1 detectable size | "7 KiB short of nominal"; misses observed once working set > 25 KiB with 64 KiB shared configured | [T4] §3.1.2, Table 3.3 | Unexplained replacement-policy effect; model as effective capacity < nominal (calibrate). | +| L1 line size | 32 B | [T4] Table 3.1 | load granularity 32 B; update granularity 128 B. | +| L1 load bandwidth (measured) | 58.83 B/cycle/SM | [T4] §3.1.1, Table 3.2 | P4 15.7; V100 108.3. `[scaled]` per-chip ×SM×clock. | +| L1 load bandwidth (theoretical) | LSU count (16) × bytes/inst | [T4] §3.1.1 | 16 LSU per SM on Turing vs 32 on Volta [T4 §1]. | +| L2 size | 4,096 KiB | [TWP] Table 4, [T4] Table 3.1 | 16-way set-associative, 64 B line [T4] §3.2. | +| L2 latency | ~188 cycles | [T4] Table 3.1 | | +| L2 load throughput (measured) | 1,270 GB/s (T4) | [T4] Table 3.4 | V100 2,155 GB/s; P4 979 GB/s. `[scaled]` for 48 SM/clock. | +| L2 read sectors per request | not published | — | open (see §10). | +| Constant caches | L1 const 2 KiB, 8 sets, 4-way, 64 B line; L1.5 ~46 KiB, 256 B line | [T4] Table 3.1 | | +| L1 TLB coverage | 32 MiB, 2 MiB pages | [T4] Table 3.1 | | +| L2 TLB coverage | ~8 GiB, 32 MiB pages | [T4] Table 3.1 | | +| Shared memory per SM | 32 or 64 KiB | [T4] Table 3.1 | 32 banks/SM, 4 B bank width. | +| Shared memory per chip (T4) | 1,280 or 2,560 KiB | [T4] Table 3.1 | `[scaled]`: ×48/40 for full TU104. | +| Shared throughput (T4 theoretical / actual) | 4,070 / 3,662 GiB/s | [T4] Table 3.1 | Chip-level, as published. | +| LSU count / SM | 16 | [T4] Table 3.1 | | +| DRAM type (T4) | GDDR6, 5,001 MHz, 256-bit | [T4] Table 3.1 | | +| DRAM bandwidth (T4 theoretical / actual) | 320 / 220 GB/s | [T4] Table 3.1 | Actual = 68.8% of theory. | +| DRAM bandwidth (2080 Super) | 496 GB/s @ 15.5 Gbps, 256-bit | [2080S] | Theoretical; model the achievable/theoretical ratio as a calibrated parameter. | +| Frame buffer (T4 / 2080 Super) | 16,384 MiB / 8,192 MiB | [T4] Table 3.1, [2080S] | Size does not affect timing beyond capacity. | +| NVLink 2 (TU104, where enabled) | 2 x8 links, 25 GB/s per direction each → 100 GB/s aggregate bidirectional | [TWP] §Second-Generation NVLink | Consumer RTX 20 cards do not expose it; model seam only (Phase 4). | + +## 5. Compute throughput (board-level, as published) + +| Metric | T4 (40 SM, 1,590 MHz) | Source | 2080 Super scaling `[scaled]` | +|---|---|---|---| +| Peak FP32 | 8.1 TFLOPS | [TWP] Table 5 | ×(48/40)×(1815/1590) ≈ **11.1 TFLOPS** — matches [2080S] 10.138/11.151 (base/boost), confirming the scaling rule. | +| Peak FP16 (non-tensor) | 16.2 TFLOPS | [TWP] Table 5 | ~22.2 TFLOPS scaled. | +| FP64 | 253 GFLOPS (1/32 of FP32; 2 FP64 units/SM) | [T4] Table 4.3, [TWP] note | 1/32 rate is architectural [TWP §TU102 note]. | +| FP32 (cuBLAS measured) | 7,174 GFLOPS | [T4] Table 4.3 | Achieved not peak. | +| FP16 (cuBLAS measured) | 41,616 GFLOPS | [T4] Table 4.3 | Tensor path. | +| INT8 (measured) | 74,934 GOPS | [T4] Table 4.3 | | +| INT4 (measured) | 114,384 GOPS | [T4] Table 4.3 | | +| INT1 (measured) | 552,230 GOPS | [T4] Table 4.3 | | +| FP16 tensor (with FP16 acc), published peak | 65 TFLOPS (T4) | [TWP] Table 5 | | +| FP16 tensor (with FP32 acc), published peak | 65 TFLOPS (T4) | [TWP] Table 5 | | +| INT8 tensor peak | 130 TOPS (T4) | [TWP] Table 5 | | +| INT4 tensor peak | 260 TOPS (T4) | [TWP] Table 5 | | +| RTX 2080 (not Super) tensor peaks | 80.5/84.8 FP16-acc; 40.3/42.4 FP32-acc; 161.1/169.6 INT8; 322.2/339.1 INT4 (ref/FE) | [TWP] Table 4 | Use RTX 2080 column only as a cross-check; 2080 Super is 48 SM like Quadro RTX 5000 (89.2 TFLOPS FP16-acc at FE clocks). | + +## 6. Tensor core instruction behavior + +| Item | Value | Source | Notes | +|---|---|---|---| +| Unit | 2nd-gen Tensor Core, 8/SM | [TWP] | Supports FP16 inputs (FP16/FP32 acc) plus INT8/INT4/INT1 inference modes. | +| `wmma.mma.sync` m16n16k16 (FP16) | 4 × `HMMA.1688.F32` on Turing (vs 16 × `HMMA.884` on Volta) | [T4] §4.3 | Instruction count per warp-level primitive. Model issue/interconnect at SASS level. | +| SASS shapes in use | `HMMA.1688`, `HMMA.884` | [AMP] §V-C (Turing comparison) | | +| Sparsity | not present on Turing | [AMP] §I vs Ampere | Ampere adds 2:4 sparsity [A100 whitepaper]; model as Turing-absent. | +| Tensor instruction latency | not tabulated per-instruction in [T4] | — | open; calibrate from SASS issue model (see docs/06 metric C1). | + +## 7. Instruction latency and throughput + +### 7.1 Native instruction latency (dependent issue), cycles + +From [T4] Table 4.1: + +| Instruction class | Turing | Volta | Pascal | +|---|---|---|---| +| IADD3, SHF, LOP3, SEL, MOV, FADD, FFMA, FMUL, ISETP, FSET, FSETP | **4** | 4 | 6 | +| IMAD, FMNMX, DSET, DSETP | **5** | 5 | 6 / 12 | +| HADD2, HMUL2, HFMA2 | **6** | 6 | 6 | +| POPC, FLO, BREV, MUFU | **~15** | ~10–14 | ~14 | +| DADD, DMUL | **~48** | 8 | 8 | +| DFMA, DSET, DSETP | **~54** | 8 | 8 | +| IMUL, IMAD (Pascal only) | — | — | ~86 (emulated) | + +Notes: Turing reduces integer/SP latency vs Pascal, but **not** vs Volta; +FP64 latency regresses sharply (48–54 cycles) because only 2 FP64 units/SM +exist and are optimized for correctness [T4] §4.1, [TWP] note. + +### 7.2 Independent-issue throughput + +[T4] §4.1 states FP32 throughput is limited by latency and register-bank +conflicts, not by a hard issue limit; long sequences of identical FFMA show +2-cycle stalls on some GPUs [T4] §4.1 setup note. The model should use the +4-cycle dependent FFMA latency and the measured B/cycle numbers rather than +assuming full dual-issue. + +### 7.3 Atomic latency (cycles), [T4] Table 4.2 + +| Contention | Shared T4 | Shared V100 | Global T4 | Global V100 | +|---|---|---|---|---| +| none | 8 | 6 | 76 | 36 | +| 2 threads | 10 | 7 | 72 | 31 | +| 4 threads | 14 | 11 | 73 | 32 | +| 8 threads | 22 | 18 | 81 | 41 | +| 16 threads | 37 | 24 | 97 | 58 | +| 32 threads | 69 | 66 | 116 | 76 | + +### 7.4 Instruction encoding facts relevant to the model + +* Turing and Volta instructions are **128-bit** (opcode + 64-bit control + section) [T4] §2.1; opcode in the least-significant bits of the first + 64-bit word, 10–13 bits long [T4] §2.3.1. +* Control fields: 4-bit reuse flags, 6-bit wait-barrier mask, read/write + barrier indices, yield flag, 4-bit stall cycles [T4] §2.1. +* 6 dependency barriers for variable-latency instructions [T4] §2.1. +* Reuse CAM: 2-way associative per source-operand slot, up to 4 operands + [T4] §2.1. +* A warp switched to another warp costs one extra cycle [T4] §2.1 (yield). + +## 8. Differences T4 (40 SM) vs full TU104 (48 SM) — scaling summary + +| Quantity | Rule | Worked example | Notes | +|---|---|---|---| +| SM-count quantities (CUDA/tensor/RT/L1/shared totals) | ×48/40 = 1.2 | 2,560 → 3,072 CUDA cores | [TWP] confirms 3,072 for Quadro RTX 5000. | +| Compute rate | ×1.2 × (clock ratio) | FP32 8.1 → 11.1 TFLOPS at 1,815 MHz | Matches [2080S] published 11.151. | +| Shared/L1 bandwidth per chip | ×1.2 × clock ratio | 4,070 → ~5,575 GiB/s at 1,815 MHz (≈6.0 TB/s) | Treat as bound; calibrate. | +| L2 size, latency | unchanged | 4 MB, 188 cycles | Architecture-level. | +| DRAM bandwidth | board-specific | T4 320; 2080 Super 496 GB/s | Do **not** scale T4 DRAM numbers. | +| Memory latencies (cycles) | unchanged | 32/188/296/616 | Architecture-level. | + +## 9. What the model can safely assert now vs what needs calibration + +* **Assert (public evidence):** SM composition, cache geometry, cycle + latencies, instruction latency table, atomic table, tensor instruction + decomposition, scheduler mapping, encoding control fields. +* **Calibrate (unknowns/board-dependent):** DRAM timings and effective + bandwidth ratio, L1 effective capacity (7 KiB discrepancy), replacement + policy behavior, crossbar/NoC topology and per-hop latencies, L2 + sectoring/partition behavior, throttling envelope, tensor-instruction + latency. + +## 10. Open / uncertain items (do not invent) + +1. TU104 GDDR6 timing set (tCL/tRCD/tRP/tRAS, refresh, bank groups) — not public. +2. L2 sector/queue/partition microarchitecture and per-request serialization. +3. L1 replacement policy (the 7 KiB effect in §4.2 is unexplained by [T4]). +4. Exact crossbar (NoC) latency from SM to L2/FB partitions. +5. Tensor-core per-instruction latency/throughput as a function of HMMA shape + beyond the wmma-level decomposition. +6. Max boost behavior under power/thermal limits for the specific + 2080 Super Max-Q board (the local card variant throttles at 80 W, well + below the desktop 250 W part [2080S]); the model must not assume desktop + clocks for a Max-Q profile. +7. NVLink 2 availability on TU104 consumer boards (present on the die, not + exposed by RTX 20 cards) — model as a Phase-4 seam only. + +## 11. Validation hooks (docs/06 C-metrics) + +* C1 instruction-latency agreement: use §7.1 table (target ≥85% within + ±1 cycle). +* C2 memory-latency agreement: use §4.1 table (target ≥80% within ±15%). +* C3 bandwidth agreement: use §4.2/§5 achieved numbers (target ≥80% within + ±20%). +* C4 kernel correlation: [T4] reports cuBLAS/CUTLASS matrix-multiply + throughputs (Table 4.3) that can seed a smoke suite. +* Any physical capture on the local TU104 board is supervised-only + (RUNBOOK P4/P7) and is never an unattended-agent requirement. diff --git a/debug/risc-v-gsp-rm-vm/docs/32-GSP-CYCLE-MODEL.md b/debug/risc-v-gsp-rm-vm/docs/32-GSP-CYCLE-MODEL.md new file mode 100644 index 0000000..018e07c --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/32-GSP-CYCLE-MODEL.md @@ -0,0 +1,187 @@ +# 32 — GSP cycle model (increment 0, in development) + +> **Status: in development (Phase 1.5, work package WP1.5.1 increment 0).** +> This page describes a table-driven **modeled** cycle count added to the +> native reference core (`sim/`) and a bounded, deterministic "cycle-0" +> profile of the real 570.144 GSP firmware. It makes **no silicon timing, +> performance, latency or accuracy claim**: the numbers are modeled defaults +> under an explicit table, not measurements. The simulator's fidelity +> statement is unchanged — L0 (instruction semantics) and L1 (memory +> behaviour) are modelled; **L2 (time/concurrency) is not** (`06-ACCURACY.md`). +> Development-only: nothing here authorises GPU hardware, kernel modules, +> mdev or guest access. + +## 1. Purpose + +The native simulator reports virtual time as an instruction count (`insns`). +That is enough for functional debugging but not for comparing instruction +mixes, for calibration-table work, or for a first, honest cycle-shaped +artifact from a real firmware run. Increment 0 adds: + +* `sim/cycles.py` — an isolated, per-instruction-class latency table with a + deterministic cost function; +* a `cycles` counter on `sim.machine.Machine`, advanced by that table + alongside the existing instruction count; +* `sim/gsp_profile.py` — a bounded, read-only run of the real + `firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin` that records a normal + `greeneye-session/1` directory with the modeled cycle totals; +* `tests/test_sim_cycles.py`. + +Everything is stdlib-only Python 3.10+, offline, and read-only with respect to +the firmware image. + +## 2. The model + +`sim/cycles.CycleModel` is a plain Python table. All defaults are **modeled** +from public, generic in-order RISC-V pipeline assumptions — one cycle for +plain ALU work, conservative multi-cycle M-extension costs, a fixed extra +latency for a completed memory access — and are deliberately *not* derived +from the GSP core, from silicon or from vendor data. + +| Class | Default | Notes | +|---|---|---| +| ALU / shift / `lui` / `auipc` / op-imm | 1 | add, sub, logical, compare | +| branch | 1 | taken/not-taken penalty deliberately not modelled | +| `jal` / `jalr` | 1 | | +| `mul` / `mulh` / `mulhsu` / `mulhu` | 3 | conservative | +| `div` / `divu` / `rem` / `remu` | 20 | conservative, data-independent | +| load / store | 1 | plus a completed-access latency (below) | +| AMO (`lr`/`sc`/`amo*`) | 2 | plus one completed-access latency | +| CSR | 2 | | +| trap (`ecall`/`ebreak`/`mret`) | 3 | | +| `fence` / `fence.i` | 1 | no ordering model (single hart) | +| illegal | 1 | execution stops immediately | + +Memory latency is a **parameter**, not a cache model: + +| Parameter | Default | Meaning | +|---|---|---| +| `memory_latency` | 3 | extra cycles when a RAM access completes | +| `mmio_latency` | 12 | extra cycles when an unmodelled-MMIO access completes | + +The cost of one instruction is `instruction_cost(kind, sub)`; a completed +load/store/AMO adds `memory_cost(is_mmio)` on top. Cost depends only on the +decoded instruction class (`sim/decode.py` kind/sub) and the access region: +no host clock, no entropy, no caches, no hazards. `CycleModel.describe()` +emits the full table for session metadata. + +## 3. Machine integration + +`sim.machine.Machine` keeps its previous behaviour exactly; cycles are +additive: + +```python +from sim.machine import Machine + +m = Machine() # DEFAULT_MODEL active +m.run(10_000) +m.insns # virtual time (unchanged semantics) +m.cycles # modeled cycles, advanced by the table +m.cycle_model.name # "rv64imac-modeled/1" + +off = Machine(cycle_model=False) # cycles stays 0; disabling path for A/B +slow = Machine(cycle_model=CycleModel(memory_latency=9)) +``` + +`greeneye.sim_backend.launch_native(..., cycle_model=...)` threads the choice +through the Session API (`None` = default table, `False` = off, or an +instance). The counter increments for the instruction the machine stops on +under a watchpoint/breakpoint, mirroring how `insns` counts it; the memory +latency is only added when the access actually completes. Determinism is by +construction: two identical runs produce identical instruction and cycle +totals (tested). + +Measured overhead on this host (CPython 3.12, 200k instructions of the real +image, best of three): ~0.073 s with accounting disabled vs ~0.082 s enabled +(~12%); disabled runs are byte-for-byte the pre-increment behaviour. + +## 4. The cycle-0 GSP profile + +```bash +# bounded, deterministic, read-only; writes a session under dumps/ +python3 -m sim.gsp_profile --instructions 200000 + +# explicit session directory and machine-readable result +python3 -m sim.gsp_profile --record dumps/gsp-cycle0 --json + +# start from a harvested first-touch map and keep what the run learned +python3 -m sim.gsp_profile --device-map dumps/<probe>/device-map.json --learn --learn-out /tmp/map.json + +# image absent (e.g. LFS not fetched): exits 2 with a skip reason, no session +python3 -m sim.gsp_profile --image /nonexistent.bin +``` + +The profile mirrors the established device-map/probe path: the ELF's PT_LOAD +plan is relocated by `bias=0x80000000` (`greeneye/elf.py`), RAM is sized to +cover the image as `greeneye/device_map.py` computes it, and all unmodelled +accesses are served leniently (zero or the mapped value) through a +`greeneye-device-map/1`. The instruction budget is exact, not wall-clock: +`Machine.run(budget)` is called directly, so the run is deterministic. + +Session metadata (`session.json`) and a `gsp_cycle_profile` event carry: +`firmware_sha256`, `bias`, `instructions_executed`, `cycles`, +`cycle_model`, `cycle_model_table`, `cycle_fidelity`, `stop_reason`/`stop_pc` +(both relocated and firmware-space), `mmio_unknown` and the first unknown +MMIO access. + +**Observed output** (this tree, 2026-10-03; sha256 +`2d1bb5c6…c3ceea`, budget 200 000): + +``` +instructions: 200000 +cycles (modeled): 200012 +stop: budget pc=0x84009070 (fw 0x4009070) +unknown MMIO accesses: 1 +first unknown MMIO: pc=0x84009014 addr=0x2000000000000a00 size=4 rw=r +``` + +This is a bounded lenient first-touch profile, **not a boot**: it reproduces +the documented QEMU probe first-touch address and continues under the modeled +lenient policy for the budget. The stop PC is a budget sample inside the +firmware's early path, not a milestone. + +## 5. What is not claimed + +* No silicon timing, latency, throughput, frequency or pipeline claim; the + table is a modeled default and says so in code, metadata and reports + (`sim/cycles.py::NOTE`, `CYCLE_FIDELITY`). +* No cache/memory-hierarchy model: `memory_latency`/`mmio_latency` are fixed + parameters, not simulated caches or DRAM. +* No device model: L3 remains absent (`06-ACCURACY.md`); the profile runs + under lenient MMIO, so its control flow after the first unknown access is + conditioned on that policy, not on device behaviour. +* No accuracy metric may be computed from this increment alone. Calibration + (`RISC-V-GSP-RM-DEBUGGER-ROADMAP.md`, `16-CUSTOM-SIMULATOR.md`) still + requires sourced latency tables and, for any product-facing claim, + supervised physical confirmation. + +## 6. Tests + +`tests/test_sim_cycles.py` (21 tests, all offline): + +* table sanity (every decoded kind has a positive cost; ALU/branch/jump unit + cost; M-extension ordering; memory latency is a parameter; labeling); +* counter arithmetic on hand-assembled programs (RAM vs MMIO latency); +* disabling the model leaves `cycles` at 0 and changes no execution state; +* determinism double-runs at machine and profile level; +* the bundled demo executes identically with and without the model; +* real-image bounded profile: first touch matches the documented probe + address, session metadata carries the cycle totals, two runs are identical; +* absent image skips cleanly. + +## 7. Next steps + +1. Device model (WP1.5.2): falcon/SEC2 windows, CSB/TRANSCOM, WPR2/FWSEC, + PTIMER, IRQ routing — the run then stops modelling MMIO leniently. +2. Boot shims with recorded assumptions (WP1.5.3) so the profile can report a + real milestone instead of a budget sample. +3. Sourced instruction/access latency tables and a documented calibration + metric (`06-ACCURACY.md`, `16-CUSTOM-SIMULATOR.md`) before any + timing-shaped statement; per-MCU cycle domains come with + WP1.5.4/WP1.5.5. +4. Additive `cycle` event class in the session schema (roadmap V2, "formats + and versioning") once the event vocabulary is reviewed. + +The increment is deliberately a table and a counter: every later step can +replace defaults without touching the execution loop (the model is passed in) +or the session format (metadata is additive). diff --git a/debug/risc-v-gsp-rm-vm/docs/33-AMPERE-PARAMETERS.md b/debug/risc-v-gsp-rm-vm/docs/33-AMPERE-PARAMETERS.md new file mode 100644 index 0000000..929db3d --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/33-AMPERE-PARAMETERS.md @@ -0,0 +1,288 @@ +# 33 — Ampere parameter dossier (Phase 2 model inputs) + +> **Status:** research dossier for the GreenEye-GPGPU-Sim Phase 2 model +> (WP2.1, `handoff-simulator.md` §5). Public-source only. Rows marked +> `[scaled]` are derived by an explicit arithmetic rule; rows marked +> `[inferred]` are reasoning, not measurement. Nothing here is a +> silicon-accuracy claim. Phase 2 is gated (U24): this document is written +> now, implemented later. +> +> **Scope:** A100 / GA100 (the microbenchmark paper's target), GA102 +> (RTX 3090), and GA106 (RTX 3060, the SM86 consumer case named in the +> handoff). Host/device modeling only; not GSP/MCU. + +## 1. Sources and citation labels + +| Label | Source | What it provides | +|---|---|---| +| **[AMP]** | H. Abdelkhalik, Y. Arafa, N. Santhi, A.-H. Badawy, *Demystifying the Nvidia Ampere Architecture through Microbenchmarking and Instruction-level Analysis*, arXiv:2208.11174v1 (2022) — measurements on an NVIDIA "AI100" (A100) Tesla GPU | Per-instruction CPI (dependent + independent), memory-unit latencies, tensor-core (WMMA) latencies/throughput and PTX→SASS mapping, PTX/SASS tables. | +| **[A100-WP]** | NVIDIA, *NVIDIA A100 Tensor Core GPU Architecture* whitepaper, https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/nvidia-ampere-architecture-whitepaper.pdf | GA100/A100 architecture: SM count, L2, HBM2, MIG, cp.async, async barriers, sparsity, NVLink 3, per-precision peaks. | +| **[A100-DS]** | NVIDIA, *NVIDIA A100 Tensor Core GPU* datasheet (Jun 2021), https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/a100/pdf/nvidia-a100-datasheet-us-nvidia-1758950-r4-web.pdf | Form-factor-resolved A100 table: memory, bandwidth, TDP, MIG, NVLink/PCIe, precision peaks. | +| **[GA102-WP]** | NVIDIA, *NVIDIA Ampere GA102 GPU Architecture* whitepaper, v1/v2.x, https://www.nvidia.com/content/PDF/nvidia-ampere-ga-102-gpu-architecture-whitepaper-v2.1.pdf | GA102/GA10x SM (128 FP32/SM), RTX 3090 table, GDDR6X, 28.3 B transistors, 628.4 mm². | +| **[RTX30-W]** | Wikipedia *GeForce RTX 30 series* product tables (retrieved 2026-10-03), citing NVIDIA product pages, TechPowerUp GPU database and AnandTech reviews: https://en.wikipedia.org/wiki/GeForce_RTX_30_series | GA106/RTX 3060 board numbers, GA102 board clocks/bandwidth cross-checks. | +| **[T4]** | arXiv:1903.07486 (Turing) | Turing comparison points. | + +## 2. Method and caveats + +1. **[AMP] measured one A100 board** (SXM-class, 108 SMs, HBM2). Its + *instruction* and *memory-latency* numbers are architecture-level for + GA100; its board-level bandwidth/power numbers are not transferable to + GA10x. +2. GA100 (datacenter) and GA10x (consumer) **SM designs differ**: GA100 has + 64 FP32 + 64 INT32 per SM; GA10x has 128 FP32-capable cores per SM with + INT32 sharing. See §4. +3. `cp.async` and asynchronous barriers are **compute-capability 8.0 (GA100) + features**; SM86 (GA10x consumer) does not expose them ([A100-WP] §CUDA + Asynchronous Copy; CUDA programming guide compute-capability notes). + Do not model cp.async on the RTX 3060 profile. +4. Dependent-issue latencies dominate a statically scheduled core; [AMP] + distinguishes dependent vs independent CPI (§6.2). Use both. +5. Board clocks, memory rates and TDP are board properties (§3); cache + geometry and cycle latencies are architecture properties. + +## 3. Chip and board parameters + +### 3.1 GA100 / A100 + +| Parameter | Value | Source | Notes | +|---|---|---|---| +| Full GA100 | 128 SMs, 8 GPCs, 8 TPCs/GPC, 6 HBM2 stacks, 12 512-bit MCs | [A100-WP] §GA100 | | +| A100 (product) | 108 SMs, 7 GPCs, 5 HBM2 stacks, 10 512-bit MCs | [A100-WP] §GA100 | 432 3rd-gen tensor cores. | +| Boost clock | 1,410 MHz | [A100-WP] A100 vs V100 table | | +| L1/shared per SM | 192 KB (vs V100 128 KB) | [A100-WP] §overview | 1.5× aggregate capacity per SM. | +| L2 | 40 MB, partitioned crossbar, 2.3× V100 L2 read bandwidth | [A100-WP] §40 GB HBM2 and 40 MB L2 | ~7× V100 capacity. | +| HBM2 (40 GB SXM/PCIe) | 1,555 GB/s | [A100-DS] | 1,215 MHz DDR, 5 stacks × 8 dies [A100-WP]. | +| HBM2e (80 GB SXM / PCIe) | 2,039 / 1,935 GB/s | [A100-DS] | | +| TDP | 400 W SXM4; 250 W PCIe (300 W 80 GB PCIe) | [A100-DS] | | +| PF32 | 19.5 TFLOPS | [A100-DS] | | +| FP64 / FP64 TC | 9.7 / 19.5 TFLOPS | [A100-DS] | | +| FP16 / BF16 (non-tensor) | 78 / 39 TFLOPS | [A100-WP] Table 1 | | +| TF32 TC | 156 TFLOPS (312 sparse) | [A100-DS] | | +| FP16 TC / BF16 TC | 312 TFLOPS (624 sparse) | [A100-DS] | | +| INT8 TC / INT4 TC | 624 TOPS (1,248 sparse) / 1,248 TOPS (2,496 sparse) | [A100-DS] | | +| NVLink 3 | 12 links, 25 GB/s per direction per link, 600 GB/s aggregate | [A100-WP] §Third-Generation NVLink, [A100-DS] | 50 Gbit/s per signal pair. | +| PCIe | Gen4 ×16, ~31.5 GB/s per direction (64 GB/s bidirectional per datasheet) | [A100-WP], [A100-DS] | SR-IOV supported. | +| MIG | Up to 7 instances; isolated crossbar ports, L2 banks, memory controllers, DRAM buses | [A100-WP] §MIG, [A100-DS] | 40 GB: 7 × 5 GB profiles; 80 GB: 7 × 10 GB. | + +### 3.2 GA102 / RTX 3090 + +| Parameter | Value | Source | Notes | +|---|---|---|---| +| Full GA102 | 7 GPCs, 42 TPCs, 84 SMs, 10752 CUDA cores, 336 tensor cores, 84 RT cores, 28.3 B transistors, 628.4 mm² | [GA102-WP] §GA102 Key Features | | +| RTX 3090 | 82 SMs, 10496 CUDA cores, 328 tensor cores (4/SM, 3rd gen), 82 RT cores, 24 GB GDDR6X, 384-bit | [GA102-WP] Table 7, [RTX30-W] | | +| Boost clock (3090 FE) | 1,695 MHz | [GA102-WP] Table 7 | Base 1,395 MHz [RTX30-W]. | +| FP32 (non-tensor) | 35.6 TFLOPS | [GA102-WP] Table 7 | 128 FP32/SM. | +| FP16/BF16 (non-tensor) | 35.6 / 35.6 TFLOPS | [GA102-WP] | | +| INT32 | 17.8 TOPS | [GA102-WP] | IMAD-based. | +| FP16 TC (FP16 acc) | 142 (284 sparse) TFLOPS | [GA102-WP] Table 7 | | +| FP16 TC (FP32 acc) | 71 (142 sparse) TFLOPS | [GA102-WP] | | +| TF32 TC | 35.6 (71 sparse) TFLOPS | [GA102-WP] | | +| INT8 / INT4 TC | 284 (568 sparse) / 568 (1136 sparse) | [GA102-WP] | | +| L1/shared per SM | 128 KB (10496 KB per 82-SM GPU) | [GA102-WP] Table 7 | | +| L2 | 6 MB | [GA102-WP] Table 7 | | +| Memory | GDDR6X 19.5 Gbps, 936 GB/s | [GA102-WP] Table 7 | 1.5× Titan RTX. | +| TGP | 350 W | [GA102-WP] | | +| NVLink 3 | 4 × x4 links, ~112.5 GB/s bidirectional | [GA102-WP] §NVLink | Consumer 3090-class only. | + +### 3.3 GA106 / RTX 3060 (SM86) + +| Parameter | Value | Source | Notes | +|---|---|---|---| +| CUDA cores / SMs / tensor cores | 3,584 / 28 / 112 (4/SM) | [RTX30-W] GA106-300 row | SM count 28. | +| Clocks (base/boost) | 1,320 / 1,777 MHz | [RTX30-W] | | +| FP32 (boost) | 12.74 TFLOPS | [RTX30-W] (9.462 × 1.777 GHz computed by the table) | `[scaled]` consistent with 28 SM × 128 × 2 × 1.777 GHz. | +| Memory | 12 GB GDDR6, 192-bit, 15 Gbps, 360 GB/s | [RTX30-W] | | +| L2 | 3 MB | [RTX30-W] (GA106 row) | | +| TGP | 170 W | [RTX30-W] | | +| Compute capability | 8.6 | NVIDIA CUDA programming guide (as cited by [RTX30-W]) | No cp.async (8.0-only). | + +## 4. Ampere SM architecture + +| Feature | GA100 (A100) | GA10x (GA102/GA106) | Source | +|---|---|---|---| +| FP32 cores / SM | 64 | 128 | [A100-WP] Fig. 7, [GA102-WP] §GA102 Key Features | +| INT32 cores / SM | 64 | (128 FP32 cores double as INT32 for many ops) | [A100-WP], [GA102-WP] | +| Tensor cores / SM | 4 (3rd gen) | 4 (3rd gen) | [A100-WP], [GA102-WP] Table 7 | +| Partitioning | 4 processing blocks | 4 processing blocks | [AMP] §V-C ("each SM … 4 tensor cores") | +| L1/shared per SM | 192 KB unified | 128 KB unified | [A100-WP], [GA102-WP] Table 7 | +| New in generation | `cp.async`, async barriers (`mbarrier`), L2 residency control, compute data compression, 2:4 structured sparsity, full-rate FP64 tensor cores | same tensor/L2 features; SM doubles FP32 per SM | [A100-WP], [GA102-WP] | +| L2 residency control | yes: persistent window (`accessPolicyWindow`), set-aside carveout | yes on GA10x | [A100-WP] §L2 residency | +| Compression | up to 4× DRAM/L2 bandwidth, up to 2× L2 capacity | not documented for GA10x | [A100-WP] §overview | + +**Data types on the 3rd-gen tensor core:** FP16, BF16, TF32 inputs +(FP16/FP32 accumulators), plus INT8/INT4/binary for inference; sparsity +(2:4) doubles effective throughput [A100-WP] §Third-Generation Tensor Core. + +## 5. Memory latencies (A100, cycles) + +From [AMP] Table IV (dependent-issue pointer chase; global-memory path +bypasses caches with `.cv`): + +| Memory type | CPI (cycles) | Source | +|---|---|---| +| Global memory | 290 | [AMP] Table IV, §V-B | +| L2 cache | 200 | [AMP] | +| L1 cache | 33 | [AMP] | +| Shared memory load / store | 23 / 19 | [AMP] | + +Comparison points quoted by [AMP] for Turing: global 434, L2 188, L1 32 +(the same values as [T4]'s measured 32/188/296/616 global-path figures, +with 434 corresponding to the DRAM-latency band). Use [T4] Table 3.1 for +the Turing shared-memory row (19 cycles no-conflict); [AMP]'s 23/19 is the +Ampere measurement. + +## 6. Instruction performance (A100, cycles) + +### 6.1 Full PTX→SASS latency table [AMP] Table V + +`cycles` = dependent-issue CPI measured by [AMP] on A100. + +| PTX | SASS | cycles | PTX | SASS | cycles | +|---|---|---|---|---|---| +| add.u16 | UIADD3 | 2 | mul.wide.u16 | LOP3.LUT+IMAD | 4 | +| addc.u32 | IADD3.X | 2 | mul.wide.u32 | IMAD | 4 | +| add.u32 | IADD | 2 | mul.lo.u16 | LOP3.LUT+IMAD | 4 | +| add.u64 | UIADD3.x+UIADD3 | 4 | mul.lo.u32 | IMAD | 2 | +| add.s64 | UIADD3.x+UIADD3 | 4 | mul.lo.u64 | IMAD | 2 | +| add.f16 | HADD | 2 | mul24.lo.u32 | PRMT+IMAD | 3 | +| add.f32 | FADD | 2 | mul24.hi.u32 | UPRMT+USHF.R.U32.HI+IMAD.U32+PRMT | 9 | +| add.f64 | DADD | 4 | mul.rn.f16 | HMUL2 | 2 | +| mad.lo.u16 | LOP3.LUT+IMAD | 4 | mul.rn.f32 | FMUL | 2 | +| mad.lo.u32 | FFMA | 2 | mul.rn.f64 | DMUL | 4 | +| mad.lo.u64 | IMAD | 2 | fma.rn.f16 | HFMA2 | 2 | +| mad24.lo.u32 | SGXT.U32+IMAD | 4 | fma.rn.f32 | FFMA | 2 | +| mad24.hi.u32 | USHF.R.U32.HI+UIMAD.WIDE.U32+2*UPRMT+IADD3 | 11 | fma.rn.f64 | DFMA | 4 | +| mad.rn.f32 | FFMA | 2 | abs.s16 | PRMT+IABS+PRMT | 4 | +| mad.rn.f64 | DFMA | 4 | abs.s32 | IABS | 2 | +| sad.u16/s16 | (2*LOP3)+ULOP3+VABSDIFF | 6 | abs.s64 | UISETP.LT.AND+UIADD3.X+UIADD3+2*USEL | 11 | +| sad.u32/s32 | VABSDIFF+IMAD | 3 | abs.f16 | PRMT | 1 | +| sad.u64/s64 | UISETP.GE.U32.AND+UIADD+IADD | 10 | abs.ftz.f32 | FADD.FTZ | 2 | +| rem/div.u16/s16 | multiple instrs | 290 | abs.f64 | DADD or (DADD+UMOV) | 4 | +| rem/div.s32/u32 | multiple instrs | 66 | neg.s16 | UIADD3+UPRMT | 5 | +| rem/div.u64/s64 | multiple instrs | 420 | neg.s32 | IADD3 | 2 | +| div.rn.f32 | multiple instrs | 525 | neg.s64 | IMAD.MOV.U32+HFMA2.MMA+MOV+UIADD3 | 10 | +| div.rn.f64 | multiple instrs | 426 | neg.f32 | FADD or IMAD.MOV.U32 | 2 | +| sqrt.rn.f32 | incl. MUFU.RSQ | 190–235 | neg.f64 | DADD+(UMOV) | 4 | +| sqrt.approx.f32 | incl. MUFU.SQRT | 2–18 | rcp.rn.f32 | incl. MUFU.RCP | 198 | +| sqrt.rn.f64 | incl. MUFU.RSQ64 | 260–340 | rcp.approx.f32 | incl. MUFU.RCP | 23 | +| rsqrt.approx.f32 | incl. MUFU.RSQ | 2–18 | rcp.rn.f64 | incl. MUFU.RCP64H | 244 | +| rsqrt.approx.f64 | MUFU.RSQ64H | 8–11 | ex2.approx.f32 | FSTEP+FMUL+MUFU.EX2+FMUL | 14 | +| sin.approx.f32 | FMUL+MUFU.SIN | 8 | ex2.approx.f32 | FSETP.GEU.AND+2*FMUL+MUFU.EX2 | 18 | +| cos.approx.f32 | FMUL.RZ+MUFU.COS | 8 | ex2.approx.f16 | MUFU.EX2.F16 | 6 | +| lg2.approx.f32 | FSETP.GEU.AND+FMUL+MUFU.LG2+FADD | 18 | tanh.approx.f32 / .f16 | MUFU.TANH / MUFU.TANH.F16 | 6 | +| popc.b32 | POPC | 6 | and.b16 | LOP3.LUT or 1.5*LOP3.LUT | 2 | +| popc.b64 | 2*UPOPC+UIADD3 | 7 | and.b32 | LOP3.LUT | 2 | +| clz.b32 | FLO.U32+IADD | 7 | and.b64 | ULOP3.LUT | 2–3 | +| clz.b64 | UISETP.NE.U32.AND+USEL+UFLO.U32+2*UIADD3 | 13 | or / xor (b16/b32/b64) | LOP3 family | 2–4 | +| bfind.u32 | FLO.U32 | 6 | not.b16 / not.b32 | LOP3.LUT | 2 | +| bfind.u64 | FLO.U32+ISETP.NE.U32.AND+IADD3+BRA | 164 | not.b64 | 2*ULOP3.LUT | 4 | +| bfind.s32 | FLO | 6 | lop3.b32 | IMAD.MOV.U32+LOP3.LUT | 4 | +| bfind.s64 | multiple instrs | 195 | cnot.b16/b32/b64 | ULOP3…/UISETP…/multiple | 5/4/11 | +| testp.normal.f32 | IMAD.MOV.U32+2*ISETP.GE.U32.AND | 0 or 6 | bfe.s32/.u32 | 3*PRMT+2*IMAD.MOV+SHF.R.U32.HI+SGXT | 11 | +| testp.subnor.f32 | ISETP.LT.U32.AND | 0 or 6 | bfe.u64 | UMOV+USHF.L.U32+(UIADD3+ULOP3.LUT) | 5 | +| testp.normal.f64 | 2*UISETP.LE.U32.AND+2*UISETP.GE.U32.AND | 13 | bfe.s64 | multiple instrs | 14 | +| testp.subnor.f64 | UISETP.LT.U32.AND+2*UISETP.GE.U32.AND.EX | 8 | bfi.b32 | 3*PRMT+2*IMAD.MOV+SHF.L.U32+BMSK+LOP3.LUT | 11 | +| min.u16 | ULOP3.LUT+UISETP.LT.U32.AND+USEL | 8 | bfi.b64 | UMOV+USHF.L.U32+(UIADD3+ULOP3.LUT) | 5 | +| min.u32 | IMNMX.U32 | 2 | brev.b32 | BREV+SGXT.U32 | 2 | +| min.u64 | UISETP.LT.U32.AND+2*USEL | 8 | brev.b64 | 2*UBREV+MOV | 6 | +| min.s16 | PRMT+IMNMX | 4 | copysign.f32 | 2*LOP3.LUT or 1.5*LOP3.LUT | 4 | +| min.s32 | IMNMX | 2 | copysign.f64 | 2*ULOP3.LUT+IMAD.U32+*MOV | 6 | +| min.s64 | UISETP.LT.U32.AND+UISETP.LT.AND.EX+2*USEL | 8 | cvt.rzi.s32.f32 | F2I.TRUNC.NTZ | 6 | +| min.f16 | HMNMX2+PRMT | 4 | setp.ne.s32 | ISETP.NE.AND | 10 | +| min.f32 | FMNMX | 2 | mov.u32 %clock | CS2R.32 | 2 | +| min.f64 | DSETP.MIN.AND+IMAD.MOV.U32+UMOV+FSEL | 10 | dp4a.u32.u32 | IMAD.MOV.U32+IDP.4A.U8.U8 | 135–170 | +| bar.warp.sync | NOP | changes | dp2a.lo.u32.u32 | IMAD.MOV.U32+IDP.2A.LO.U16.U8 | 135–170 | +| fns.b32 | multiple instrs | 79 | | | | + +### 6.2 Dependence and issue effects + +[AMP] Table II (dependent vs independent CPI): + +| Instruction | dependent | independent | +|---|---|---| +| add.f16 | 3 | 2 | +| add.u32 | 4 | 2 | +| add.f64 | 5 | 4 | +| mul.lo.u32 | 3 | 2 | +| mad.rn.f32 | 4 | 2 | + +[AMP] §V-A notes: 2 `add` + 2 `mad` complete in ~4 cycles total, i.e. ~1 +cycle each, because MAD issues on the FP pipeline and ADD on the integer +pipeline (they overlap). `mad` always runs on the FP pipeline even for +integers (mapped to FFMA). Signed/unsigned pairs map identically except +`bfind`, `min`, `max`. Input initialization can change PTX→SASS mapping +(e.g. `neg.f32` → `FADD` vs `IMAD.MOV.u32`). + +## 7. Tensor core (WMMA) latencies and throughput, A100 [AMP] Table III + +| PTX shapes | Input | Acc | Cycles | Measured theoretical | PTX instruction | SASS | +|---|---|---|---|---|---|---| +| m16n16k16 / m8n32k16 / m32n8k16 | f16 | f16 | 16 | 311–312 GB/s | `wmma.mma.sync...m16n16k16.f16.f16` | 2× HMMA.16816.F16, 8 cyc each | +| same | f16 | f32 | 16 | 310–312 GB/s | `...f16.f32` | 2× HMMA.16816.F32, 8 cyc each | +| same | bf16 | f32 | 16 | 310–312 GB/s | `...f32.bf16.bf16.f32` | 2× HMMA.16816.F32.BF16, 8 cyc each | +| m16n16k8 | tf32 | f32 | 16 | 132–156 GB/s | `...f32.tf32.tf32.f32` | 4× HMMA.1684.F32.TF32, 4 cyc each | +| m8n8k4 | f64 | f64 | 16 | 19–19.5 GB/s | `...m8n8k4.f64...rn` | 1× DMMA.884, 16 cyc | +| m16n16k16 / m32n8k16 / m8n32k16 | u8 | u32 | 8 | 594–624 GB/s | `...s32.u8.u8.s32` | 2× IMMA.16816.U8.U8, 4 cyc each | +| m8n8k32 | u4 | u32 | 4 | 1229–1248 GB/s | `...s32.u4.u4.s32` (row.col) | 1× IMMA.8832.U4.U4, 4 cyc | + +Additional findings from [AMP] §V-C: + +* Each Ampere SM holds 4 tensor cores; the physical TC implementation can + perform 8×4×8 [AMP, citing Sun et al. arXiv:2206.02874]. +* PTX and SASS shapes differ; the SASS instruction count depends on how + many SASS-shaped ops tile the PTX shape (hence 2 vs 4 SASS per WMMA). +* For the same data type, **different PTX shapes do not change** the + measured latency on Ampere (unlike Turing, where shape matters [AMP, + citing Yan et al.]). +* FP16/BF16 WMMA requires a `MOVM.16.MT88` matrix-load/transpose + instruction; its count depends on row/column layout. +* Volta had only `HMMA.884`; Turing added `HMMA.1688`; Ampere adds + `DMMA.884` (FP64), `IMMA.16816` (U8), `IMMA.8832` (U4) [AMP] §V-C. + +## 8. Generation changes the model must express (not just retune) + +| Change | Turing (Phase 1) | Ampere (Phase 2) | Source | +|---|---|---|---| +| FP32/SM | 64 | 64 (GA100) / 128 (GA10x) | [A100-WP], [GA102-WP] | +| Concurrent FP32+INT32 | separate 16+16 per block | GA10x doubles FP32 and shares; GA100 keeps separate INT32 | [TWP], [GA102-WP] | +| Global memory latency | 434 cycles | 290 cycles | [AMP] | +| L2 latency | 188 | 200 | [T4], [AMP] | +| L1 latency | 32 | 33 | [T4], [AMP] | +| L2 size | 4 MB (TU104) | 40 MB (A100) / 6 MB (GA102) / 3 MB (GA106) | [TWP], [A100-WP], [GA102-WP], [RTX30-W] | +| cp.async | absent | present on sm_80 only | [A100-WP] | +| Async barrier | absent | present | [A100-WP] | +| Sparsity | absent | 2:4 structured, 2× tensor rate | [A100-WP] | +| Tensor dtypes | FP16, INT8/4/1 | + BF16, TF32, FP64; INT4; binary | [TWP], [A100-WP] | +| L2 residency / compression | no / no | yes / yes (A100) | [A100-WP] | +| MIG | no | A100 up to 7 instances | [A100-WP] | + +## 9. Open / uncertain items (do not invent) + +1. GA100 **L1 sectoring/line behaviour** and L2 request coalescing are not + published; [AMP] gives only aggregate latencies. +2. cp.async **issue/completion timing** and mbarrier latency are not + published. +3. GA10x (SM86/SM89) L1/L2 microarchitecture differs from GA100; [AMP] + does not measure GA10x. Treat GA10x memory latencies as Turing-band + until a GA10x source exists; mark the profile's uncertainty. +4. MIG slice-level bandwidth/latency QoS curves are not published. +5. GDDR6X timing set (PAM4) and the 936 GB/s achievable ratio are not + public; model as JEDEC-class + calibrated efficiency. +6. A100 80 GB HBM2e (2,039 GB/s) changes memory timing but not + instruction/cache cycles; keep memory parameters separate. +7. Clock-throttling envelopes for GA102/GA106 boards are not modelled from + published curves. +8. The A100 microbenchmark table does not include every PTX op (e.g. + `cp.async` variants, `mbarrier` ops); these need direct measurement or + Triton/PTX docs before any cycle claim. + +## 10. Validation hooks + +* C1: §6.1 table (≥85% of entries within ±1 cycle). +* C2: §5 latencies (≥80% within ±15%). +* C3: A100 HBM2 1,555 GB/s theoretical vs achievable; GDDR6X 936 GB/s + theoretical vs achievable. +* C4: kernel correlation using triangular GEMM sizes from [AMP] (WMMA + throughputs) plus a reduction/streaming suite. +* Phase 2 entry additionally requires rebasing or superseding the + upstream `origin/dev` 4.2.0 RTX 3060 SM86 config (WP2.1/WP2.2). diff --git a/debug/risc-v-gsp-rm-vm/docs/34-BLACKWELL-SOURCES.md b/debug/risc-v-gsp-rm-vm/docs/34-BLACKWELL-SOURCES.md new file mode 100644 index 0000000..f0e28df --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/34-BLACKWELL-SOURCES.md @@ -0,0 +1,190 @@ +# 34 — Blackwell sources and known-public parameters (planned / uncertain) + +> **Status: planned, uncertain, do not implement.** Phase 3 (U10/U17) is +> gated behind the Phase 2 exit and a legal/source review +> (`handoff-simulator.md` §5, Phase 3). This document is a bibliography and +> a confidence-labelled parameter register only. It is **not** a model spec +> and must not be used to seed runnable configuration until Phase 3 entry. +> +> **Rules applied here:** +> * every number has a source and a confidence level; +> * values that are not public are written as **open questions** — never +> invented, interpolated or copied from a sibling architecture; +> * first-party NVIDIA publications outrank aggregators; where only an +> aggregator exists the row is labelled accordingly; +> +> **Confidence legend:** **A** = first-party NVIDIA publication (whitepaper, +> newsroom, datasheet, product page); **B** = first-party claim corroborated +> by at least one independent secondary source; **C** = secondary source only +> (aggregator/press, single-source or unverifiable against NVIDIA); +> **U** = unknown / not found — an explicit open question. + +## 1. Bibliography + +| # | Source | Type / URL | +|---|---|---| +| [RBW] | NVIDIA, *NVIDIA RTX Blackwell GPU Architecture* whitepaper (Jan 2025, v1.x) | First-party whitepaper, consumer GB20x. https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf | +| [B200-N] | NVIDIA Newsroom, *NVIDIA Blackwell Platform Arrives to Power a New Era of Computing* (GTC, Mar 2024) | First-party press release, B100/B200/GB200. https://nvidianews.nvidia.com/news/nvidia-blackwell-platform-arrives-to-power-a-new-era-of-computing | +| [GB200] | NVIDIA, *GB200 NVL72* product page | First-party product/spec page. https://www.nvidia.com/en-us/data-center/gb200-nvl72/ | +| [GB300] | NVIDIA, *GB300 NVL72* product page | First-party product/spec page. https://www.nvidia.com/en-us/data-center/gb300-nvl72/ | +| [SPARK] | NVIDIA, *NVIDIA DGX Spark* product page (Specifications section) | First-party product page, GB10. https://www.nvidia.com/en-us/products/workstations/dgx-spark/ | +| [SPARK-DS] | NVIDIA, *DGX Spark* datasheet (GTC 2025 spring) | First-party datasheet, GB10. https://nvdam.widen.net/s/tlzm8smqjx/workstation-datasheet-dgx-spark-gtc25-spring-nvidia-us-3716899-web | +| [CUDA-CC] | NVIDIA, *CUDA C Programming Guide* — compute capability 10.x / 12.x | First-party ISA/feature statement. https://docs.nvidia.com/cuda/cuda-c-programming-guide/ | +| [OCP-MX] | Open Compute Project, *OCP Microscaling Formats (MX) Specification v1.0* (2024) | Industry spec defining MXFP4/MXFP6. https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf | +| [MX-PAPER] | Rouhani et al., *Microscaling Data Formats for Deep Learning*, arXiv:2310.10537 (2023) | Published format analysis. | +| [WIKI-BW] | Wikipedia, *Blackwell (microarchitecture)* (retrieved 2026-10-03), citing TechPowerUp GPU database and AnandTech | Secondary aggregation; provenance for per-die core counts. https://en.wikipedia.org/wiki/Blackwell_(microarchitecture) | + +## 2. Datacenter Blackwell (GB100 / B100 / B200 / GB200) + +| Parameter | Value | Confidence | Source | Notes | +|---|---|---|---|---| +| Die | GB100, TSMC 4NP, reticle-limit size | A | [B200-N], [WIKI-BW] | | +| Transistors per die | 104 B | C | [WIKI-BW] (citing AnandTech) | NVIDIA does not headline a per-die count. | +| Package (B100/B200) | two GB100 dies, 208 B transistors total | A | [B200-N] | | +| Die-to-die link | 10 TB/s (NV-HBI) | A | [B200-N] | B200 acts as one unified GPU with cache coherency. | +| NVLink 5 per GPU | 1.8 TB/s bidirectional | A | [B200-N] | NVL72 supports up to 576 GPUs. | +| NVLink-C2C (GB200 → Grace CPU) | 900 GB/s | A | [B200-N] | "ultra-low-power NVLink chip-to-chip interconnect". | +| NVLink switch total (NVL72) | 130 TB/s | A | [GB300] (identical fabric class) | Cited on the GB300 page; GB200 NVL72 is the same 72-GPU fabric. | +| CUDA cores per die | 18,432 | C | [WIKI-BW] (TechPowerUp) | Not published by NVIDIA. | +| Tensor cores per die | 576 | C | [WIKI-BW] (TechPowerUp) | 4/die? No — per-die count not first-party; treat as unconfirmed. | +| L2 per die | 60 MB | C | [WIKI-BW] (TechPowerUp) | | +| Memory interface | 8,192-bit HBM3e per package | C | [WIKI-BW] | | +| B200 memory | 192 GB HBM3e, ~8 TB/s | B/C | widely reported; **not captured from a first-party page in this survey** | open question §5.1 | +| B100 memory | 192 GB HBM3e (variant differences open) | C | [WIKI-BW] | B100/B200 memory split varies by SKU; verify. | +| Compute capability | 10.x | A | [CUDA-CC] | | +| Low-precision formats | MXFP6, MXFP4 (OCP microscaling), FP8, INT8/INT4, binary | A | [B200-N] (Transformer Engine), [OCP-MX], [MX-PAPER] | 5th-gen tensor core with micro-tensor scaling. | +| FP4 performance | 20 PFLOPS (GB200 dual-GPU, excluding 2× sparsity claim) | A | [GB200] (as cited by [WIKI-BW]) | Per-GPU and per-die numbers need the technical brief; see §5.1. | +| Products | B100/B200 SXM, GB200 Grace-Blackwell superchip | A | [B200-N] | | + +## 3. Consumer Blackwell (GB202 / GB203 / GB205, RTX 50 series) + +All rows in this section are **[A]** from the RTX Blackwell whitepaper +unless noted; this is the **best-documented Blackwell part** and the natural +first Phase-3 target on the consumer side. + +| Parameter | GB202 full | RTX 5090 | Source | +|---|---|---|---| +| GPCs / TPCs / SMs | 12 / 96 / 192 | 11 / 85 / 170 | [RBW] §GB202, Table 3 | +| CUDA cores / SM | 128 | 128 | [RBW] | +| CUDA cores / GPU | 24,576 | 21,760 | [RBW] | +| Tensor cores / SM / GPU | 4 (5th gen) / 768 | 4 (5th gen) / 680 | [RBW] Table 3 | +| RT cores | 192 (4th gen) | 170 (4th gen) | [RBW] | +| Register file / SM | 256 KB | 256 KB | [RBW] §SM | +| L1/shared per SM | 128 KB | 128 KB | [RBW] §SM | +| L2 | 128 MB | 96 MB | [RBW] §GB202 | +| Memory | 512-bit GDDR7 (16 × 32-bit MCs) | 32 GB GDDR7, 28 Gbps, 1,792 GB/s | [RBW] Table 3 | +| Boost clock | — | 2,407 MHz | [RBW] Table 3 | +| FP32 (non-tensor) | — | 104.8 TFLOPS | [RBW] Table 3 | +| FP16/BF16/INT32 (non-tensor) | — | 104.8 / 104.8 / 104.8 TFLOPS | [RBW] Table 3 (INT32 fully unified per SM) | +| FP4 tensor | — | 1,676 / 3,352 TFLOPS (non-sparse / sparse) | [RBW] Table 3 | +| FP8 tensor (FP16 acc) | — | 838 / 1,676 TFLOPS | [RBW] | +| FP16 tensor (FP16 acc) | — | 419 / 838 TFLOPS | [RBW] | +| FP16 tensor (FP32 acc) / BF16 | — | 209.5 / 419 TFLOPS | [RBW] | +| TF32 tensor | — | 104.8 / 209.5 TFLOPS | [RBW] | +| INT8 tensor | — | 838 / 1,676 TOPS | [RBW] | +| FP64 | — | 1/64 of FP32 rate, 384 FP64 cores (2/SM) on the full die | [RBW] §GB202 note | `[derived]` ≈ 1.64 TFLOPS for the full chip. | +| Transistors / die size | 92.2 B / 750 mm² | 92.2 B / 750 mm² | [RBW] Table 3 | +| TGP | — | 575 W | [RBW] Table 3 | +| SM changes | INT32 fully unified with FP32 (2× many integer ops; unified cores are either FP32 or INT32 per cycle); one 4th-gen RT core + four 5th-gen tensor cores per SM | | [RBW] §SM | + +## 4. GB10 (Grace Blackwell superchip; DGX Spark class) and GB300 + +### 4.1 GB10 + +| Parameter | Value | Confidence | Source | +|---|---|---|---| +| CPU | 20-core Arm: 10 × Cortex-X925 + 10 × Cortex-A725 | A | [SPARK] | +| GPU | Blackwell architecture, 5th-gen tensor cores, 4th-gen RT cores | A | [SPARK] | +| Tensor performance | up to 1 PFLOP FP4 (theoretical, sparsity) | A | [SPARK], [SPARK-DS] | +| System memory | 64 GB (OEM) or 128 GB LPDDR5X, coherent unified | A | [SPARK], [SPARK-DS] | +| Memory interface / bandwidth | 256-bit / 273 GB/s | A | [SPARK], [SPARK-DS] | +| NVLink-C2C | "5× the bandwidth of PCIe Gen 5" (exact GB/s not stated) | A (ratio only) | [SPARK-DS] | +| Networking | ConnectX-7 200 Gb/s; up to 4 systems connected | A | [SPARK] | +| TDP | 140 W (GB10 chip, incl. CPU+GPU) | A | [SPARK] | +| SMs / CUDA cores | 48 SMs / 6,144 CUDA cores | C | [WIKI-BW] (TechPowerUp) | +| Tensor / RT cores | 384 / 48 | C | [WIKI-BW] | +| L1 / L2 | 6 MB / 50 MB | C | [WIKI-BW] | +| Compute capability | 12.x (consumer Blackwell family; confirm GB10 SKU) | U | — | open; see §5.2 | + +### 4.2 GB300 / Blackwell Ultra + +| Parameter | Value | Confidence | Source | +|---|---|---|---| +| Product | GB300 NVL72: 72 Blackwell Ultra GPUs + 36 Grace CPUs | A | [GB300] | +| FP4 dense uplift | 1.5× dense FP4 FLOPS vs Blackwell (GB200-class) | A | [GB300] | +| Attention performance | 2× higher attention vs Blackwell | A | [GB300] | +| NVLink fabric | 130 TB/s per rack | A | [GB300] | +| GPU memory (rack) | 20 TB HBM3e class ("Fast Memory 37 TB" total incl. CPU) | A | [GB300] table | +| HBM bandwidth (rack) | up to 576 TB/s aggregate | A | [GB300] table | +| CPU memory | 17 TB LPDDR5X, 14 TB/s | A | [GB300] | +| CPU cores | 2,592 Arm Neoverse V2 cores | A | [GB300] | +| Rack compute | FP4 1,440 / 1,080 PFLOPS (sparse / without sparsity); FP8-FP6 720 PFLOPS; INT8 24 POPS; FP16/BF16 360 PFLOPS; TF32 180 PFLOPS; FP32 6 PFLOPS; FP64 100 TFLOPS | A | [GB300] table | +| Per-GPU derived | 288 GB HBM3e, ~8 TB/s HBM, 20 PFLOPS FP4 sparse / 15 PFLOPS dense | B | `[derived]` from [GB300]: 20 TB ÷ 72 ≈ 278–288 GB (nominal 288 GB), 576 TB/s ÷ 72 = 8 TB/s, 1,440 ÷ 72 = 20 PFLOPS | mark derived; NVIDIA has separately quoted 288 GB per Blackwell Ultra GPU. | +| GPU clocks, SM counts, cache sizes | — | U | — | open; see §5.3 | + +## 5. Open questions (explicit; never fill by invention) + +### 5.1 GB100 / B200 +1. Per-die SM count, CUDA/tensor core counts and per-SM composition are not + in a first-party public source found in this survey ([WIKI-BW] only). +2. HBM3e capacity/bandwidth per SKU (B100 vs B200, 192 GB vs other) needs + the NVIDIA Blackwell technical brief or product brief. +3. L2 organization/coherence across the two dies beyond "cache coherent" + ([B200-N]) is unpublished. +4. NV-HBI latency/bandwidth characteristics beyond the 10 TB/s peak. +5. No per-instruction latency table exists for Blackwell comparable to + [AMP]/[T4]. A microbenchmark campaign (own or literature) is required + before any C1 claim. + +### 5.2 GB10 +6. Exact NVLink-C2C bandwidth in GB/s (only the "5× PCIe Gen 5" ratio is + first-party). +7. Whether GB10's compute capability is 12.0 or 12.x for the relevant SKU. +8. SM/core/cache counts are secondary-source only. +9. LPDDR5X timing and real achievable bandwidth (273 GB/s is the + first-party theoretical interface rate). + +### 5.3 GB300 / Blackwell Ultra +10. Per-GPU clocks, SM counts, cache sizes and tensor shape/issue details. +11. Whether per-GPU HBM is exactly 288 GB on every SKU (the rack arithmetic + is approximate). +12. NVLink-C2C generation/bandwidth in GB300 (assume same class as GB200 + only as a hypothesis, not a model input). +13. FP4/MXFP4 numerical semantics for simulation (block scaling, exponent + sharing) beyond the OCP spec. + +### 5.4 Cross-cutting +14. A public **Hot Chips** presentation with GB100/GB202 microarchitecture + details was **not found** in this survey pass. If one exists (or is + released later), add it here with the exact deck title/URL — this is the + single highest-value missing source. +15. Blackwell memory-latency, sectoring, cp.async-successor semantics and + SM issue details are unpublished; Phase 3 must either measure locally + (supervised, hardware-permitting) or ship explicit uncertainty bands. +16. Blackwell datacenter firmware/boot material is out of scope here and + remains gated by the RM 610 no-fly zone (docs/23) — no 610 artifact may + be fetched or analysed for Phase 3 without the approval token. + +## 6. What a Phase-3 smoke parameter set may and may not contain + +* **May** use §3 rows (all first-party) as the consumer starting point, with + per-row uncertainty labels stored in the simulator session metadata. +* **May** use §2/§4 first-party *aggregate* claims as sanity bands. +* **May not** use any [C] row as a calibration target; [C] rows are leads to + verify, not values to fit. +* **May not** interpolate GB100 from GB202 or from Hopper; die topology, + clock domains and cache hierarchy are different designs. +* Any physical Blackwell capture is supervised-only and never an agent task + (RUNBOOK P4/P7); any firmware/BSP input obeys docs/23. +* Exit evidence for Phase 3 is defined in WP3.1–WP3.3: source dossier (this + document), an explicit uncertainty report, and a device profile — not an + accuracy claim. + +## 7. Bibliography maintenance + +Add to §1 whenever any of these appear: NVIDIA Blackwell technical brief +(datacenter), NVIDIA GB10/C2C technical brief, Hot Chips/ISSCC decks, +peer-reviewed Blackwell microbenchmarking papers, official CUDA PTX ISA +additions for `sm_100`/`sm_120` (e.g. tcgen05/5th-gen tensor instructions), +and OCP MX follow-ups. Keep [C] rows attached to their aggregator and +re-verify before promotion. diff --git a/debug/risc-v-gsp-rm-vm/docs/35-GREENEYE-SIMULATOR-DESIGN.md b/debug/risc-v-gsp-rm-vm/docs/35-GREENEYE-SIMULATOR-DESIGN.md new file mode 100644 index 0000000..83954d4 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/35-GREENEYE-SIMULATOR-DESIGN.md @@ -0,0 +1,291 @@ +# 35 — GreenEye simulator design (single design document) + +> **Status:** in development (2026-10-03). Phase S (standalone GSP simulator) +> is the only implementation scope until its pause gate; Phase G (GPGPU-Sim +> board model) is designed and gated, not started. +> +> **Audience:** agents and maintainers of risc-v-gsp-rm-debugger. +> + +--- + +## 0. What this document is + +One consolidated design for the simulator program requested for +risc-v-gsp-rm-debugger. It covers, in order of execution: + +1. **Phase S — standalone GSP simulator** (now): execution of the real + firmware under the modelled, evidence-labelled device environment (L0/L1; + silicon accuracy is **not** claimed), full tool/workflow integration, and + the above-the-QEMU-line contract on the frozen checks (docs/37 §3). + **The program pauses at Phase S exit** before any GPGPU-Sim work. +2. **Phase G — GPGPU-Sim full-board model** (later, gated): TU104 board + devices (GMMU, FBIF, FALCON→GMMU DMA, vidmem heap, VBIOS), GSP↔core + interactions, PCIe/VFIO bridge for a vGSP VF, QEMU-gdb time sync, MCP + extension, multi-GPU/fabric backlog. +3. **Plan B** — the narrowly scoped reference plan for "a simulator simply + better than the QEMU baseline for the open-iov workflow" + (`../Handoff-Simulator-Plan-B.md`, private), with an explicit Plan A/Plan B + comparison and decision procedure. + +Owner directives are numbered U1–U45 in the private handoff (§1 and §13); +this document restates the operative ones and never contradicts them. + +## 1. Definitions + +### 1.1 The QEMU line (the bar to beat) + +The **QEMU baseline** is the v0.1/v0.2 tool behaviour measured on the real +GSP image: RSP transport, hooks/breakpoints/watchpoints, memory/struct/ +snapshot tooling, sessions, symbols/analyze/compare, probes/plugins, Ghidra +symbol + control bridges, TCG trace, injection SDK, `Session.call`/ +`run_until`; and on the real `gsp-570.144.fwimage.bin`: relocation-free flat +load, first exception at firmware vaddr `0x4009014` (load access fault +reading `0x2000000000000A00`), then `mtvec=0` fetch loop. Planned QEMU +features (device model H2.2, boot shims H2.3, deterministic multi-core H2.4, +RM service injection H1.3, trace→session folding R4, debugger parity H1.5) +are **part of the bar** even though QEMU has not implemented them. + +**"Above the QEMU line"** means, at minimum: + +* executes the real 570.144 image deterministically to at least one recorded + milestone **past** the QEMU first fault (ladder in §3.3), with the device + model and shims documented; +* every QEMU feature available against the native backend, plus the planned + QEMU features above; +* every run produces `greeneye-session/1` evidence with simulator build hash, + firmware hash, profile/hash, shim assumptions and fidelity labels; +* run-to-run determinism proven; cycle counts emitted and labelled *modeled*; +* a real-workload demonstration whose eng-ops baseline/threshold were frozen + before the run. + +### 1.2 Fidelity labels + +`L0` instruction semantics, `L1` memory behaviour, `L2` time/concurrency, +`L3` device environment (extended from docs/06). The standalone GSP +simulator targets **L0–L2 for the GSP core and its device model**, with L2 +explicitly labelled *modeled* (table/source-derived) until calibrated. The +GPGPU-Sim board model adds board-level L3. No silicon-accuracy claim without +calibration evidence and a declared uncertainty band. + +### 1.3 Accuracy metrics + +* `M1–M6` — the existing metric contract (inventory, replies, ordering, state, + error paths, memory side effects; docs/06). +* `C1–C6` — cycle calibration (instruction latency agreement, memory latency, + bandwidth, kernel correlation, determinism, clock-domain ratio); bands are + frozen in the private handoff §9.2 and must be recorded before first use. + +## 2. The workflow this exists to serve + +The simulator must move one of the project's OODA loops faster with evidence +(eng-ops §3): **dynamic RE** (what does the firmware do?), **portability** +(new firmware/device triage), **adversarial** (unexpected behaviour), and +**correctness** (does our emulation match?). Time-to-value is the currency: +every capability below names the baseline it replaces and the number that +decides keep/redesign/retire. + +## 3. Phase S — standalone GSP simulator (NOW) + +### 3.1 Architecture + +``` +greeneye control plane (Python; unchanged public API) + Session · hooks · bp/watch · probes · plugins · recorder · analyze/compare + Ghidra bridge · MCP · injection SDK + │ Backend ABI (typed; QEMU and native are peers) + ▼ +native backend (sim/) + machine.py RV64IMAC core, deterministic virtual time, cycle model + devices/ NEW: GSP device model (falcon/SEC2 windows, CSB/TRANSCOM, + WPR2/FWSEC state, PTIMER, IRQ routing, DMA/IOMMU, PMU stub) + profiles/ NEW: versioned device profiles (JSON) + boot shims + trace.py exec/access/cycle events → session blob + moments (S2) snapshot/restore + deterministic replay +``` + +*The QEMU backend stays untouched and remains the differential reference.* +Backend selection is explicit (`--backend qemu|native`); no shared mutable +state; the session format evolves additively (`greeneye-session/1` + new +event classes `cycle`, `device`, `mcu`). + +### 3.2 Work packages + +| WP | Name | Deliverable | Agent | +|---|---|---|---| +| S0 | baseline (done) | native core, session parity, M1.5 trace, modeled cycles, real-image bounded run | — | +| S1 | GSP device model + boot ladder | `sim/devices/` + `sim/profiles/` + milestone sessions + tests | S1 | +| S2 | debugger parity | catchpoints, core/thread selection, memory-region metadata, moments/replay, parity matrix | S2 | +| S3 | MCP + scripting + injection | MCP tools against native backend; injection against the simulated GSP; docs | S3 | +| S4 | real-workload validation | boot-ladder A/B vs QEMU probe; scenario harness; value demo + report | S4 | +| S5 | stability/packaging | long-run soak, error taxonomy, session size budgets, quickstart, release notes | successor | + +### 3.3 Boot ladder (S1 acceptance) + +Each rung is a recorded session with a session hash; each rung must be +reproducible twice identically and have its device requirements listed. + +| Rung | Milestone | Current status | +|---|---|---| +| B0 | flat image loads; first MMIO observed at `0x2000000000000A00` | done (native, lenient) | +| B1 | device model serves the whole boot path: 0 unknown MMIO, 0 unhandled in windows; deterministic 2M-instruction run stopping at the boot-halt/report loop (`0x4010928`) | done (S1 device model; sessions `dumps/gsp-boot-b1-run1/2`) | +| B2 | boot stage progress: vector table/TCM/MPU/trap handling exercised with the shim; passes the bootargs consumer and reaches the relocation copy, then stops on the known DMEM-runspace unknown (`fault @ 0x400EA0C`; docs/36 §4) | done (shim) | +| B3 | first CSB/TRANSCOM mailbox poll with correct read-back semantics | target | +| B4 | message loop reached (`appMain`/equivalent); waits on host message deterministically | target | +| B5 | one scripted host-side RM message served end-to-end with a recorded reply | target (gated by S3 injection) | + +The ladder is deliberately versioned-data driven: a new firmware version is a +new profile + shim record, not a code fork. + +### 3.4 What is explicitly not in Phase S + +GPGPU-Sim changes, board devices beyond the GSP complex, guest/PCIe models, +VBIOS, VF bridge, Ampere/Blackwell, fabric — all Phase G or backlog (§10). + +### 3.5 Pause gate + +Phase S ends when: S1–S4 exits met; suite green incl. new tests; a real +workload demo exists with a frozen baseline and measured time-to-value; +`docs/30–40` updated; adversarial review (§9) signed off. **Pause and report +before any Phase G work.** + +## 4. Phase G — GPGPU-Sim board model (later, gated) + +This is the previously requested GPGPU-Sim program, now explicitly sequenced +after Phase S. It is *designed* now so Phase S artifacts (device map, +profiles, session/event classes, metrics, MCP surface) fit it without rework. + +### 4.1 Board device inventory (TU104 family) + +Expanded from owner requirements (GMMU, FBIF, FALCON→GMMU DMA, vidmem heap) +and the porting dossier. + +| Device | Model target | Evidence class | +|---|---|---| +| GMMU (+ PTEs, page tables, ATS/IOMMU seam) | page-table walk, TLB, fault reporting, VA→PA translation for firmware descriptors | register/field maps + formats; A/B vs probe | +| FBIF (FB interface, compression, ECC) | FB access path, alignment/partition hashing, vidmem heap model | register maps; documented assumptions | +| FALCON→GMMU DMA line | descriptor-driven DMA with cycle/latency accounting | boot/mainline firmware usage; source traces | +| vidmem heap stack | allocator map, reserved regions, WPR2 carve-outs | boot state + shims | +| GSP core + CSB/TRANSCOM | message queues, doorbells, interrupts | Phase S artifacts | +| PMU / SEC2 / booter / ACR/SOE | interfaces, mailboxes, state machines, latencies | Phase S MCU models | +| VBIOS/PROM | image layout, init tables, device declaration for guests | public parsing + profile | +| PCIe endpoint | config space, BARs/MMIO windows, MSI-X, TLPs, D2D | descriptor + QEMU/GDB observations | +| VF bridge | vGSP VF traffic → simulator BAR/MMIO instead of real GPU | design-only until human approval | + +### 4.2 Light-touch VF bridge (design) + +* Userspace gpgpu-sim hosts the board model; a future (separately approved) + kernel module presents a PCIe/VFIO device FD. +* Protocol: shared-memory queue + eventfd doorbells; simulator services + MMIO/BAR reads/writes, MSI-X injection and DMA windows as messages; no + changes to `vgsp` primitives or data paths. +* **Hard rule:** no `vgsp` kernel-module edits without explicit human + approval. Phase S/G implement everything up to that boundary; the module is + design + tests on stubs only. +* A conformance harness replays recorded QEMU/VFIO traffic against the + simulator model and compares replies (accuracy loop), so the bridge is + proven before any kernel work is proposed. + +### 4.3 QEMU-gdb time sync (design) + +Guest QEMU and the simulator exchange a clock-sync protocol (periodic +time-base beacons + event ordering barriers). Deterministic mode: simulator +virtual time is the master; QEMU is granted step/sync credits. Never a +security boundary; no confidentiality claims. + +### 4.4 MCP extension (design) + +The existing 31-tool MCP surface grows simulator-native tools: run/step with +budget, cycle queries, device-state dumps, MCU state, boot-ladder report, +board topology, DMA/TLP traces. Read/observe default; writes/step gated by +flags; every call attributable to a session. + +## 5. Feature parity matrix + +| Capability | QEMU (current) | QEMU (planned) | Native GSP (now) | Native GSP target | +|---|---|---|---|---| +| RSP-free transports/launch | yes | — | launch/attach | yes | +| Hooks/watchpoints/breakpoints | yes | catchpoints/core select | hooks/watch/bp | full parity (S2) | +| Memory/struct/snapshots | yes | region metadata | memory/struct | snapshots/moments (S2) | +| Sessions/analyze/compare | yes | trace folding | yes + cycles | trace folded, coverage (S2) | +| Probe/record/plugins | yes | — | backend peer | yes | +| Ghidra symbol/control bridges | yes | watch/heat-map/nav | protocol-compatible | full (S2/S3) | +| Injection SDK | demo | RM-core interception H1.3 | demo backend | services vs GSP model (S3) | +| Device model / real boot | no (faults) | H2.2/H2.3/H2.4 planned | ladder B0–B2 (S1, device model + shim) | B4/B5 (S1/S3) | +| Determinism/replay | wall-clock | H2.4 budgets | instruction-count | event replay (S2) | +| MCP | yes (QEMU) | — | native sim tools + backend selection (20) | wider GSP-model tools (S3) | +| Tiering | — | — | modeled cycles | calibrated bands (S4) | + +## 6. Boundaries and invariants + +1. All changes live under `debug/risc-v-gsp-rm-vm/` (private handoff copies at + workspace root excepted). Never edit project directories outside it. +2. **No `vgsp` kernel-module edits without explicit human approval** (U33). +3. No physical GPU, mdev, guest, module loads, `/dev` or reboots from agents; + RM 610 is a no-fly zone (docs/23); no unattended cycles (RUNBOOK P1–P9). +4. The simulator is a study environment, never a product dependency, never in + a guest, no security claims (RUNBOOK P5/P6). +5. Upstream GPGPU-Sim stays a pinned read-only checkout; our changes are + tracked patches + overlay (Phase G), never a silent fork. +6. The QEMU path must remain green while native features land (differential + suite is the regression net). + +## 7. eng-ops admissions + +`gpgpu-sim/ADMISSION.md` holds draft records TOOL-2026-SIM-01..05 (build/run, +TU104 calibration, GSP-in-sim, MCU model, debugger backend) with the five +required fields; Phase S adds one admission record per S work package (S1 +device model, S2 parity, S3 MCP/injection, S4 validation). Thresholds are +frozen before the first run and never moved silently; every result carries a +session directory and hashes. + +## 8. Real-workload proof (S4) + +The "real workload" for Phase S is the open-iov workflow on the real image: +boot the 570.144 GSP to the highest ladder rung, expose device/MCU state +through the standard tooling (CLI/Python/MCP/Ghidra), serve one scripted +message, and record sessions. Baselines: (a) QEMU `probe` first fault; +(b) static analysis time estimates from the project handoff. The demo is +accepted only if it beats the frozen threshold; otherwise the failure signal +triggers redesign per eng-ops §4. + +## 9. Adversarial review + +| Risk | Failure signal | Mitigation | +|---|---|---| +| "Accuracy" theatre (lenient MMIO masquerades as a model) | rung claimed with lenient policy | each rung requires explicit device semantics; lenient runs labelled | +| Device model guesses without evidence | unexplained delta in A/B | every register backed by source or observed firmware use; record assumption | +| Parity breaks the QEMU path | differential suite red | QEMU untouched; feature-flag additions | +| Scope creep into Phase G | files outside `sim/`+docs | gate checks; private handoff scope contract | +| Non-determinism | replay delta non-empty | virtual-time only; double-run tests | +| False "above QEMU" claim | missing A/B session | acceptance requires session pair + hashes | +| Kernel-module temptation | any `open-iov/kernel/` diff | hard rule U33; reviews | +| Time-to-value negative | measured baseline worse | per-WP admission thresholds | + +## 10. Plan B comparison plan + +`Handoff-Simulator-Plan-B.md` (private) defines the narrow fallback: the +smallest simulator that is simply better than QEMU for the open-iov workflow. +At Phase S exit (and earlier if a work package fails twice), compare Plan A +output vs the Plan B design on: rung reached, feature coverage, determinism, +cycle evidence, integration effort, maintenance cost, and measured +time-to-value. Decision: **keep Plan A**, **apply Plan B lessons in place**, +or **start the Plan B implementation** (with a migration note and re-pilot). +The comparison and decision are recorded in the private handoff. + +## 11. Status and wave log + +| Wave | Date | Result | +|---|---|---| +| Recon + master handoff | 2026-10-03 | `handoff-simulator.md`, local sources register | +| W1 GPGPU-Sim build | 2026-10-03 | CUDA 12 port patches; `vectorAdd` functional+perf green | +| W1 TU104 config/overlay | 2026-10-03 | config scaffold + sanity 26/26 + admissions draft | +| W1 GSP cycle-0 | 2026-10-03 | modeled cycles; real image 200k instr deterministic; 298 tests green (2026-10-03 suite run; count varies with prerequisites) | +| W1 dossier | 2026-10-03 | Turing/Ampere/Blackwell parameter docs + cross-generation survey | +| W2 Phase S (S1 + S4) | 2026-10-03 | `sim/devices` + `sim/profiles` device model: B0–B2 on the real image (0 unknown MMIO), 2-run determinism; A/B harness discovers the profile and records verdict `above` on the frozen checks (docs/36, docs/37 §4.3, docs/40) | +| W2 Phase S | launched | S2 parity, S3 MCP/injection, S4 validation | + +Backlog (recorded, not started): Ampere/Blackwell models; fabric (PCIe, +NVLink/C2C, RDMA/NIC/switch/cable/transceiver, NVMe TLPs, bounce buffers, +DRAM tiers, rendezvous hashing); 8-GPU chassis; H100 SuperPOD RA; QEMU-gdb +time sync; VBIOS; VF bridge module; Verilog/FPGA; multi-version sweeps. diff --git a/debug/risc-v-gsp-rm-vm/docs/36-GSP-DEVICE-MODEL.md b/debug/risc-v-gsp-rm-vm/docs/36-GSP-DEVICE-MODEL.md new file mode 100644 index 0000000..9516fd5 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/36-GSP-DEVICE-MODEL.md @@ -0,0 +1,175 @@ +# 36 — GSP device model (in development) + +> **Status: in development (2026-10-03, S1 work package).** This page +> documents the `sim/devices/` device model, the versioned +> `sim/profiles/` documents and the real-firmware boot-ladder results. It is +> the companion to `35-GREENEYE-SIMULATOR-DESIGN.md` §3 (Phase S) and +> `06-ACCURACY.md`. Development-only: nothing here authorises GPU hardware, +> kernel modules, mdev, guest access or reboots. +> +> **See also:** `37-SIM-QEMU-LINE.md` (the B0–B2 device model on the frozen +> simulator-vs-QEMU checks) and `40-NEW-VERSION-TRIAGE.md` (one-command triage +> of a new image through this ladder). + +## 1. What this is + +The device model gives the native RV64 core (`sim/machine.py`) real MMIO +semantics for the windows the real GSP firmware touches while booting, so a +bounded run advances **past the QEMU first fault** with evidence instead of +lenient zero-reads. Modeled devices take precedence over the lenient +device-map/unknown fallback; an access inside a modeled window that the +device does not implement is still reported as unknown (never silently +zeroed). Every modeled register carries an **evidence kind**: + +* **header** — a hardware register/field map (offset, width, access type, + field positions, reset/alignment); +* **firmware use** — the disassembled firmware code that reads/writes it; +* **observed access** — the address/width/direction/PC/value sequence seen in + a real run; +* **shim** — reset-time state the real SEC2 booter/host would provide; always + an explicit, session-recorded assumption, never a device claim. + +If a window cannot be proven, none of it is modeled and it is listed as an +unknown (§6). + +## 2. Architecture + +``` +sim/profiles/<name>.json versioned profile: windows, chip id, load layout, + │ DMA aperture windows, boot-shim values + ▼ +sim/devices/__init__.py registry_from_profile(profile, machine) + │ + ├─ base.py DeviceRegistry: range+size decode, access log, + │ JSON state/snapshot, peek() debugger surface + ├─ pmc.py PMC/master block (NV_PMC_BOOT_42 chip ID) + ├─ falcon.py falcon block: IRQ, MAILBOX0/1, DEBUGINFO, DMA, + │ FBIF control/transcfg + ├─ priscv.py RISC-V-local block: IRQ mask, trace, error regs + ├─ timer.py PTIMER (modeled virtual time; opt-in) + ├─ csb.py CSB/TRANSCOM message window seam (B3 landing) + ├─ fwsec.py WPR2 address pair (FWSEC state unmodeled) + ├─ pmu.py PMU stub: observes, never fakes + ├─ dma.py aperture → RAM translation seam (FBGPA/SYSGPA) + └─ irq.py falcon + RISC-V-local IRQ aggregation + ▼ +sim/machine.py Machine(devices=..., translator=..., reset_regs=...) + ▼ +sim/gsp_boot.py bounded real-firmware run + session + rung +``` + +`Machine` consults devices after RAM/UART and before the device map/lenient +fallback; the aperture translator (`dma.py`) is consulted first so +FBGPA/SYSGPA memory accesses resolve to RAM. Overlapping device windows +resolve last-registered-first, and a window that does not model an access +defers to the next matching window (safe fallback), so a narrow overlay can +serve some offsets without hiding the wider device. `Machine.device_state()` +and the `greeneye-sim-devices/1` snapshot expose all device state as +serialisable data (snapshot/restore support is additive and not yet wired to +moments). + +## 3. Modeled inventory + +Offsets are relative to each block base. `RW` semantics are storage unless +noted; every unlisted offset is unhandled and logged. + +| Block | Window(s) (PRIV unless noted) | Modeled registers | Evidence | +|---|---|---|---| +| PMC | `+0x0` (size 0x1000) | `BOOT_42` @0xA00: ARCHITECTURE 28:24, IMPLEMENTATION 23:20, MAJOR 19:16, MINOR 15:12, MINOR_EXT 11:8, CHIP_ID 28:20 (TU104 = 0x164), read-only | header + firmware use + observed (first MMIO) | +| Falcon | `+0x110000`, alias `0x300000000` | `IRQSSET` 0x000 (`irq_status \|=`), `IRQSCLR` 0x004 (`&= ~`), `IRQSTAT` 0x008 (read), `MAILBOX0/1` 0x040/0x044, `OS` 0x080, `DEBUGINFO` 0x094, `CPUCTL` 0x100, `BOOTVEC` 0x104, `DMACTL` 0x10c, `DMATRFBASE` 0x110, `DMATRFMOFFS` 0x114, `DMATRFCMD` 0x118 (transfer on write), `DMATRFFBOFFS` 0x11c, `DMATRFBASE1` 0x128, `FBIF_TRANSCFG(0..7)` 0x600, `FBIF_CTL` 0x624 | header + firmware use + observed | +| RISC-V local | `+0x111000`, alias `0x300001000` | `IRQMSET` 0x2ac, `IRQMCLR` 0x2b0, `IRQMASK` 0x2b4 (read), `IRQDEST` 0x2b8, `IRQTYPE` 0x2bc, `TRACECTL` 0x344, `TRACE_RDIDX` 0x348, `TRACE_WTIDX` 0x34c, `PRIV_ERR_STAT` 0x360, `PRIV_ERR_INFO` 0x364 (read), `PRIV_ERR_ADDR` 0x368 (read), `HUB_ERR_STAT` 0x36c | header + observed | +| PTIMER | `+0x9000` | `TIME_0` aperture 0x9400 / block `+0x400` (NSEC 31:5), `TIME_1` aperture 0x9410 / block `+0x410` (NSEC 28:0); value = instruction count × `ns_per_insn` (**modeled**, not calibrated) | header + **modeled** time | +| WPR2 | `+0x1FA000` | `WPR2_ADDR_LO` 0x1FA824, `WPR2_ADDR_HI` 0x1FA828 (4 KiB alignment; HI points at the last 128 KiB block) | header + firmware use | +| CSB/TRANSCOM | profile-declared | none by default; a profile may declare individual registers with width/value and `rw`/`ro`/`wo`/`w1c`/`set`/`clear` semantics | none yet (B3 landing point) | +| PMU | profile-declared | none; the stub records accesses and returns unhandled | none yet | +| DMA seam | FBGPA/SYSGPA (profile) | GPU-physical → RAM windows for falcon DMA and aperture loads/stores; unmapped transfers are refused and recorded | address-map header + firmware use | + +Falcon DMA follows the header field semantics: `size = 1 << (SIZE+2)`, +`IMEM`/`DMEM` aperture select, `WRITE` direction, address +`((BASE1<<32 | BASE) << 8) + FBOFFS`, `IDLE` set on success and `ERROR` on a +refused transfer. Which FB/SYS aperture a DMA address targets (the +`FBIF_TRANSCFG.TARGET` decode) is **not proven yet** and is listed in §6. + +### 3.1 Boot shim (declared assumptions) + +The firmware's `_start` consumes boot arguments the real SEC2 booter/host +writes; the simulator reconstructs them from the documented structure layout +and records each as an assumption in `session.json.boot_shim.assumptions`: + +* `NV_RISCV_BOOT_PARAMS` at a RAM address (bootType `NVRM`, size 120, + version 1, core-dump buffer, one `WPRM` libos init arg); +* a `GspFwWprMeta` (magic/revision/verified and a self-consistent FB layout); +* raw-image mirror at the DMEM aperture base so the app's own relocation copy + is self-consistent (it is not: see §6); +* `a0`–`a5` register arguments (bootargs PA/VA, ELF PA/size, load base, WPR + id). + +The DEBUGINFO **host-consume** behaviour is also a shim: the firmware writes a +boot-stage marker and polls bits 22:20 until they clear, and no self-clearing +path exists in the firmware, so the host side is modelled as consuming the +stage marker on read. The device never changes a read value except for this +documented shim, and the shim is profile-enabled and session-recorded. + +## 4. Boot ladder results (observed, 2026-10-03) + +| Rung | Claim | Evidence | +|---|---|---| +| **B0** | flat image loads; first MMIO is the unmodelled PMC `BOOT_42` read at `0x2000_0000_0000_0A00` | `tests/test_sim_devices.py` (B0 classification) | +| **B1** | device model serves the whole boot path: **0 unknown MMIO, 0 unhandled in windows**, deterministic 2M-instruction run stopping at the boot-halt/report loop (`0x4010928`) | `run_boot(use_shim=False)`; sessions `dumps/gsp-boot-b1-run1`, `dumps/gsp-boot-b1-run2`; test `test_no_shim_b1_device_model_serves_boot` | +| **B2** | with the shim, the firmware passes the chip gate, MPU/vector setup, the bootargs consumer (`0x4007A30`) and reaches its **relocation copy** (`0x4007B7C`), then stops at a known unknown (DMEM runspace) | sessions `dumps/gsp-boot-b2-run1`, `dumps/gsp-boot-b2-run2`; test `test_shim_reaches_relocation_b2` | + +Reproduction (read-only; no hardware): + +```bash +cd open-iov/debug/risc-v-gsp-rm-vm +python3 -m sim.gsp_boot --instructions 2000000 --record dumps/gsp-boot-b2-run1 +python3 -m unittest tests.test_sim_devices -v +``` + +Two-run determinism is asserted by `test_two_runs_identical` and was checked +at the recorded B2 sessions: identical instruction count (14 682), stop +reason/PC (`fault @ 0x400EA0C`), device-state SHA-256 +(`3fa264b9…`) and register file. The B1 no-shim run is likewise identical +across runs (2 000 000 instructions, budget stop at `0x4010928`, device-state +SHA-256 `88eb07b7…`). + +## 5. Session evidence + +Each `run_boot` invocation writes a `greeneye-session/1` directory with: + +* `session.json` — firmware SHA-256, profile name, load layout, rung, + milestones, stop reason/PC, unknown-MMIO counts, device-state SHA-256, + device-access counts and the full boot-shim assumption list; +* `blobs/device-state.json` — the `greeneye-sim-devices/1` snapshot of every + device (registers, IRQ state, DMA windows/counters, shim flags); +* `events.jsonl` — the normal session event stream (`gsp_boot` note). + +## 6. Unknowns (explicitly not modeled / not verified) + +1. **DMEM runspace content.** The app's relocation copy reads the DMEM + aperture (`0x1_0000_0000+`) and writes it to the `0x400E000` runtime + region; the bootloader-provided content is not reconstructed, so the copy + writes zeros and the run stops at `0x400EA0C` (illegal instruction). This + is the B2 ceiling today. +2. **CSB/TRANSCOM register semantics.** No message-window poll has been + observed yet, so no register is modeled. B3 waits for the first observed + poll; the `csb.py` seam and profile register declarations are in place. +3. **PMU.** No PMU register access observed; stub only. +4. **FWSEC state machine.** Only the WPR2 address pair is modeled; FWSEC + execution is not. +5. **DMA aperture selection.** `FBIF_TRANSCFG.TARGET`/`MEM_TYPE` decode is + not proven; only the profile's DMA windows decide what an address means. +6. **Interrupt delivery.** Falcon/RISC-V IRQ state is aggregated and + serialisable, but mstatus/mie/mip/mtvec trap entry and interrupt injection + are not modelled. +7. **PTIMER calibration.** Values are modeled from the instruction count with + a placeholder `ns_per_insn`; no firmware PTIMER read has been observed and + no timing claim is made. +8. **MPU translation.** The firmware programs its MPU CSRs (observed), but + the machine does not enforce translations; the libos private mapping is + served by explicit device aliases and the FBGPA/SYSGPA seam, not by an MPU + model. + +Nothing in this page is a silicon-accuracy claim; `06-ACCURACY.md` fidelity +levels apply (L0/L1 modelled, L2 modelled-not-calibrated, L3 partial and +listed above). diff --git a/debug/risc-v-gsp-rm-vm/docs/37-SIM-QEMU-LINE.md b/debug/risc-v-gsp-rm-vm/docs/37-SIM-QEMU-LINE.md new file mode 100644 index 0000000..42c76cc --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/37-SIM-QEMU-LINE.md @@ -0,0 +1,249 @@ +# 37 — Simulator vs the QEMU line: evaluation harness + real RE proof + +> **Status: in development (2026-10-03).** This document is the S4 (value +> validation) record for the standalone GSP simulator workstream. It defines +> the evaluation harness, freezes the thresholds used, reports the first +> measured A/B run on the real TU104 570.144 image, and records the U51 +> real-reverse-engineering demonstration. Nothing here is a product claim: +> simulator evidence is L0/L1 (docs/06). +> +> **Update (2026-10-03, later):** S1 (device model + profiles + boot shim) +> landed and the harness was wired to it; §4.3 records the re-measured A/B +> where the native side is the S1 `sim.gsp_boot` path (verdict `above` on the +> frozen checks). §4.1/§4.2 remain the pre-S1 measured record, and §7's +> "S1 not landed" limit applies only to those sections. +> +> **Artifacts:** `harness/sim_qemu_line.py` (A/B harness), +> `tests/test_sim_qemu_line.py` (shape + determinism + QEMU regression), +> `harness/re_demo.py` + `harness/re-demo-report.md` (real RE proof). +> **Line definition source:** docs/35-GREENEYE-SIMULATOR-DESIGN.md §1.1 and +> the boot ladder in §3.3. + +--- + +## 1. The line being evaluated + +The QEMU line (docs/35 §1.1) is the v0.1/v0.2 tool behaviour on the real +image: hooks/watchpoints, memory/struct/snapshot tooling, sessions, symbols +and analyze/compare, probes/plugins, Ghidra bridges, trace, injection SDK and +`Session.call`/`run_until`, plus the documented first fault at firmware vaddr +`0x4009014` (4-byte read of `0x2000000000000A00`). + +The boot ladder (docs/35 §3.3) is `pre-B0` (image loads, first MMIO observed +*as a fault*) < `B0` (first MMIO observed; lenient) < `B1` (first fault past +the QEMU point, explained by device semantics) < `B2` (boot-stage progress) < +`B3` (first mailbox poll) < `B4` (message loop) < `B5` (one scripted message +served). + +**"Above the QEMU line"** requires *all* of the frozen checks in §3, +including the docs/35 §1.1 requirement that the milestone be reached "with +the device model and shims documented". Lenient MMIO is an observation +mechanism, not a device model, so it cannot satisfy the contract by itself. + +## 2. Method + +Everything is stdlib-only, bounded (`<= 2,000,000` native instructions, +`<= 10 s` QEMU), and touches no hardware, GPU, mdev, module, guest or network. + +```bash +cd open-iov/debug/risc-v-gsp-rm-vm +python3 harness/sim_qemu_line.py # A/B harness -> harness/qemu_line/ +python3 -m unittest tests.test_sim_qemu_line -v +python3 harness/re_demo.py # U51 proof -> harness/re-demo-report.md +``` + +* **QEMU side** — `harness.common.open_flat_session` (the same relocation and + `guest_errors,unimp,int` logging as `greeneye probe`), bounded run, first + exception parsed from the bounded head of the log (trap loops flood it), + first-touch device map harvested, session recorded with the log blob. +* **Native side** — a discovered S1 profile runs `sim.gsp_boot` (device model + + recorded boot shim) twice and reports the observed rung; without one, the + lenient `sim.gsp_profile.run_profile` baseline (deterministic RV64IMAC, + modeled cycles) runs twice with `greeneye.device_map` for MMIO. S1's + profile is discovered defensively (`sim.devices` Python entry points, then + `sim/profiles/*.json` matched by firmware sha / `sim/devices/*.json`); when + absent the run is the lenient `B0` baseline and the report says so. +* **Feature matrix** — exercised on the bundled demo firmware + (`firmware/uproc_demo`): hooks, breakpoints, watchpoints, memory, struct + decode, sessions, analyze/compare, exec trace, injection SDK. MCP is + recorded `not_run` here to keep this harness independent of the MCP surface + (the native MCP tools are tested in `tests/test_mcp.py`; docs/20). + +## 3. Frozen thresholds (2026-10-03, before the first measurement) + +Stored in `harness/sim_qemu_line.py:FROZEN_THRESHOLDS` and +`harness/re_demo.py:FROZEN_THRESHOLDS`; they are never moved silently. + +| check | threshold | why | +|---|---|---| +| `qemu_first_fault_reproduced` | QEMU reproduces fw `0x4009014` / `0x2000000000000A00` | otherwise the two sides are not comparable | +| `native_above_qemu` | native rung >= QEMU rung + 1 | the line's rung rule | +| `identical_runs` | 2 identical native runs (key fields) | determinism is a docs/35 §1.1 requirement | +| `native_pair_wall_s` | <= 20 s for the two native runs | keeps the harness usable | +| `device_model_documented` | a discovered S1 device map/profile is used | docs/35 §1.1 "with the device model and shims documented" | + +`re_demo.py` additionally freezes: emulator wall <= 120 s, 5 answers present, +2 identical lenient runs, milestone past the QEMU point; the manual static +baseline is a documented **floor estimate** of 600 s (method note in the +script: ~90 instructions across three sections read by hand and correlated +with the QEMU fault report; not measured). + +## 4. Measured results (2026-10-03) + +### 4.1 A/B harness on the real image + +Firmware `gsp-570.144.fwimage.bin`, sha256 `2d1bb5c60b208cfd…`; S1 absent at +run time (`sim/devices/` and `sim/profiles/` not present), so the native side +ran lenient. + +| side | ran | rung | instructions | cycles (modeled) | stop | wall s | evidence | +|---|---|---|---:|---:|---|---:|---| +| QEMU probe | yes | `pre-B0` | – | – | `fault_load` fw `0x4009014` -> `0x2000000000000A00` | 0.95 | `harness/qemu_line/sessions/qemu-probe` | +| native run 1 | yes | `B0` | 200,000 | 200,012 | `budget@0x4009070` | 0.21 | `harness/qemu_line/sessions/native-1` | +| native run 2 | yes | `B0` | 200,000 | 200,012 | `budget@0x4009070` | 0.22 | `harness/qemu_line/sessions/native-2` | + +Wall figures are from one measured run (host-load dependent); re-run the +command for current numbers. Session hashes for the record are in +`harness/qemu_line/sim-qemu-line.json`. + +* first-touch match with the documented reference: **true** (same address, + same 4-byte read, same fw PC); +* identical across the two runs: **true**; session compare ran (breakpoint, + watchpoint and written-page deltas); +* verdict: **`above_lenient_only`** — rung progress exists (`B0` is one past + `pre-B0`) but the device-model check fails, so `above_qemu_line = false`. + This is the honest underclaim: the native run observes and continues past + the QEMU fault, but lenient MMIO is not device semantics. +* native feature matrix (demo firmware): `hooks`, `breakpoints`, `watch`, + `memory`, `struct`, `sessions`, `analyze_compare`, `trace`, `injection` all + **pass**; `mcp` **not_run** (S3-owned). +* session hashes for every side/session are in + `harness/qemu_line/sim-qemu-line.json` and rendered in `.md`. + +Expected delta when S1 landed (realised; see §4.3): `device_model_used = true`, +rung `B2` because the model serves the QEMU first touch and the shim reaches +the relocation stage, and `verdict = above` because the run stays deterministic +and within the wall bound. S1's profile is picked up automatically from +`sim/profiles/*.json` when its `firmware.sha256` matches (or pass +`--device-map FILE` for an observational map). + +### 4.2 Real RE proof (U51) + +Scenario: *what happens on the real GSP image between entry and the first +fault/milestone, and what does the state tell us about boot?* + +| side | method | wall s | answers | +|---|---|---:|---| +| baseline | QEMU probe (measured, 0.5 s bound) | 0.99 | first fault only | +| baseline | manual static reading (floor estimate) | 600 | entry->fault path by hand | +| baseline | **total** | **600.99** | full question | +| emulator | 3 recorded native sessions (`harness/re_demo.py`) | **0.13** | full question + evidence | + +Frozen threshold: emulator wall <= 120 s, 5/5 answers present, two identical +lenient runs, milestone past the QEMU point. Result: **PASS** (all checks +true). Session hashes and the JSON answers are in `harness/re_demo/result.json` +and `harness/re-demo-report.md`. + +Answer (evidence-backed; simulator L0/L1, no silicon claim): + +1. **First touch:** a 4-byte read of PA `0x2000000000000A00` at fw PC + `0x4009014`; it is exactly the QEMU-documented first fault. +2. **Path:** the entry state is all-zero (no bootloader handoff is modelled); + `.bootstrap` materialises the device PA with five arithmetic instructions, + reads it, then runs a chip-ID comparison chain — 25 executed instructions + from entry to the halt (exec trace). +3. **Field:** the read value's bits `[28:20]` are extracted (`srli 20`, + `andi 0x1ff`) and compared against `354..360` (Turing IDs) and `368` + (Ampere ID). +4. **Dispatch:** `354..360` jumps to `.tu10x_text` at `0x4007000`; `368` + jumps to `.ga100_text` at `0x4005000`; any other value (including the + lenient zero) falls into the fail-closed `j .` loop at `0x4009070`. +5. **Next blocker (B1 candidate):** with a Turing ID served from a one-entry + device map, `.tu10x_text` programs CSRs `0x7c8–0x7cc` (region + descriptors), enables `mstatus` FP/SIMD state, builds the **absolute** + link-time address `0x40070f8` and jumps to it. Because the loader + relocates segments but not absolute immediates, that jump lands outside + RAM and faults (first fault past the QEMU point, 74 instructions). The + next rung therefore needs a chip-ID device model **plus** load-at-link + address (or alias/translation of absolute immediates); the modeled ID + value itself is an assumption (the QEMU baseline faults before reading + it), while the field position and the compared IDs are firmware evidence. + +### 4.3 Re-measurement after S1 landed (2026-10-03, later) + +The harness now discovers the S1 `greeneye-sim-profile/1` document +(`sim/profiles/*.json`, matched by `firmware.sha256`) and runs the native side +through `sim.gsp_boot` (device model + recorded boot shim) instead of lenient +MMIO. Frozen thresholds unchanged. Measured run on the same image +(sha256 `2d1bb5c60b208cfd…`): + +| side | ran | rung | instructions | cycles (modeled) | stop | wall s | evidence | +|---|---|---|---:|---:|---|---:|---| +| QEMU probe | yes | `pre-B0` | – | – | `fault_load` fw `0x4009014` -> `0x2000000000000A00` | 0.98 | `harness/qemu_line/sessions/qemu-probe` | +| native run 1 | yes | `B2` | 14,682 | 27,294 | `fault@0x400EA0C` (DMEM runspace unknown) | 0.16 | `harness/qemu_line/sessions/native-1` | +| native run 2 | yes | `B2` | 14,682 | 27,294 | `fault@0x400EA0C` | 0.17 | `harness/qemu_line/sessions/native-2` | + +* `device_model_used = true`, `device_model_documented = true`, + `identical_runs = true` (device-state sha256 + `3fa264b9f81081e8…` on both runs), `native_pair_wall_s = 0.33`, + `native_above_qemu = true`; **verdict `above`** on all five frozen checks. +* milestones observed: `bootargs_consumer`, `relocation_copy`; + `unknown MMIO = 0`, `device_unhandled = 0`. +* What this does **not** claim: B3/B4 are not reached (the run stops at the + documented DMEM-runspace unknown); the shim values are declared assumptions; + cycles are modeled (docs/32); no timing/device-environment claim. +* Command: `python3 harness/sim_qemu_line.py` (regenerates + `harness/qemu_line/sim-qemu-line.json|.md`). Wall figures are host-load + dependent. + +## 5. Failure signals (when to disable, per U48/U56) + +* Any frozen check false **with QEMU available and the firmware present**: + keep `above_qemu_line = false`, do not claim the line. +* `device_model_documented` still false at the Phase S pause review: the S1 + work package has not met its acceptance; the harness keeps reporting + `above_lenient_only`. +* A feature matrix entry `fail`: disable that feature flag and record it as a + failed experiment; an entry silently stuck at `not_run` blocks the S4 exit. +* `re_demo.py` pass false: U51 is not satisfied; fix the tool chain before + any Phase G design work. +* Thresholds change only with a dated entry in this document; a moved + threshold without a measurement is a review failure. + +## 6. Re-running / reproducing + +```bash +cd open-iov/debug/risc-v-gsp-rm-vm +python3 harness/sim_qemu_line.py # full A/B + feature matrix +python3 harness/sim_qemu_line.py --no-qemu # native only, no baseline claim +python3 -m unittest tests.test_sim_qemu_line -v # shape/determinism/QEMU gate +python3 harness/re_demo.py # real RE proof +``` + +Without the 570.144 image the commands skip cleanly (exit 2 for the CLIs, +skipped tests), because the tool tree is used on machines without the +firmware. The QEMU side also needs `qemu-system-riscv64`; without it the +report is emitted with `unverified_qemu_absent`. + +## 7. Honest limits + +* **S1 partial** — the device model reaches B2 (relocation copy) with the + recorded boot shim; B3/B4 (CSB poll, message loop) are not reached and the + DMEM-runspace unknown is the documented ceiling (docs/36 §6). The §4.1 + lenient record predates S1; §4.3 is the post-S1 measurement. +* **L2/L3 partial** — cycles are modeled (`rv64imac-modeled/1`, docs/32), not + measured; the device model is partial and carries per-register evidence + (docs/36). No timing or full device-environment claim is made. +* **MCP native surface not exercised here** — the A/B feature matrix records + it `not_run`; the native MCP tools have their own suite (docs/20 §9). +* **Modeled chip-ID value is an assumption** — only the field extraction and + the compared ID set come from the image; in the S1 profile the `0x164` + value is declared by the profile, and the boot shim values are declared + assumptions recorded in the session. +* **Relocation artifact** — the B1-candidate fault at `0x40070f8` is a + property of the bias-relocated harness, not of silicon; it is labelled as + such and not counted as a device milestone. (The S1 path loads at the link + addresses and does not use that relocation.) +* **Manual baseline is an estimate** — labelled a floor with its method; the + measured parts of the baseline (QEMU probe) are reproducible with the + harness. diff --git a/debug/risc-v-gsp-rm-vm/docs/38-GSP-EM-API.md b/debug/risc-v-gsp-rm-vm/docs/38-GSP-EM-API.md new file mode 100644 index 0000000..311e8e1 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/38-GSP-EM-API.md @@ -0,0 +1,139 @@ +# 38 — GSP-Em API: TLV boundary between the GSP emulator and a full-system simulator + +> **Status:** design (2026-10-03). Implementation gated by the standalone +> GSP simulator's validation gate (handoff-simulator.md §13.5/§14). The +> bridge build flag `GREENEYE_GSP_EM_BRIDGE` is **OFF by default** until then. +> +> **Purpose:** define one stable, documented, TLV-framed protocol so the +> standalone GSP emulator can be driven by (and drive) any full-system +> simulator — GPGPU-Sim first, any API-compliant alternative later — without +> either side depending on the other's internals. +> +> **See also:** `35-GREENEYE-SIMULATOR-DESIGN.md` §3.4/§4 (what is and is not +> in Phase S vs Phase G) and `36-GSP-DEVICE-MODEL.md` (the device model behind +> this API). + +## 1. Roles and boundary + +``` + Standalone GSP emulator (this tool) Full-system simulator (GPGPU-Sim or alternative) + ┌──────────────────────────────┐ GSP-Em API ┌────────────────────────────────────────┐ + │ real firmware · device model │◄──(TLV/UDS)───►│ SMs · caches · DRAM · fabric · host │ + │ MCUs · cycles · sessions │ │ consumes/implements the same API │ + └──────────────────────────────┘ └────────────────────────────────────────┘ +``` + +* The GSP emulator is the **authority** for GSP/device/MMIO semantics. +* The system simulator is the **authority** for SM/memory/fabric timing. +* Neither side parses the other's logs: all interaction is API messages. +* Same API is used for A/B replay of captured QEMU/VFIO traffic. + +## 2. Transport and framing + +* Transport: UNIX `SOCK_SEQPACKET` (ordered, message-preserving) at a path + the caller supplies; shared-memory doorbell mode considered later, same + framing. Local only (never a network). +* Every message is one frame: 16-byte header + payload. Little-endian. + +``` +header: magic 'G''S''E''1' (4B) | version u16 | flags u16 | seq u32 | type u16 | rsvd u16 | payload_len u32 +payload: repeated TLV fields +TLV: tag u16 | len u16 | value[len] (pad to 4B boundary with zeroes) +``` + +* Unknown TLVs MUST be ignored (forward compatibility). Unknown message types + MUST be answered with `E_UNIMPLEMENTED` unless `NO_REPLY` is set. +* Version: major in header version; additive minor via `CAPS`. Breaking + changes require a new major and a shim. + +## 3. Message types + +| Type | Name | Direction | Payload essentials | +|---|---|---|---| +| 0x01 | HELLO | both | PROTO_MIN, PROTO_MAX, BUILD_HASH, FW_HASH, PROFILE_ID | +| 0x02 | HELLO_ACK | both | selected version, session ID | +| 0x03 | CAPS | both | CAP_MASK (bitmask), limits (max frame, max DMA), mode flags | +| 0x10 | MMIO_READ | system→gsp | ADDR, SIZE, DEADLINE_CYCLE; reply: DATA or error | +| 0x11 | MMIO_WRITE | system→gsp | ADDR, SIZE, DATA; reply: STATUS | +| 0x12 | IRQ_RAISE | gsp→system | IRQ_VECTOR, DOMAIN, CYCLE | +| 0x13 | DMA_READ | both | GSP_ADDR/PA, SYSTEM_ADDR, SIZE, DEADLINE_CYCLE | +| 0x14 | DMA_WRITE | both | as above | +| 0x15 | CLOCK_TICK | system→gsp | DOMAIN, CYCLE_DELTA; reply: CYCLE_NOW | +| 0x16 | CLOCK_SYNC | both | EPOCH, GRANULARITY, MODE (free-run / credited) | +| 0x17 | RESET | system→gsp | RESET_KIND (cold/warm/domain) | +| 0x18 | STATE_SNAPSHOT | both | REGION selector; reply: STATE blob TLV (opaque, hashed) | +| 0x19 | EVENT_SUBSCRIBE | system→gsp | MASK (mmio, dma, irq, mcu, trap, cycle) | +| 0x1A | EVENT | gsp→system | EVENT_KIND + fields + CYCLE | +| 0x7F | ERROR | both | STATUS, TEXT, ORIGINAL_TYPE, SEQ | +| 0x7E | BYE | both | — | + +Common TLV tags: `ADDR u64`, `SIZE u32`, `DATA bytes`, `VALUE u64`, +`STATUS u32`, `ERROR_CODE u32`, `TEXT utf8`, `CYCLE u64`, `DOMAIN u16`, +`IRQ_VECTOR u32`, `CAP_MASK u64`, `PROFILE utf8`, `FW_HASH bytes32`, +`BUILD_HASH bytes32`, `SESSION_ID bytes16`. + +## 4. Semantics + +* **Ordering:** per-domain FIFO; MMIO/DMA replies carry the `seq` of their + request. The system simulator may issue in-order per domain only; the GSP + emulator may reorder internally but must preserve observable order. +* **Time:** in `credited` mode the system clock advances only by `CLOCK_TICK` + grants; the GSP emulator returns `CYCLE_NOW` so both sides share one + timeline. In `free-run` mode the emulator advances by its own modeled + cycles and reports them in events. Wall clock never enters a decision. +* **State:** snapshots are opaque blobs with a hash; format is the emulator's + `greeneye-session/1` state section. A system simulator may store them but + not parse them. +* **Determinism:** same inputs + same grants ⇒ byte-identical transcripts. + Every conformance run must demonstrate this twice. +* **Security:** study environment only; no confidentiality claim; no message + may be used to defeat a hardware boundary (RUNBOOK P5). + +## 5. Error model + +`OK=0`, `E_UNIMPLEMENTED=1`, `E_RANGE=2`, `E_SIZE=3`, `E_STATE=4`, +`E_BUSY=5`, `E_PROTO=6`, `E_TIMEOUT=7`, `E_DENIED=8`. An error never +terminates the session; `BYE` is graceful. Timeouts are caller-budgeted. + +## 6. GPGPU-Sim bridge (first system simulator) + +* Build-time: `GREENEYE_GSP_EM_BRIDGE=1` compiles the adapter into the + overlay (`gpgpu-sim/overlay/`). Default OFF. +* Runtime: `--gsp-em-endpoint <path>`; absent endpoint ⇒ GPGPU-Sim runs + standalone exactly as upstream (light-touch guarantee). +* Adapter maps: BAR/MMIO trap → `MMIO_READ/WRITE`; GSP doorbell/IRQ → + `IRQ_RAISE`; firmware DMA descriptors → `DMA_*`; simulator clock domains → + `CLOCK_TICK/SYNC`. +* The adapter is additive: no upstream data structure or algorithm is + modified; issues go through the existing device-model seam only. + +## 7. API-compliant backup (replacing GPGPU-Sim) + +The same API must allow a different system simulator (or a purpose-built +service simulator) to take GPGPU-Sim's place: + +1. **Interface contract:** implement the *system side* of §2–§5 (client for + MMIO/DMA/IRQ, source for CLOCK_TICK, event consumer). +2. **Conformance suite:** `gsp-em-conformance` replays golden transcripts + (hello/caps, MMIO R/W, IRQ, DMA, clock, snapshot, error paths) and must be + byte-identical twice. Any implementation passing it is interchangeable. +3. **Replacement criteria:** passes conformance; sustains required + cycles/s and frame rates; preserves session hashes for the same stimulus; + no changes to the GSP emulator. +4. **Build flag:** the system side selects implementation by + `GREENEYE_SYSTEM_SIM=gpgpu-sim|api-compliant-ref` (design; default + `gpgpu-sim`). + +## 8. Validation plan for this spec + +* Unit: frame codec round-trips, unknown-TLV tolerance, error mapping. +* Integration: emulator ↔ stub system client; credited-clock determinism; + snapshot hash stability across runs. +* Conformance harness is the acceptance gate for both the GPGPU-Sim bridge + and any backup implementation. + +## 9. Non-goals + +No remote/network use; no guest access; no hardware writes; no product +dependency. This document specifies the boundary only; implementation is +gated by the standalone simulator's value proof (handoff §14.5). diff --git a/debug/risc-v-gsp-rm-vm/docs/39-RE-WORKFLOW-REVIEW.md b/debug/risc-v-gsp-rm-vm/docs/39-RE-WORKFLOW-REVIEW.md new file mode 100644 index 0000000..2b94746 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/39-RE-WORKFLOW-REVIEW.md @@ -0,0 +1,678 @@ +# 39 — Reverse-engineering workflow review: tools, real usage, time sinks, stale assumptions and pragmatic upgrades + +> **Status:** review (2026-10-03). Written by a read-only analysis pass over the +> tool tree, the public state page, the private project session logs (cited by +> PART/ADDENDUM and date only; no private paths appear here) and the +> engineering-ops framework. It is a documented review, not a product change; +> nothing in it authorises hardware, module, guest, mdev or RM 610 work. +> +> **Scope of the review:** every reverse-engineering tool/harness/debugger this +> project has built or used, how each has actually been used, where wall-clock +> time is lost, which tool-design assumptions are now stale, how the +> documentation can be made cheaper to maintain, and the smallest set of +> upgrades that would move the dominant cost first. +> +> **Method for reading it:** claims are labelled **[E]** (evidence: a file, +> line, session or recorded event) or **[I]** (inference: the analyst's +> judgement). Section references use `docs/NN` for the tool's documentation set +> and `open-iov/...` for tracked project files. Private project session records +> are cited as `[log PART nn/Add. nn, date]`; they live outside the tracked +> tree and are not named here. + +--- + +## §1 Method and sources + +### 1.1 Question + +The review answers one question for the risc-v-gsp-rm-debugger program: **what +actually reduces the time from "a new GSP/driver release or a live product +wall" to "a supported `vgsp`"** — and where is that time being lost today? + +It is deliberately not a feature wish-list. Every recommendation in §8 carries +the engineering-ops admission fields (scenario, measurable outcome, baseline, +threshold, failure signal) in the shape required by +`open-iov/docs/eng-ops.md` §2. + +### 1.2 Sources read + +| Class | Source | What it provided | +|---|---|---| +| Public live state | `open-iov/STATE-CURRENT.md` (snapshot 2026-10-02 22:12) | milestone ladder, frontier walls, physical-backend record, tool status paragraph | +| Tool documentation | `debug/risc-v-gsp-rm-vm/docs/00`–`38`, README, roadmap | design, accuracy contract, audit, value demos, porting/CI, new simulator work (30–38) | +| Code and harnesses | `greeneye/`, `sim/`, `harness/` (`*.py`, `value_demos/`, `qemu_line/`, `re_demo/`), `plugins/`, `tests/`, `qemu/`, `ghidra/`, `rmharness/`, `gpgpu-sim/` overlay, `sdk/` | what exists and what its tests exercise | +| Recorded evidence | 458 session directories under `dumps/` (counted 2026-10-03), `harness/qemu_line/sim-qemu-line.json`, `harness/re_demo/result.json`, value-demo artifact JSON | what has actually been run, not just built | +| Project ops | `open-iov/docs/eng-ops.md`, `future-eng-ops.md`, `RUNBOOK.md`, `docs/05-operations/03`, `04`, `docs/07-developers/02` | admission discipline, unused-capability catalogue, scenario runbooks | +| Private project logs | the session-part logs and the tool-program ledger (2026-09-24 → 2026-10-03), the progress-analysis log (2026-09-29 → 2026-10-02) | real walls, real costs, real tool usage (cited by PART/date only) | +| Project tooling | Ghidra corpora + headless helper scripts, CUDA porting tools, guest diagnostics, host RM oracle, physical claim harness | the incumbent methods the tool must beat | + +### 1.3 Method and limits + +* **Behaviour framing.** The review describes components and behaviour; it + names no source locations, vendor file paths or filenames. +* **Evidence over recollection.** Cost claims are anchored to recorded numbers + (docs/25 §3–§6, `STATE-CURRENT.md`, PART/date) rather than estimates, except + where explicitly marked as an estimate. +* **No new runs were performed.** No hardware, GPU, module, mdev, guest or + reboot was touched; no test suite was re-run. Test-count claims are therefore + quoted from their source with the source named, and the count discrepancy + found in §5 is reported, not resolved. +* **Sampling.** The 458 session directories were classified by name and + timestamp; individual sessions were sampled, not all read. +* **Fidelity rules apply.** All simulator evidence is L0/L1 unless a source + says otherwise (`docs/06`); every "blocked" real-firmware projection in this + review is marked as such. + +--- + +## §2 Complete RE/tooling inventory + +### 2.1 The tool tree (`open-iov/debug/risc-v-gsp-rm-vm/`) + +Status vocabulary follows `docs/27` §0: **verified useful** (exercised and/or +wired into tests/demos/docs), **in development** (owner-directed work landed +but unfinished; docs 30–38 and the untracked `sim/devices`/`gpgpu-sim` trees), +**pilot** (unadmitted, per `future-eng-ops` F2–F4), **built, not used yet** +(no consumer). + +| # | Tool | Location | Purpose | Who uses it | Evidence of use | Status | +|---|---|---|---|---|---|---| +| 1 | Launcher + CLI (`info/demo/run/attach/launch/probe/record/analyze/compare/symbols/plugins/inject/ghidra`) | `risc-v-gsp-rm-debugger.sh`, `greeneye/cli.py` | entry surface; run/inspect real or demo firmware | agents, owner | audit smokes (`docs/27` §1); sessions in `dumps/` (`demo`, `probe-*`, `inject`) | verified useful | +| 2 | Session/scripting API (`launch`, `attach`, `Session`, hooks, watchpoints, `call`, `run_until`, memory/structs/snapshots) | `greeneye/session.py`, `memory.py`, `structs.py`, `record.py`, `analyze.py` | Frida-like core; sessions as evidence | scripts, tests, value demos | `docs/27` §2; s01–s10 | verified useful | +| 3 | Pure-Python RSP client (QEMU backend) | `greeneye/rsp.py`, `target.py` | transport; no GDB dependency | default backend | demo/probe live-verified (`docs/01` §3) | verified useful | +| 4 | Native RV64IMAC backend (M1) | `sim/decode.py`, `sim/machine.py`, `greeneye/sim_backend.py` | deterministic in-process backend; QEMU parity | tests, demos, new sim work | differential test (`docs/16` §1.1); `sim_diff.py` `PARITY: OK` | verified useful (reference core) | +| 5 | Native exec/access trace (M1.5/R4) | `sim/trace.py` | full instruction/access trace as session blob | record/analyze, value demos | `docs/16` §1.1a; trace tests | verified useful | +| 6 | Modeled cycle counter | `sim/cycles.py` | table-driven modeled cycles (explicitly not silicon timing) | 21 offline tests | `docs/32`; `results.json` of `test_sim_cycles` | in development (increment 0) | +| 7 | GSP device model + profiles + bounded boot runner | `sim/devices/`, `sim/profiles/`, `sim/gsp_boot.py`, `sim/gsp_profile.py` | real 570.144 image past the QEMU first fault (B0–B2) | S1 workstream | `dumps/gsp-boot-b1-run1/2`, `b2-run1/2`; 2-run determinism asserted (`docs/36` §4) | in development (S1) | +| 8 | Declarative probe specs | `greeneye/probes.py`, `examples/probes/` | JSON hooks/conditions without scripts | audit smoke; documented pattern | `docs/27` §3 (3 probes fired) | verified useful | +| 9 | Plugin loader + example plugin | `greeneye/plugins.py`, `plugins/example_plugin.py` | extension point | audit smoke | `plugins` CLI lists all three | verified useful | +| 10 | MCP server | `plugins/mcp_server.py` | agent/CI machine surface (JSON-RPC over stdio/loopback) | no live agent consumer found | 55 tests (`docs/20` §9); tool catalogue 31 (`docs/20` §5) | implemented; **unused in real workflow** (F1 `partially used`) | +| 11 | Injection SDK + `inject` | `sdk/`, `greeneye/inject.py` | position-independent RV64 service load/call, direct + mailbox | tests, s06 | `docs/11`/`12`; audit smoke `0xbeae` both paths | verified useful | +| 12 | Ghidra bridge (symbol export, hit annotation, control) | `ghidra/*.java`, `greeneye/ghidra.py`, `gdbridge.py` | static↔dynamic address join; run-until from decompiler | Ghidra headless (program ledger wave 2) | 25-command bridge tested (`docs/27` §2); Java verified in the tool ledger | verified useful | +| 13 | TCG full-trace plugin | `qemu/greeneye_trace.c` | complete exec/access JSONL without stops | audit live run | 8,012,129 events in ~1 s (`docs/27` §0) | verified useful | +| 14 | Mutation harness | `harness/mutation_harness.py` | bounded deterministic one-field mutation; outcome classes | tests, s08 | seed replay byte-identical (`docs/18` §2) | verified useful | +| 15 | Portability matrix | `harness/portability_matrix.py` | probe+scenario+compare across targets | tests | self-comparison all-zero deltas | verified useful | +| 16 | Release triage pipeline | `harness/version_diff.py`, `harness/ci_release.py` | old/new image diff + hashed bundle; RM610 gate | tests; CI docs | self-diff zero; gate refusals tested (`docs/21`) | verified useful | +| 17 | Control discovery + reachability | `harness/control_discovery.py`, `harness/reach.py` | unknown-call sweep; "did it run / what args" | demo sweep only | 189 probes ≈ 36 s (`docs/19` §3.4); `--surface rm` exits clean | `--surface demo` verified; real-RM surface **built, not used yet** (F5) | +| 18 | Simulator-vs-QEMU A/B harness | `harness/sim_qemu_line.py` | frozen-threshold evaluation on the real image | S4 workstream | verdict `above_lenient_only`, `device_model_used=false` (`harness/qemu_line/sim-qemu-line.json`) at review time; **same-day post-review**: S1 profile discovery wired, verdict `above`, native B2 (docs/37 §4.3) | in development (S4) | +| 19 | Real-RE proof harness | `harness/re_demo.py` | U51 demonstration: entry→first-fault question | S4 workstream | `harness/re_demo/result.json`; baseline floor estimate 600 s vs 0.13 s emulator | in development | +| 20 | Driver-version sweep | `harness/port_driver_versions.py` | 540→current per-version evidence | none in a real sweep yet | 23 tests; dry-run default; versioned inputs absent locally | pilot (unadmitted F2) | +| 21 | New-device scaffold | `harness/port_new_device.py` | read-only device profile capture + pairing | none yet | tests; no profile captured in a workflow | pilot (unadmitted F3) | +| 22 | Trial packaging | `harness/package_sim.py` | deterministic sim+injection bundle for review | none yet | 6 tests; `--pack-image` pre-injected demo | pilot (unadmitted F4) | +| 23 | Value demos s01–s10 | `harness/value_demos/` | measured analog time-to-value vs recorded real baselines | tests; cited in docs 25/27/eng-ops | `artifacts/results.json` (last local run: 3 demos, s01/s04/s05, 1.72 s) | implemented; artifact currently partial (§5) | +| 24 | Host RM oracle harness | `rmharness/`, wrappers for `tools/rmtool`, `rmtop`, `rmq` | read-only host RM identity/memory/engine captures into sessions | audit capture smoke | 3 tests outside CI; read-only allowlist (`docs/17`) | working with caveats (device tables permission-dependent) | +| 25 | GPGPU-Sim overlay | `gpgpu-sim/` | Phase G full-board substrate (gated) | W1 build agents | CUDA 12 build + `vectorAdd` functional/perf (program ledger §13.3); `BUILD-PORT.md` | in development, deferred behind Phase S | +| 26 | Test suite + CI | `tests/` (22 files), `.github/workflows/risc-v-gsp-rm-debugger.yml` | regression net | CI, every change | 155 at audit; 194 claimed at flag removal; 298 `def test_` statically countable and 298 collected in the 2026-10-03 R2 run (live QEMU present) | verified useful; count claim drifted (§5) | +| 27 | Documentation set | `docs/00`–`38`, README, roadmap | design/usage/proof/portability | agents | link checker 61 files OK at wave 8 | mixed: 00–29 audited; 30–38 unindexed elsewhere | + +### 2.2 Project tooling outside the tool tree (incumbent methods) + +| Tool | Location (public ref) | Purpose | Evidence of use | Relationship to this tool | +|---|---|---|---|---| +| Ghidra corpus + headless helper scripts (`DumpFuncs`, `Xrefs`, `Disasm`, `ScanBranches`, `MemCheck`) | `open-iov/tools/ghidra/` | decompile/disassemble/xref the firmware | `[log PART 92/Add.25, 2026-10-02]` decode of the GR commit assert; `[log PART 91/Add.9]` GPFIFO fix | static half of the loop; the bridge joins it to runtime | +| CUDA/userspace porting tools (`cudatool.py`, `diff_corpus.py`, `annotate_corpus.py`, symbol exports) | `open-iov/userspace/cuda/tools/` | userspace version porting | `docs/22` §2; F2 in production use for 570.144 | separate axis (CUDA userspace); the tool handles the firmware axis | +| Guest diagnostics (`tools/guest-diag/`: ioctl tracer, kprobe/tracker scripts, pagemap tools, VM restart scripts) | `open-iov/tools/guest-diag/` | what the stock driver requested/where it blocks | `STATE-CURRENT.md` (guest-wall fixes); `[log PARTs 29–64]` | complements the firmware question; the tool does not replace it | +| Kernel-module debug planes (default-off params, `/sys/kernel/debug/vgsp/*`, `gsp-phys-state`) | `open-iov/kernel/`, `open-iov/docs/eng-ops.md` §10 | live emulation/physical state | every recorded physical cycle | supplies the real-A/B reference for M1–M6 comparisons | +| Supervised physical claim harness | `tools/guest-diag/phys_claim_cycle.sh` (per `STATE-CURRENT.md`) | safe GPU ownership/boot cycles | 3/3 green pre-lock cycles; all later cycles | the expensive oracle the simulator exists to reduce | +| x86 QEMU/guest harness (`vgpu_vm.py`, `arcd`, guest agent) | `open-iov/scripts/` | drive the stock guest | smoke runs (`ALL OK` 64/64) | produces the payload/failure stimulus | +| Evidence archives (per-cycle logs, rings, decodes) | local evidence archive (untracked) | manual evidence packs | cited per milestone in `STATE-CURRENT.md` | the manual method that sessions/analysis should replace | +| Hourly check-back loop | `checkback/hourly.sh` (per progress analysis §1aq) | commits/pushes `STATE-CURRENT.md` monthly-minutes | running since 2026-09-30 | process automation, not an RE tool | + +### 2.3 Actual usage versus build inventory + +**[E]** Of 458 session directories in `dumps/` (2026-10-03): 413 are +`unittest-*` runner sessions, 5 are `opencode-*` agent sessions, 5 are +`gsp-boot-b*` device-model runs, and the rest are demo/probe/inject/record +sessions from self-verification (list sampled). No session directory in the +tree is tagged with a live product-wall question (e.g. a GR-promote or +UVM-push question). The only real-image sessions are `probe-*` first-fault +runs and the `gsp-boot` B1/B2 ladder runs. + +**[I]** The tool has been *used* overwhelmingly to validate itself and to +characterise the real image's first fault; it has not yet been used as the +daily instrument on a live product wall. That is consistent with the tool's own +honesty (`docs/25` §2.1 marks every real case `blocked_on_device_model`) and +with the private logs: the one place the tool is named as a next route for a +live wall — the GR engine-context promote/commit wall — records it as route 3, +unused so far `[log PART 92/Add.25, 2026-10-02; STATE-CURRENT.md:122-125]`. + +--- + +## §3 Usage history: recurring RE tasks, tools, costs, bottlenecks + +The project's real work decomposes into eight recurring task classes. Each row +gives the recorded cost and the bottleneck that made the cost. + +### 3.1 Task class A — guest-initiation walls (userspace driving the emulated vGPU) + +**Typical form:** the stock guest driver/client blocks or returns an opaque +error; find the first unsatisfied expectation. + +**Recorded instances:** `cuCtxCreate` rc=999 and the 20 per-channel +completion counters (five days, `[log PARTs 29–64, 2026-09-24…29]`); UVM +tracker/semaphore waits; the kernel UVM map wait `[log PART 22]`; clock-domain +probe decode (`STATE-CURRENT.md:520-558`). + +**Tools used:** guest diagnostics (strace on ioctls, guest kprobes, pagemap, +counter/tracker samplers), kernel-module debug planes (channel dump, map +add/probe), QMP `gva2gpa`, serial console, and the inline CUDA driver-API smoke +test. **Recorded cost:** ~5 days for the counter campaign; multiple +days-scale walls before that. **Bottleneck [E]:** guest-side observability — +ptrace was broken in the guest (`[log PART 22]`), so every question cost a +custom sampler; the host's VA→PA map was rebuilt/rescanned under live traffic +and the decisive page (counter page, pool page) had no translation until a raw +live dump proved it (`docs/25` §5 s02). + +### 3.2 Task class B — RM control/RPC gaps and opaque server failures + +**Typical form:** the server faults or reports a generic status; the first +error is invisible. + +**Recorded instances:** the 0x59 RM-init chase (cycles 2–13; **~5 h 10 m**, +10+ supervised boots, reply was a generic `0x59`; `docs/25` §3 s01 and §5 s01); +the missing pre-boot messages (fn72/fn73) found only after the NOCAT storm +collapsed 851→5 records `[log PART 91/Adds.2–6]`; the RM log-ring id bug that +hid the first error (`STATE-CURRENT.md:439-457`). + +**Tools used:** raw host-memory reads of message buffers, the server's own +journal ring (once mapped), static source review, supervised cycles; the +Ghidra corpus + headless scripts for the commit path `[log PART 92/Add.25]`. +**Bottleneck [E]:** one experiment per supervised boot and an invisible first +error; static review closed host-side leads but the decisive field was only +visible in a raw live page. + +### 3.3 Task class C — GR engine/channel bring-up and execution + +**Typical form:** allocate channel, commit context, submit work, get a +fault or a soft `INVALID_STATE`. + +**Recorded instances:** the GR ring layout and 20-channel autopin +(`[log PART 30 addenda]`); the first GPFIFO submit fault +(`docs/25` s04/s09: cycles 18–25, **~1 h 41 m**, 8 supervised boots); the +engine-context promote/commit campaign of 2026-10-02 (modes 4–22, **a full +day**, 730+ lines of firmware RE, three wedge-reboot cycles +`[log PART 92/Adds.13–25]`, and the campaign continued into +`STATE-CURRENT.md:98-131`). + +**Tools used:** the module's own ladder modes and debugfs, the decoded status +queue, Ghidra headless (commit assert `0x52a8b04` classified as a *secondary* +failure), host reads. **Bottleneck [E]:** every hypothesis cost one full +boot+cycle (~5–6 min) and the failing path is server-internal; the one +hypothesis with 730+ lines of static RE produced an accepted root cause +(externally-owned VAS) but the next gate still wedged and required a reboot. + +### 3.4 Task class D — physical boot chain and backend bring-up + +**Typical form:** FWSEC → booter → RISC-V → RM init → clean release on the +real GPU. + +**Recorded instances:** FWSEC-only carve proved non-recoverable (fuse-locked), +requiring an approved reboot and retiring the one-way stage +(`STATE-CURRENT.md:466-519`); the first real boot and first GSP-RM message +(`STATE-CURRENT.md:645-705`); cycles 4–13 to init-done +(`STATE-CURRENT.md:72`, `:495-519`); every cycle ran under the supervised +harness. + +**Tools used:** the module-owned claim/boot path, the power-domain D0 harness, +debugfs state (`gsp-phys-state`), pstore inspection, the fail-safe. **Cost:** +10+ supervised cycles for one question; each failure originally needed a +reboot, until the server's own shutdown supplied the missing handoff bit and +made clean releases possible (`STATE-CURRENT.md:672-705`). +**Bottleneck [E]:** the release path: fused parts cannot be host-recovered, so +any wedge converts a 6-minute experiment into a reboot that kills every other +agent on the host. + +### 3.5 Task class E — guest work forwarding and hardware execution (P5/P6) + +**Typical form:** guest-originated pushes must execute on the physical engine +with correct data and exactly-once releases. + +**Recorded instances:** P6a guest-page DMA (`STATE-CURRENT.md:242-270`), P6b +waves 1/1.5/2, P6c relay, all on 2026-10-02; the P6a→P6c span was +**~02:20–08:30 (~6 h)** with one supervised cycle per rung on average +(`docs/25` §3 s06). Two false walls: the guest-RAM file opened without the +large-file flag (root-caused in-run) and the stale shared-memory image that +poisoned cross-run state and cost **7 cycles** +`[log PART 93/Add.29, 2026-10-02]`. + +**Tools used:** the module's forwarding parameters/counters, a guest pagemap +probe, offline reconstruction scripts against the shared guest-RAM image +`[log PART 93/Add.30]`, and the CUDA smoke as the acceptance gate. +**Bottleneck [E]:** cross-run state contamination and the absence of a +running, symbol-aware firmware view; the decisive 12.7 KB push was decoded +*offline from a dead run's memory image*, not from a live session. + +### 3.6 Task class F — firmware portability/version questions + +**Typical form:** does firmware version X behave differently from anchor Y? + +**Recorded instances:** 570.144 vs 570.211 pairing disproven at one full boot +each (`docs/25` s07; `STATE-CURRENT.md:485-490`); CUDA/userspace porting via +the corpus tools (`docs/22` §2, F2). **Tools used:** manual probe + boot +comparison; `version_diff.py` exists but has no admitted real sweep; `run_all` +s07 demonstrates the shape on the analog. **Bottleneck [E]:** each version +question needs hardware or a hand diff; symbols/corpus transfer warnings +(~1% wrapper re-resolution) are a known trap (`future-eng-ops.md` F2). + +### 3.7 Task class G — evidence assembly and status upkeep + +**Typical form:** turn a cycle into a reviewable claim. + +**Recorded instances:** every milestone's evidence pack is assembled by hand +from per-cycle directories (multi-MB logs/rings/decodes) plus the session log; +the value demo s10 packages the same contract in 4.92 s on the analog, and its +"real case" is explicitly device-model-blocked (`docs/25` §3 s10). Status +upkeep is continuous: an hourly check-back loop commits `STATE-CURRENT.md`, +and the session logs are append-only — the main log alone is ~15k lines +(measured 2026-10-03) with the newest entries prepended. +**Bottleneck [E]:** there is no machine-readable cross-cycle evidence index; +every claim is re-narrated in prose, and prose drifts (see §5). + +### 3.8 Task class H — self-verification and audit + +**Typical form:** prove the tool works so its claims can be trusted. + +**Recorded instances:** two adversarial passes (AR1/AR2/AR3), the 67-surface +audit, flag removal, value demos, 194-test claims; the current wave is +device-model B1/B2 with 2-run determinism (`docs/36` §4) and the A/B harness +verdict `above_lenient_only` (`docs/37` §4.1). **Cost:** substantial but +front-loaded; it is the reason the honest `blocked_on_device_model` labels +exist. **Bottleneck [I]:** self-verification consumes the sessions that the +live workflow should be producing, and there is no separate "live-question" +session tag (§2.3). + +--- + +## §4 Time-sink analysis: top 10 recurring costs, ranked + +Ranking is by recorded project minutes lost × frequency × recoverability. +Each item names the evidence and the tool-side lever (if any). + +| # | Time sink | Recorded evidence | Why it costs | Tool-side lever | +|---|---|---|---|---| +| 1 | **Supervised hardware cycle as the only oracle** | ~5–6 min/cycle (`docs/25` §6); 10+ cycles ≈ 5 h 10 m for the 0x59 question; 8 cycles ≈ 1 h 41 m for the ladder; 10 NOCAT cycles; P6a–c ≈ 6 h | every hypothesis = one full boot + fail-safe discipline; cross-question batching is manual | finish device model (S1) so hypotheses resolve off-hardware | +| 2 | **Server wedge → reboot loop** | 22 reboot-request lines in the main log since 2026-10-01 (counted); 8 host-freeze mentions; 3 wedge-reboots in one afternoon `[log PART 92/Adds.13–24]`; a user-paused reboot at 10:25 2026-10-02 | fused-part WPR2 cannot be cleared; a reboot kills all other agents' sessions and breaks continuity | none in-tool today; a recoverable wedge or snapshot-resume would collapse this | +| 3 | **Guest-side observability gaps** | ~5 days for the 20-counter wall; ptrace broken in guest; custom samplers per question `[log PARTs 29–64]` | host emulation state and guest expectations are only visible through ad-hoc probes | pair each guest wall with a firmware-side session instead of a new sampler | +| 4 | **Opaque server-internal failures** | 0x59 invisible until the log ring was mapped (id bug); promote `0x40`/CTXNOTVALID; 730+ lines of static RE for one root cause | first error hidden behind generic statuses and unmapped rings | hooks/watchpoints on the rehosted failure path; session compare | +| 5 | **Work loss / tree hygiene** | PART 44/45 fixes lost from the tree, PART 47 re-root-cause `[log PART 47]`; modes 4–22 uncommitted at the campaign's end; progress analysis notes repeated "highest-loss-risk" moments | long uncommitted experiments die with a reboot/host reset | commit-per-mode discipline; sessions preserve the evidence even when code is lost | +| 6 | **Documentation sprawl and contradiction** | main log ~15k lines + progress analysis ~4.8k + tool ledger ~1k + state page ~0.9k; ≥8 contradictory live status claims found (§5) | every agent re-derives state; wrong assumptions send work down dead ends | single generated status/index; archive superseded logs | +| 7 | **Cross-run state contamination** | stale shared guest-RAM image cost 7 cycles `[log PART 93/Add.29]`; stale-PTE data-check residual (`STATE-CURRENT.md:195-205`); evidence dirs reused | runs are not hermetic; a phantom wall looks like a firmware bug | pre-run state hash + hermetic temp state; session records inputs | +| 8 | **Manual evidence assembly/comparison** | per-cycle multi-MB dirs; s10 shows the same contract in 4.92 s on the analog but is device-model-blocked; M-tables hand-filled | every claim re-narrated; comparisons by eye | one cycle → session → `compare` pipeline (rmharness pattern, extended read-only) | +| 9 | **Long-run guest stalls and timeouts** | agent/SSH timeouts from a trace reader flooding the ring; UVM spins; one no-watcher guest kernel crash (`STATE-CURRENT.md:577-582`) | half-finished runs and VM restarts | timeout budgets in the harness; no-watcher runs marked unsafe | +| 10 | **Planning churn** | owner directives U24→U31–U45→U46–U58 within 24 h (program ledger §13–§14), superseding §5 phases twice | plans and docs rewritten; in-flight work re-scoped | one live plan with an append-only directive ledger, not a rewritten one | + +**[I]** Rank 1 is the root cause; ranks 3, 4, 6, 8 are amplifiers. If only one +thing improves, it must be the number of supervised cycles per question — by +resolving hypotheses off-hardware (rank 1) or by extracting more per cycle +(rank 8). + +--- + +## §5 Design-assumption audit: stale or contradicted claims + +The tool's design and status text contains assumptions that its own newer work +has invalidated. Each row names the document section, the stale claim, the +contradicting evidence, and the correction. These are documentation defects, +not fidelity claims: the underlying work is honest; the text lagged it. + +| # | Stale claim (exact location) | Contradicting evidence | Correction | +|---|---|---|---| +| 1 | "**No TU104 device model**: no boot beyond the first unmapped access" — `docs/06` §8 gap 1; "does not boot the real GSP image to its message loop yet (**no TU104 device model**)" — agent brief, field-guide status text (2026-10-02); "Native M2+ device model — roadmap only" — `docs/00` status table line 56 | `sim/devices/`, `sim/profiles/`, `sim/gsp_boot.py` exist (untracked, 2026-10-03); B1: "**0 unknown MMIO, 0 unhandled in windows**" and B2 reaches the relocation copy; sessions `gsp-boot-b1/b2` (`docs/36` §3–§4) | say: "a partial GSP device model exists (B1/B2); the image does not yet reach the message loop; B3/B4 are the targets" | +| 2 | "L2 — **not modelled** (do not claim)" — `docs/06` §2; "It is not: a time, interrupt or device model (L2/L3 absent)" — `docs/26` §1; "L3 absent" — `docs/16` §3.1 table | `docs/32` models per-instruction cycles (labelled modeled, not calibrated); `docs/35` §1.2 states the standalone simulator "targets L0–L2 … with L2 explicitly labelled *modeled*"; `docs/36` models device windows, IRQ aggregation and PTIMER (partial L3) | the canonical accuracy doc should add a "modelled, uncalibrated" state between L2 absent and L2 calibrated, and all status text should use it | +| 3 | Documentation indexes stop at **docs 29**: README map lines 75–109, `docs/00` map, agent field-guide index/table | docs 30–38 exist and are untracked/in development (git status 2026-10-03): GPGPU-Sim integration, Turing/Ampere/Blackwell dossiers, cycle model, simulator design, device model, sim-QEMU line, GSP-Em API | add 30–38 to every map with an "in development" tag; the new docs are currently discoverable only by directory listing | +| 4 | "**24-tool** JSON-RPC surface" — agent brief, field-guide MCP rows; F1 "24 tools" — `future-eng-ops` F1 | `docs/20` §5 lists **31** tools and §9 says 55 tests (the doc itself is modified in the working tree) | pick one number from the code (`tools/list`) and generate it into all status text | +| 5 | Test-count claims: **194** (`docs/00` line 47; agent brief 194-test references; `eng-ops` register; project doc §6), **155** at audit (`docs/27` §0), **215** in the simulator ledger §13.3; 272 `def test_` statically countable 2026-10-03 | three different live claims plus a fourth countable value; no source states which suites were skipped in which environment | one generated badge from a recorded `make test` output; claims must name the environment (QEMU present?) | +| 6 | "Not implemented: device model (R1), custom simulator (R2), **MCP plugin (R3)** — all roadmap" — `open-iov/docs/05-operations/03` §5 line 97 | `docs/00` line 54: MCP "working (20)"; device model in development (`docs/36`); MCP 54 tests | project page: MCP implemented; device model in development; only custom simulator M2+ remains roadmap | +| 7 | Roadmap gates: "Items here are **not** v0.1. Do not start them ad hoc; each has a gate" and R1 "starts only when a portability task or an accuracy measurement requires it" — `docs/08` lines 5–8, 31–37; `docs/16` lines 2, 12–15 "nothing here exists yet" | owner-directed S1/B1/B2 device-model work and the Phase S program started 2026-10-03 (private program ledger §13–§14; `docs/35`, `docs/36`) | mark R1/R3/R4 started/superseded with the directive citation; keep the gate text for M2+ | +| 8 | R4 trace "**roadmap** … available once the QEMU plugin headers are vendored" — `docs/08` R4; MCP R3 framed as original design sketch even while "implemented" | audit: trace plugin verified live (8M events); native M1.5 trace implemented (`docs/16` §1.1a) | R4: implemented in both forms; session folding remains the only roadmap piece | +| 9 | `STATE-CURRENT.md` self-contradiction: line 160 "(2026-10-02 08:36, **in development**)" vs lines 841–844 "in-development notices were removed 2026-10-02 after … 194-test suite" | same file, same day, two statuses | the dated frontier bullets should be labelled historical and superseded inline | +| 10 | Value-demo numbers: "`run_all` aggregate (**~18.4 s total**)" — `docs/25` §3; "value demos 10/10 in **22.5 s**" — tool ledger wave-8 final; local `artifacts/results.json` records only **3 demos, 1.72 s** | the artifact is gitignored and currently a partial `--only s01,s04,s05` run; doc says "do not quote a stale number without re-running it" (`docs/25` §10) | regenerate before quoting; make `run_all.py` record the selection list and date into the doc via a generator | +| 11 | Private tool ledger header declares v0.1 acceptance frozen and "MCP is roadmap-only / explicitly not v0.1 … no custom simulator", while waves 2–8 and the simulator program superseded it | the ledger's own change log (waves 2–8) and `docs/35` §13 | treat the ledger as historical; point readers at README + docs 00/27 instead | +| 12 | Design assumption: "QEMU is just the first execution backend" and "the whole stack scriptable" — `docs/00`; "the only production backend is QemuTarget + RspClient" — `docs/16` §1 | M1 native backend is a peer with a differential test; device model only exists natively, not in QEMU | state that native is the only backend with device semantics today; QEMU remains the only one able to host the real image meaningfully? (Neither boots it; QEMU gives the first fault) | +| 13 | Design assumption in `docs/25` §2.1: projections become real "once H2.2 boots the image" | the program replaced H2.2-first sequencing with Phase S (standalone simulator) and Plan B; H2.2 is a legacy-handoff concept not mapped to the public docs 30–38 | map legacy H-numbers to docs 30–38 in one migration table | + +**[I]** The pattern is consistent: docs 00–29 describe the audited v0.1 +surface correctly, but the 2026-10-03 simulator wave (docs 30–38, untracked +`sim/devices`, `gpgpu-sim/`, new harnesses) has not been propagated into the +status claims. The cheapest durable fix is a generated status block plus a +claim scanner (§6, §8-U3). + +--- + +## §6 Documentation efficiency + +### 6.1 What is duplicated + +1. **Documentation maps (5 copies).** README §Documentation map (`README.md` + lines 75–109), `docs/00` §Document map, the agent field-guide 22-row + discoverability index, the project user guide §10, and the project master + log's "when to use it" table. All five enumerate the same files; only some + include docs 28/29, none include 30–38. **[E]** +2. **Status blocks (≈20 copies).** Nearly every doc opens with the same + "Status: implemented, documented and tested (audit docs/27 …; value demos + docs/25 …)" line. A status change requires ~20 edits; the test-count drift + (§5 #5) proves the mechanism fails. +3. **The "it is / it is not" framing** appears verbatim in `docs/26` §1, the + project user guide §1 and (paraphrased) the project page §2. +4. **Gating text** for RM 610 / no-hardware appears in README, docs 06/11/12/ + 14/15/19/20/21/22/23/24/25/28/29/35/36/38 and the project docs. Honest, but + repeated in full rather than linked. + +### 6.2 What is stale + +The 13 rows of §5. The highest-impact three: device model existence (#1), +L2/L3 fidelity wording (#2), and maps/status stopping at 29 (#3). + +### 6.3 What is unlinked + +| Item | Where it should be linked | Why | +|---|---|---| +| docs 30–38 | README map, `docs/00` map, agent field-guide index, project page §5 | currently invisible to a cold reader | +| `SIMULATOR-WORK-BOUNDARY.txt` | README layout + docs/35 §6 | the scope contract for the current wave | +| `gpgpu-sim/` overlay (`ADMISSION.md`, `BUILD-PORT.md`) | docs/30 and README | the only place the pinned methodology is recorded | +| `harness/qemu_line/`, `harness/re_demo*` artifacts | docs/37 (it references them) | evidence exists but is outside any rendered report someone will find | + +### 6.4 What is too verbose + +* **The main session log** (~15k lines, newest-first addenda) — excellent + evidence, poor orientation. It answers "what happened" but not "what is + true now". +* **The agent field guide** — a 22-row index plus a 12-row matrix plus F–K + examples, duplicating docs/00 and the user guide. +* **The private simulator handoff** — §4/§5 (the original plan) are superseded + by §13/§14 in the same file; readers must internalise two plans and a + supersession order. **[E]** +* **The historical tool-program ledger** — a wave-by-wave journal whose + header claims to be "the source of truth for what is being built"; now + history. **[E]** + +### 6.5 Concrete trim/re-link plan (cheap, ordered) + +| Step | Action | Files touched | Measurable effect | +|---|---|---|---| +| T1 | Generate `docs/STATUS.md` from the test runner output + `git describe` + harness JSON (`qemu_line`, `re_demo`, `results.json`); replace the ≈20 status blocks with one link and a date | tool docs (mechanical) | status edits: 20 → 1; no stale count survives a test run | +| T2 | Add a claim scanner to the link checker: flag numeric claims (`\d+-test`, `\d+-tool`, `L2 not modelled`, `roadmap only`) whose source value differs from the generated status | `scripts/check-docs-links.py` | contradictions detectable in CI | +| T3 | Add docs 30–38 (and the boundary file) to README, docs/00 map, agent field-guide index, project page; tag each "in development (Phase S)" | 4 files | cold-start discovery | +| T4 | Mark superseded sections in the private simulator handoff (§5 superseded by §13; §4 phases by §14) and the tool ledger (historical header); keep them as evidence | 2 private files | halves the reading needed to find the live plan | +| T5 | Collapse the four doc maps into README as the single map; other maps link to it | README, docs/00, user guide, agent brief | one map to maintain | +| T6 | Add a retention rule for `dumps/`: keep named/live-question sessions, prune `unittest-*` older than N days in CI or a script | `.gitignore` already excludes; add script | 107 MB today, >90 % runner noise; keeps real evidence findable | +| T7 | Replace hand-assembled M-tables with a generator over session pairs (the `compare` output + M1–M6 skeleton already exist) | harness | evidence pack becomes machine-produced | +| T8 | One "where to start" flow: README → user guide → STATUS; remove the duplicate tables | maps | orientation time down; this review's §5 defects become non-recurring | + +--- + +## §7 First-principles re-derivation: what actually minimises time + +For each dominant problem in §4, the minimum-time approach, including methods +not currently used. + +### 7.1 Problem: hypotheses are evaluated at 5–6 minutes per experiment + +**Current method:** each candidate fix/hypothesis is compiled into the module +and paid for with a supervised hardware cycle; failures can wedge the server +and cost a reboot. + +**First principles:** the marginal cost of an experiment should be the cost of +*evaluating the hypothesis*, not the cost of *rebooting the device*. +Two independent moves: + +1. **Move evaluation off-hardware against real firmware.** The only currently + viable route is the S1 device model + rehosted execution (docs/35 §3.3). + Until B4 exists, the tool cannot answer a live firmware question at all — + every session in `dumps/` is either self-verification or a first-fault + probe. +2. **Until then, batch per cycle.** A single boot can be instrumented to + answer many questions (the module already exposes debugfs state, counters + and ladders). The recorded campaigns did one hypothesis per cycle even when + the per-cycle data would have covered several (`docs/25` §5 s01: ten boots + for ten candidate fixes). A "one boot, N recorded observations" harness is + cheaper than N boots and needs no new fidelity. + +**Not currently used:** *surgical rehost* — compiling and running only the +failing function region with recorded inputs, instead of booting the whole +image. The initial failure is localized (a commit helper, a parser), and +rehosting a function does not require the full device model — only its +observed input/output state. This is the shortest path from "730 lines of +static RE" to "watched it decide". + +### 7.2 Problem: the server wedge makes failures catastrophic + +**Current method:** fail-safe holds the device; recovery is a fuse-limited +teardown or an approved host reboot. + +**First principles:** a failure should stop the experiment, not the host. The +tool-side analogue is *deterministic replay from a snapshot* (moments, F7): +experiment on a state image, never on the only copy. That is gated on M2/M3 +today, but a weaker version is available now: record the full stimulus and +state hash so a wedge cannot lose the evidence and the next cycle can +reproduce the exact pre-failure state by construction (the session already +carries snapshots; the missing piece is the capture path for the physical +cycle). + +### 7.3 Problem: opacity (first error invisible) + +**Current method:** map the journal ring, read raw host memory, static review. + +**First principles:** the first error is a data question. Watchpoints on the +reply/state object plus before/after diff answer it directly — the same shape +as value demos s01/s05, which are measured in ~1 s on the analog. The tool +has these primitives; what it lacks is a running firmware to attach them to. +Again: the device model is the unblocker, and *surgical rehost* can provide it +locally for one failure path. + +### 7.4 Problem: documentation and status re-derivation + +**First principles:** state should be produced by the system that changes it, +not retyped by hand. Test results, session manifests, harness JSON and git +describe already exist; a generated status page costs one small script and +removes an entire class of stale claims (§5). The session format is already +the evidence contract; the status page should consume it. + +### 7.5 Problem: phantom walls from state contamination + +**First principles:** make runs hermetic and make inputs visible. A pre-run +state manifest (image hashes, shared-RAM file identity/hash, mdev/module +versions, boot id) recorded in the session would have made the stale +shared-memory image immediately obvious instead of seven cycles later +`[log PART 93/Add.29]`. This is cheap, local and applies to both the physical +and simulator paths. + +### 7.6 What not to re-derive + +Do not rebuild what already works: QEMU execution, the session format, hooks/ +watchpoints, the Ghidra join, the injection SDK. The review found no +evidence that reimplementing these would pay; the evidence points entirely at +evaluation cost and evidence plumbing. + +--- + +## §8 Ranked pragmatic upgrade list + +Ranking rule: expected reduction of §4 rank-1/2/8 costs per engineering week, +with the eng-ops admission fields (`open-iov/docs/eng-ops.md` §2). Thresholds +are stated for the record and must be frozen before the first measurement run. + +### U1 (top) — Land the S1 device model to B4 (message loop) on the real image + +* **Scenario (dynamic-RE loop):** "what does the real 570.144 GSP firmware do + between entry and its message loop, deterministically, without hardware?" +* **Measurable outcome:** boot-ladder rung reached, session hash, device state + hash, unknown-MMIO count, first host message; `sim_qemu_line` verdict field. +* **Baseline (recorded):** QEMU `probe` first fault at fw `0x4009014` + (`docs/06` §3) plus static reading (floor estimate 600 s in `re_demo.py`); + every real-firmware value demo marked `blocked_on_device_model`. +* **Threshold:** reach B3 (first CSB/TRANSCOM poll with read-back) and B4 + (message loop) twice with byte-identical key fields; one scripted host + message served end-to-end with a recorded reply (B5); `device_model_used = + true` and `above_qemu_line = true` in `harness/sim_qemu_line.py`; N=3 runs, + one host. +* **Failure signal:** two consecutive image/profile versions require + unabsorbed code changes (profile seam failed), or replay key fields differ + after three attempts (scheduler/determinism failed); or lenient MMIO is + still load-bearing in a rung claimed as modelled. +* **Why first:** every measured value projection in `docs/25` and every + blocked question in §4 is gated on this one capability. + +### U2 — Prove the tool on the live wall: surgical rehost of the GR +engine-context promote/commit path (and the UVM large-push parser regression) + +* **Scenario (dynamic-RE):** the project's current product wall is a + server-internal commit failure; answer its open hypotheses (the V6 + soft-`INVALID_STATE` site; the V5 commit assert's *first* failure) from + rehosted execution with hooks/watchpoints, not from another 730-line static + read or another reboot. +* **Measurable outcome:** questions resolved per session; supervised cycles + and reboot requests avoided; recorded session pair per hypothesis. +* **Baseline (recorded):** modes 4–22 across 2026-10-02 (a full day), three + wedge-reboot cycles, one accepted root cause after 730+ lines of static RE + `[log PART 92/Adds.13–25; STATE-CURRENT.md:98-131]`. +* **Threshold:** ≥2 open hypotheses answered with A/B sessions and **zero** + hardware cycles; the answer agrees with at least one live/session artefact; + total effort ≤2 engineering days on the rehost path. N=1 path, 2 sessions. +* **Failure signal:** after two bounded attempts the failing code cannot be + driven (required device semantics absent) or rehosted outcomes disagree with + live evidence; then stop, record the gap, and fall back to static+physical. +* **Why:** it converts the largest single-day cost on record into a + device-free artifact and produces the first non-probe real-firmware sessions, + which is exactly the missing usage in §2.3. + +### U3 — One-command physical-cycle evidence capture → session → `compare` + +* **Scenario (correctness/evidence loop):** every supervised cycle leaves a + machine-readable session and an automatic delta against the previous cycle. +* **Measurable outcome:** human minutes per cycle evidence pack; M1–M6 + skeleton rows auto-filled; unexpected-delta list. +* **Baseline (recorded):** manual multi-MB per-cycle directories, rings + decoded by hand, M-tables filled in prose; s10 takes 4.92 s on the analog + (`docs/25` §6). +* **Threshold:** one cycle's read-only captures (module state, counters, + ring/journal excerpts, pstore status) become a session + `compare` report in + ≤2 wall-clock minutes, N=3 cycles; a field change between cycles is + attributed or listed as unexplained. +* **Failure signal:** the capture misses the decisive field twice (tool does + not see what the failure needs) or deltas are non-reproducible; retire to a + checklist and keep sessions for the simulator only. +* **Why:** it attacks rank-1/rank-8 cost directly without waiting for the + device model, is read-only by construction, and reuses `rmharness`'s proven + recording pattern (`docs/17`). + +### Runners-up (below the top 3, kept for the next review) + +* Get one agent to actually use the MCP server end-to-end on a release-triage + rehearsal (F1 threshold) or retire the claim of agent-driven monitoring. +* Extend value demos to the rehosted path (F6: ≥6/10 on rehosted code or + stop calling them value validation). +* Session retention/prune + a session index (`dumps` is >90 % test noise). +* Fold `port_driver_versions` into `ci_release` rather than maintaining a + third orchestrator (`future-eng-ops` F2 adjudication). +* Calibrate cycles (C1–C6) only when a supervised capture window is approved; + until then keep the modeled label and claim nothing. + +### Do-not-build list (with the trigger that would reopen each) + +| Do not build | Why | Reopen trigger | +|---|---|---| +| GPGPU-Sim board model (GMMU/FBIF/DMA/vidmem/VBIOS), Phase G | Phase S is the owner-locked scope; designed-only | Phase S pause + a funded board question | +| Ampere/Blackwell architecture models | parameter dossiers exist; no consumer | a device port actually scheduled | +| Verilog/FPGA soft processor (F12/F13) | multi-quarter, no funded timing claim | a funded cycle-accuracy claim | +| SIMD/kernel execution on device (F11) | unbounded research; hardware executes kernels today | funded thesis/cycle-accuracy use | +| Moments/time-travel before B4 | deferred by the pragmatic scope lock; convenience, not support latency | message loop reached + a real replay need | +| TCG-trace session folding (R4 remainder) | trace already exists; folding is convenience | trace consumers ask for coverage reports | +| Ghidra heat-maps/bidirectional nav (B2–B4) | bridges work; polish | a real navigation complaint | +| MCP port-chain automation (F1) until a release rehearsal runs | no consumer has exercised the chain | first agent-driven release run | +| Packaging/trialing (F4) as a workflow | no reviewer demand yet | a reviewer asks for a replayable bundle | +| A second multi-version orchestrator | duplicates `ci_release` | F2 threshold unmet **without** a bundle precondition | +| RM 610 anything | hard human gate (`docs/23`) | explicit named-human approval | +| SBOM/observation inventory (F15) | legal/review gates; no consumer | SBOM review asks for per-image rows | +| New MCP transports / network exposure | loopback/stdio is the safe surface | never (security rule) | + +--- + +## §9 Frida-like capability gap analysis + +### 9.1 What exists + +**[E]** hooks (enter/leave on RV64 ABI, `Session.hook`), breakpoints (sw/hw), +watchpoints (write/read/access with clear/step/re-arm to match QEMU stop +semantics), memory read/write/hexdump/search/snapshot/diff, schema struct +decode, recorded sessions, `run_until` (run to cursor), `call` (remote +function call with scratch stack and ABI save/restore), injection of +position-independent services (direct and firmware-mailbox paths), exec/access +trace (native and TCG), plugins, MCP, and Ghidra in/out bridges. Source: +`docs/00` §Status, `docs/01` §4, `docs/11`–`13`, `docs/27`. + +### 9.2 What is missing, and whether it matters day-to-day + +| Frida-class capability | In the tool? | Day-to-day matter | Verdict | +|---|---|---|---| +| Backtrace / stack unwind on a hit | no (only `ra`-based leave hooks) | high: "who called this handler" is a daily RE question | **build later** — an `ra`/frame-pointer walk is bounded; pairs with `run_until` | +| Semantic call tracing (Stalker-like call graph) | partial: full exec trace, no call graph/summary | medium-high: call ordering at scale | **extend trace** with a call/extent fold when B4 exists | +| Catchpoints (trap/exception class selection) | partial: traps stop as plain stops; catchpoints planned (S2) | high for error-path RE (fault handling, asserts) | **build now (thin)** — approved in the pragmatic scope table | +| Hart/thread selection | no (single hart) | low today; needed for multi-core libos | defer with H2.4 | +| Interceptor *replace* (patch a function body, not just insert) | partial: SDK services + debugger writes; no atomic replace | medium: fault injection/return-value forcing | keep injection SDK; add a documented return-patch helper only if a scenario needs it | +| Module/symbol enumeration at runtime | n/a (stripped bare-metal; symbol DB from static exports) | n/a | not applicable | +| DWARF type introspection | no (manual schemas) | medium: manual schema per structure is a cost | acceptable; schemas are versioned data by design | +| Memory allocation tracking | no | low for firmware servers | do not build | +| Remote/network attach, ASLR/module-load handling, Java/ObjC bridges, debugger-side allocation hooks | no (deliberately) | none | not applicable; keep out | +| Allocation/protection changes (mprotect-like) | no; MPU is not enforced (`docs/36` §6.8) | medium for fault experiments | defer; documented gap | + +### 9.3 The real gap + +**[I]** The Frida-like feature list is not the constraint. The constraint is +that none of it can yet run against the real firmware to the point where a +live question exists (B4). Every missing feature above is worth less than the +device model. The Frida-like work that *is* worth doing before B4 is small and +already approved: crash/trap catchpoints, hart/core metadata, and memory +region metadata (S2 "thin"); a backtrace would be the next candidate because +it converts the most common live question into one call. + +**[I]** Conversely, several Frida-ish ideas have no firmware use case: +network attach, module enumeration, allocation tracking, OS-level process +injection, Java/ObjC bridges. They should stay out of the roadmap's active +list to keep the feature-count pressure off the critical path. + +--- + +## §10 What to scrap or disable + +Nothing is deleted by this review; these are recorded recommendations for the +next audit (`docs/27`) and the `future-eng-ops` §4 prune/demote review. + +| Item | Action | Evidence / rule | +|---|---|---| +| `Session.symbol_at` (no caller) | **prune at next audit** unless a caller appears | already the first prune candidate (`future-eng-ops` §4) | +| "Value validation" framing of s01–s10 | **demote** to "tooling regression suite" until ≥6/10 class transfer on rehosted code | F6 kill criterion: fewer than four classes transfer | +| `--surface rm` control discovery | keep code, **remove from advertised next steps** until H1.3/H2.2 exposes the table | F5: `built, not used yet`; exits clean | +| `harness/port_driver_versions.py` as a third orchestrator | **fold into `ci_release.py`** unless the F2 threshold is met without a bundle precondition | F2 adjudication note | +| `harness/port_new_device.py` / `package_sim.py` without a consumer | keep as pilots, **do not advertise as admitted**; expire per eng-ops pilot rule | F3/F4 unadmitted | +| Trace session folding / heat-maps / bidirectional nav | remove from "next" lists; keep issues recorded | pragmatic scope lock (§14.2 of the private program ledger) | +| MCP tools that are QEMU-only on the native backend | keep the structured `not_available`, but stop counting them toward "native parity" | `docs/20` §8; parity matrix in `docs/35` §5 | +| `dumps/` growth | add retention/prune for runner sessions; never track | 458 dirs / 107 MB, >90 % `unittest-*` (measured 2026-10-03) | +| GPGPU-Sim overlay further work | freeze until Phase S pause | owner scope lock; overlay build only | +| Historical tool ledgers | mark historical; stop treating as live plan | §5 #11; §6.4 | +| Test-count claims | replace with generated value; delete hand-written numbers | §5 #5 | + +**Disable-now candidates (if a hard choice is needed):** stop advertising +`--surface rm`, port/packaging pilots and trace folding as available +workflows, and stop quoting hand-maintained test/tool counts. Each currently +consumes review attention without a consumer. + +--- + +## §11 Review conclusion (one paragraph) + +**[I]** The tooling is real, honest about fidelity, and already proven on its +own terms, but the evidence shows it has not yet paid for itself on the +project's dominant cost: the supervised hardware cycle. The single +highest-value move is to finish the device model to the message loop so the +hundreds of blocked questions become sessions; the second is to prove the tool +on the current product wall by rehosting the failing path; the third is to +capture every physical cycle as a session so evidence stops being hand-built. +Everything else — more features, more docs, more architectures — should wait +until those three produce recorded, measurable time savings. + +--- + +*Change note: new document; scope: review only; benefit: consolidates the +2026-10-03 tool/simulator/wall evidence into actionable, gated upgrades; +no product, hardware or guest action authorised.* diff --git a/debug/risc-v-gsp-rm-vm/docs/40-NEW-VERSION-TRIAGE.md b/debug/risc-v-gsp-rm-vm/docs/40-NEW-VERSION-TRIAGE.md new file mode 100644 index 0000000..613e966 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/40-NEW-VERSION-TRIAGE.md @@ -0,0 +1,296 @@ +# 40 — New-version triage (one command) + +> **Status: in development (2026-10-03).** Implemented, tested and measured on +> the TU104 570.144 image; the measured value proof is section 5. Interfaces +> and report fields may still change. Simulator-only: never touches hardware, +> GPU, mdev, a kernel module or a guest; nothing is downloaded. +> +> **Audience:** agents and maintainers of risc-v-gsp-rm-debugger. +> +> This document is public-safe: it describes components, behaviour and +> measured numbers only. + +--- + +## 1. Why this exists (U46) + +The program has one goal: cut the time from a new NVIDIA RM driver / GSP +firmware release to supported in `vgsp`. Today a new image is triaged by hand +by reading docs/07 P1 and invoking four different tools in sequence (probe, +native boot, A/B, version diff), then collecting session paths and writing a +summary. + +`harness/triage.py` turns that into **one bounded command** that reuses the +existing tools - it does not reimplement any of them: + +| Step | Reuses | Answers | +|---|---|---| +| `identity` | `greeneye.elf`, `sha256` | what is the image, which version, which baseline | +| `probe` | `harness/sim_qemu_line.py:run_qemu_baseline` (the v0.1 probe path) | where does it first fault / first touch under QEMU | +| `native_boot` | `python3 -m sim.gsp_boot` (S1 device model + boot ladder) | how far does the native ladder get (B0–B4) | +| `ab_eval` | `harness/sim_qemu_line.py:run_harness` | QEMU vs native A/B, determinism, frozen thresholds | +| `version_diff` | `harness/version_diff.py` (only with `--baseline`) | image/symbol/corpus/probe/behavior delta, RM 610 gate respected | +| `features` | none (introspection) | hooks/watch/memory/session API, native sim, TCG plugin, MCP, demo | +| `support_readiness` | derived | blockers and ordered next actions for `vgsp` support work | + +Every step is wrapped: a failing step is recorded as `status: error`, the +remaining steps still run, and the report is still written. Every session a +step used is listed with its sha256 hashes. + +--- + +## 2. Usage + +Run from the tool root (`open-iov/debug/risc-v-gsp-rm-vm`): + +```bash +# Plan only (no targets opened, no --out created) +python3 harness/triage.py \ + --firmware ../../firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \ + --version 570.144 --out dumps/triage-570.144 --dry-run + +# Single new image: identity + probe + bootstrap ladder + A/B + features +python3 harness/triage.py \ + --firmware <new.fwimage.bin> --version <label> --out dumps/triage-<label> + +# With a baseline: adds the full version diff (docs/21) +python3 harness/triage.py \ + --firmware <new.fwimage.bin> --version <new-label> \ + --baseline <old.fwimage.bin> --baseline-label <old-label> \ + --out dumps/triage-<old>-<new> + +# One command via the launcher or make +./risc-v-gsp-rm-debugger.sh triage --firmware <new> --out dumps/triage-new +make triage FW=<new> OUT=dumps/triage-new TRIAGE_ARGS="--baseline <old>" +``` + +Flags: `--firmware` (required), `--baseline`, `--version`, `--baseline-label`, +`--out` (required unless `--dry-run`), `--budget` (native instructions per +attempt, default 200000, hard cap 2000000), `--qemu-duration` (default 0.5s), +`--step-timeout` (per-step wall-clock net, default 300s), `--release`, +`--approval-token`, `--dry-run`. + +Safety: `--dry-run` opens no target and creates no `--out`. The RM 610 gate +runs first in every mode (docs/23) and refuses with exit `3`; labels checked +are `--release`, `--version`, `--baseline-label` and both file basenames. +Exit codes: `0` report written (skipped steps included), `1` at least one step +errored, `2` tool/input error (or firmware absent), `3` RM 610 refusal. + +Output tree under `--out`: + +```text +<out>/ + triage-report.json stable machine-readable report + triage-report.md rendered report + sessions/qemu-probe/ QEMU probe session + sessions/native-boot/ S1 boot-ladder session + ab-eval/ docs/37 A/B report + sim-qemu-line.json|.md + sessions/ + version-diff/ only with --baseline (docs/21 tree) +``` + +--- + +## 3. Report schema (`greeneye-new-version-triage/1`) + +```json +{ + "format": "greeneye-new-version-triage/1", + "tool": "harness/triage.py", + "generated_at": "2026-10-03T04:44:24Z", + "rm610_gate": {"status": "passed", "labels": ["570.144"]}, + "input": { + "firmware": {"path": "...", "sha256": "...", "size": 28528288, + "version": "570.144W", "version_source": "gsp_version.txt sidecar"}, + "baseline": null, "baseline_supplied": false, + "budget": 200000, "qemu_duration": 0.5, "triage_command": "..." + }, + "thresholds": {"frozen_on": "2026-10-03", "manual_baseline_s": 1800.0, + "triage_wall_s_max": 600.0}, + "steps": [ + {"name": "identity", "status": "ok", "wall_s": 0.099, "firmware": {...}, "elf": {...}}, + {"name": "probe", "status": "ok", "rung": "pre-B0", "first_fault": {...}, + "stopped_pc": 0, "sessions": [...]}, + {"name": "native_boot", "status": "ok", "rung": "B2", + "instructions_executed": 14682, "stop": {...}}, + {"name": "ab_eval", "status": "ok", "native_rung": "B2", + "native_identical": true, "above_qemu_line": true, "verdict": "above"}, + {"name": "version_diff", "status": "skipped", "reason": "no --baseline supplied"}, + {"name": "features", "status": "ok", "checks": {...}, "unavailable": []} + ], + "sessions": [{"step": "probe", "name": "qemu-probe", "dir": "sessions/qemu-probe", + "present": true, "hashes": {"session.json": "...", "events.jsonl": "...", "summary.md": "..."}}], + "support_readiness": {"verdict": "sim-observable", "baseline_compared": false, + "observable_rung": "B2", "blockers": [], "next_actions": ["..."]}, + "threshold_checks": {"triage_wall_s": 2.781, "triage_wall_s_max": 600.0, + "wall_within_max": true, "manual_baseline_s": 1800.0, + "speedup_x": 647.2, "beats_manual_baseline": true, + "required_steps_missing": []}, + "wall_s": 2.781, + "artifacts": {"report_json": "<out>/triage-report.json", "report_md": "<out>/triage-report.md"} +} +``` + +The `wall_s`/`threshold_checks` values above are the **measured 2026-10-03 +sample** and are host-load dependent, not fixed constants. + +Key fields: + +* `steps[].status` is `ok | skipped | error`; `skipped` carries a reason and + never fails the run. +* `support_readiness.verdict` is `blocked` (identity/step errors), + `first-touch-only` (no runtime observation yet) or `sim-observable` (native + ladder reached `B2`+, the point where the firmware runs past boot and can be + instrumented). +* `support_readiness.next_actions` is the ordered work list for `vgsp` support + (device-model gaps, symbol DBs, A/B above-the-line work, missing inputs). +* `sessions[].hashes` is the docs/07 evidence-checklist hash list; attach it + with the recorded sessions. + +--- + +## 4. Worked example — 570.144 (measured 2026-10-03) + +Commands (real, read-only; `/usr/bin/time` around the wrapper): + +```bash +./risc-v-gsp-rm-debugger.sh triage \ + --firmware ../../firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \ + --version 570.144 --out dumps/triage-570.144 +``` + +Measured output (trimmed): + +```text +[triage] identity: status=ok +[triage] probe: status=ok reason=ok +[triage] native_boot: status=ok +[triage] ab_eval: status=ok +[triage] version_diff: status=skipped reason=no --baseline supplied +[triage] features: status=ok +[triage] readiness: sim-observable (baseline_compared=False) +[triage] next: native ladder is at B2 (runtime-observable): inspect the milestone/stop with hooks ... +[triage] next: native A/B is not yet above the QEMU line (verdict=above_lenient_only): device model ... +[triage] next: re-run with --baseline <previous.fwimage> for the symbol/behavior delta ... +[triage] wall: 2.781s vs manual baseline 1800.0s (speedup 647.2x) +``` + +The `2.781s`/`647.2x` line is the **measured 2026-10-03 sample** (host-load +dependent, not a fixed constant). + +What the report recorded: + +| Step | Measured | Meaning | +|---|---|---| +| identity | `570.144W` via `gsp_version.txt`, sha256 + 28,528,288 bytes | image paired and hashed before any run | +| probe | QEMU first fault `pc_fw 0x4009014`, load access at `0x2000000000000A00` | matches the docs/37 QEMU reference first touch | +| native_boot | rung **B2**, 14,682 instructions, stop fault `pc_fw 0x400EA0C`, 0 unknown MMIO | S1 ladder is two rungs past the QEMU fault; boot shim consumed, relocation milestone hit | +| ab_eval | native `B2` (14,682 instructions, 0 unknown MMIO), identical across the repeat, `above_qemu_line: true`, verdict `above` | post-S1 re-run (2026-10-03, later): the discovered `sim/profiles` model + shim now satisfy the docs/37 line contract; the pre-S1 run was `B0`/`above_lenient_only` | +| features | greeneye API, native backend, boot ladder, A/B, version_diff, TCG plugin, MCP, demo, QEMU all present | capability surface for the follow-up work | + +Post-S1 re-run (2026-10-03, later; same command, after S1 and the harness +wiring landed): `ab_eval` reports native rung **B2**, all five frozen +thresholds true and `above_qemu_line: true`; the "native A/B is not yet above +the QEMU line" next-action line no longer appears. Wall ≈2.7 s +(host-load dependent). The pre-S1 trace above is kept as the recorded +before/after evidence. + +Baseline variant (same image both sides, an unchanged-version rehearsal with +the docs/21 path): + +```bash +./risc-v-gsp-rm-debugger.sh triage \ + --firmware ../../firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \ + --baseline ../../firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \ + --version 570.144 --baseline-label 570.144 --out dumps/triage-570.144-baseline +``` + +Measured (2026-10-03, host-load dependent): **11.388s** internal wall +(11.64s including interpreter startup); +`version_diff` = 8.526s, sections `image/probe/behavior: ok`, +`symbols/corpus: skipped` with reasons; image self-diff `same: true`; 7 session +directories hashed (probe, native boot, 2 A/B native, A/B QEMU, version-diff +probe + behavior). Readiness stayed `sim-observable`, `baseline_compared: +true`, with next actions for the missing symbol/corpus inputs. + +Frozen thresholds (recorded in `harness/triage.py` on 2026-10-03 **before** +the runs, per handoff §14.5): manual baseline `1800s`, triage wall max +`600s`. Result: `wall_within_max: true`, `beats_manual_baseline: true`, +speedup `647x` (single image) / `158x` (with baseline) — both wall times are +**measured 2026-10-03 samples (host-load dependent)**, not fixed constants. The +manual baseline is +a conservative understatement of the steps replaced (read docs/07 P1, choose +budgets, run probe + boot ladder + A/B, run version diff, collect session +hashes and write the summary for one image). + +--- + +## 5. Value proof and its limits + +* The frozen contract is in the report itself (`thresholds`, + `threshold_checks`); the same numbers are restated in the module docstring + and here. Never move them silently. +* The measured numbers above are from this checkout on 2026-10-03; reproduce + with the exact commands in section 4. +* **Conservative baseline:** 1800s is an *estimate of manual effort*, not a + timed human study. The speedup numbers are reported against it, and the + report flags them as such. +* **What the run proves:** one bounded command produces the identity, probe, + boot-ladder, A/B and capability evidence with hashed sessions in seconds - + the mechanical part of docs/07 P1. +* **What it does not prove:** that the remaining human work (interpreting the + deltas, extending the device model, the accuracy loop, the `vgsp` change) is + automated; it is not, and the next-actions list is where it stays explicit. + +--- + +## 6. Release-to-support mapping + +| Triage output | Next workflow (docs) | +|---|---| +| identity + hash + version | docs/07 P1 step 1, docs/22 pairing checks, docs/28 sweep input | +| probe first fault / first touch | docs/07 P1 step 2, docs/24 device map harvest | +| native boot rung + stop | docs/35 §3.3 ladder; the `vgsp` support scope starts at `B2`+ | +| A/B verdict | docs/37 line contract (U50) | +| version_diff sections + symbol/corpus skips | docs/21, docs/14 §6 evidence pack | +| readiness next actions | docs/07 P1.7 acceptance, the actual `vgsp` support work items | +| RM 610 gate | docs/23 (never bypassed) | + +--- + +## 7. Not verified (explicit) + +* **No real 570.211+ new-version pair exists locally**; the cross-version + delta path is exercised only by the self-diff rehearsal in section 4. +* **Symbols and corpus are skipped for raw `.fwimage` targets** (no `.symtab`, + no Ghidra exports supplied): the handler-inventory delta is not measured + until those inputs are supplied. +* **The S4 A/B verdict is `above` on the frozen checks after S1 landed** + (B2, documented device model + shim, deterministic, within the wall + bound); B3/B4 are not reached and triage makes no "beats QEMU" claim beyond + the documented rung contract (`above_lenient_only` was the pre-S1 result). +* **Fidelity:** L0/L1 execution facts only; no L2 timing and no full L3 + device-environment claims (docs/06, docs/36). Cycles are modeled; the boot + shim values are declared assumptions. +* **No physical backend, `/dev`, module, mdev, guest or network access** was + used or is performed by the tool. + +--- + +## 8. Tests + +```bash +python3 -m unittest tests.test_triage -v # 14 tests +make test # full suite +``` + +`tests/test_triage.py` covers the version-label extraction chain, the report +shape and artifact writes, session hashing, partial-failure handling +(injected step failure still yields a report), missing-firmware behaviour +(reported, not raised), budget bounds, RM 610 refusal (exit `3`), dry-run +side-effect freedom, and a tiny-budget integration run against the real +570.144 image that skips cleanly when the image or QEMU is absent. + +<!-- change (2026-10-03): new document recording the one-command new-version +triage workflow (U46), its measured 570.144 value proof and its limits; +scope: docs/40 only; benefit: the release-to-support loop has one documented +entry point and one attachable evidence bundle. --> diff --git a/debug/risc-v-gsp-rm-vm/docs/41-STABILITY-AND-TIME-TO-VALUE.md b/debug/risc-v-gsp-rm-vm/docs/41-STABILITY-AND-TIME-TO-VALUE.md new file mode 100644 index 0000000..77e1298 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/41-STABILITY-AND-TIME-TO-VALUE.md @@ -0,0 +1,122 @@ +# 41 — Phased practical improvements to risc-v-gsp-rm-debugger: stability and time-to-value report + +> **Date:** 2026-10-03. **Scope:** the simulator/debugger workstream inside +> `debug/risc-v-gsp-rm-vm/` only. **Verdict:** the minimum bar for marking the +> *tool and standalone GSP simulator* as **stable (conditional)** is passed: +> 298 tests green twice, deterministic real-firmware runs, an independently +> reproduced A/B verdict "above the QEMU line", and reproducible value +> measurements. Conditions are listed in §7 — chiefly that the device model +> currently reaches boot rung B2 (not the message loop) and cycle numbers are +> modelled, not silicon-calibrated. +> +> Local notes: `../../../../handoff-simulator.md` (directive ledger, top work), +> `../LOCAL-REVIEW-NOTES.md` (local provenance). Public text describes +> components and behaviour only. + +## 1. What we were doing before (the QEMU baseline) + +* The only execution engine was `qemu-system-riscv64` via the gdbstub. On the + real `gsp-570.144.fwimage.bin` it loaded flat, executed a handful of + instructions, faulted at firmware vaddr `0x4009014` reading + `0x2000000000000A00`, then looped on `mtvec=0`. +* Questions about GSP boot progress could only be answered by static reading + of decompiled code; every dynamic question stalled at the first fault. +* Tool runs produced sessions, but with no device semantics, no cycle context, + and no way to prove "the emulator knows more than the probe". +* New-firmware triage was a manual sequence of probe + version diff + reading. + +## 2. What we are doing now (the upgrade, phased) + +| Phase | Improvement | Status | +|---|---|---| +| P1 | **GSP device model** (`sim/devices/`, `sim/profiles/`) with PMC/FALCON/PRISCV/timer/CSB/FWSEC/PMU/DMA/IRQ windows, versioned profile, boot shims | B2: 14,682 instr, fault `0x400EA0C`, 0 unknown MMIO, two-run identical | +| P2 | **`sim.gsp_boot` boot-ladder runner** with rung classification and serialisable device state | B0–B2 (B3/B4 targets) | +| P3 | **One-command release triage** (`./risc-v-gsp-rm-debugger.sh triage`) orchestrating identity/QEMU probe/native boot/A-B eval/version diff/readiness | 570.144 in ~2.7 s; baseline pair ~11.4 s | +| P4 | **QEMU-line A/B evaluation harness** (`harness/sim_qemu_line.py`, frozen checks) | verdict `above`, all threshold checks computed from live runs | +| P5 | **Real RE demonstration** (`harness/re_demo.py`) answering a concrete bootstrap question with simulator tools | PASS 5/5, 0.13 s vs 601 s baseline floor | +| P6 | **MCP native-backend tools** (launch, cycles, device state, profile, regions, budgets) | 55 MCP tests green; read-only default preserved | +| P7 | **Documentation and workflow review** (`docs/39`), docs 30–41, README/Makefile integration, link checks | 143 tool files link-checked, 0 broken | + +## 3. Proof of value (before → after) + +| Scenario | Before | Now | Evidence | +|---|---|---|---| +| What happens in GSP boot before the first wall? | QEMU: fault at `0x4009014`, stop | Device model serves the path; deterministic B2 fault `0x400EA0C`; unknown accesses 0 | `python3 -m sim.gsp_boot`; sessions `dumps/gsp-boot-b2-run1/2` | +| Triage a new firmware image | manual probe + reading, ~1800 s estimated | one command, **2.68 s**, JSON+MD report with hashes | `./risc-v-gsp-rm-debugger.sh triage --firmware … --version 570.144` | +| Is the emulator better than QEMU? | no mechanism | validators run both, compute rung/feature deltas; verdict `above` | `python3 harness/sim_qemu_line.py` | +| Answer a bootstrap RE question | static reading only | 5/5 recorded answers in 0.13 s | `python3 harness/re_demo.py` | +| Scripted/agent operation against the simulator | QEMU-only MCP | native backend tools tested | `tests/test_mcp.py` | + +Baselines are explicitly labelled estimates where they are not measured human +studies (frozen thresholds: 600 s RE-time floor, 1800 s triage baseline); all +wall times are labelled host-load dependent. Every number above has a +one-line command in the report/doc that produced it. + +## 4. Stability evidence + +* `python3 -m unittest discover -s tests -t .` → **298 tests, OK**, twice + (rounded identity across runs); MCP 55, triage 14, device model 34 new + checks included. +* Determinism: B1/B2 device-state SHA identical across runs; triage sessions + hashed; A/B harness repeats native runs and compares. +* Back-compat: `Session`, `RspClient`, `SimClient` and CLI keep prior + behaviour; additions are keyword/default forms; QEMU path untouched + (toggle `--backend qemu|native`). +* Boundary compliance: no writes outside the tool dir; no hardware, module, + guest, mdev, reboot or RM 610 action; MCP read/observe default unchanged. +* Fidelity honesty: cycles are labelled *modeled*; L2 is not claimed + silicon-accurate; L3 device semantics are declared assumptions in-session; + "above QEMU" is computed against the frozen rung contract, not asserted. + +## 5. Adversarial review record + +* **Round 1** (review + fix): 3 P1s found and fixed — device model not wired + into the A/B harness/triage, dead PTIMER window offsets, MCP device-state + returning a non-JSON object. P2 backlog listed. +* **Round 2** (independent, harsher): falsification runs confirmed all + headline numbers; 2 P1s found and fixed (docs/35 B1 overclaim; personal + host path in `gpgpu-sim/setup.sh`), 8 P2s fixed (timer opt-in, FWSEC state + derivation, DMA refusal counting, PMC chip-ID duplication, overlapping + window fallback, stale doc counts, `session_launch` documentation, docs/40 + measurement labels). One latent item recorded: WPR2 window offsets versus + profile base (firmware never touches it in B0–B2). +* Verdict after fixes: conditional stability as stated in §7; no unverified + "beats silicon" or "boots RM" statement anywhere. + +## 6. Top work for the next session (time-to-value first) + +1. **Push the device model to B3/B4** (CSB/TRANSCOM mailbox semantics, + DMEM runspace source, PMU/FWSEC stubs) — this is the gate to serving a + real RM message and the largest remaining time-to-value lever. +2. **Surgical rehost of the live GR engine-context promote/commit wall** + (docs/39 top-3): convert the manual, multi-cycle physical campaign into + device-free hook/watch/compare sessions. +3. **Physical-cycle capture → session → compare** helper (read-only), to cut + the dominant recorded cost of manual evidence assembly. +4. Everything else remains in the private handoff: GPGPU-Sim board model, + VF bridge (`GREENEYE_GSP_EM_BRIDGE`, default OFF), VBIOS, fabric, + Ampere/Blackwell, and the "not useful yet" list. + +## 7. Conditions on the stability claim + +1. Device model ceiling is B2; B3/B4 pending profile semantics. +2. Cycle counts are modelled defaults, not calibrated bands. +3. Manual-baseline speedups use labelled estimates, not timed human studies. +4. Cross-version triage was exercised as a self-diff rehearsal (no second + local firmware version); RM 610 remains human-gated. +5. The stability claim covers the tool and its standalone GSP simulator, not + any GPGPU-Sim integration or silicon equivalence. + +## 8. Reproduce everything + +```bash +cd open-iov/debug/risc-v-gsp-rm-vm +python3 -m unittest discover -s tests -t . # 298 tests +python3 -m sim.gsp_boot # B2 +python3 -m sim.gsp_boot --no-shim # B1 budget stop +python3 harness/sim_qemu_line.py # verdict "above" +python3 harness/re_demo.py # PASS 5/5 +./risc-v-gsp-rm-debugger.sh triage \ + --firmware ../../../firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \ + --version 570.144 --out /tmp/triage-570.144 +``` diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/ADMISSION.md b/debug/risc-v-gsp-rm-vm/gpgpu-sim/ADMISSION.md new file mode 100644 index 0000000..0cf7585 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/ADMISSION.md @@ -0,0 +1,38 @@ +# Admission records — GreenEye-GPGPU-Sim (DRAFT, pending maintainer filing) + +> **Status: drafts.** These five records were prepared with the Phase-0 +> program plan and are reproduced here for review. They are **not yet filed** +> as public eng-ops pilot admissions: a maintainer must review, name the value +> axis and file them in the public pilot docs before any of these capabilities +> is treated as admitted. Nothing below changes a threshold after the fact. +> +> Per the project admission rule, each record carries all five fields — +> scenario (loop), measurable outcome, baseline, threshold with N and +> environment, and failure signal. A missing field means do-not-build, not +> "deferred". Thresholds were frozen before the first run of each capability. +> +> This overlay is development-only; it never becomes a product or guest +> component, and no record authorises hardware, module, mdev or guest access. + +## Draft records + +| Record | Tool | Scenario (loop) | Measurable outcome | Baseline | Threshold (N, env) | Failure signal | +|---|---|---|---|---|---|---| +| TOOL-2026-SIM-01 | greeneye-gpgpu-sim build/run | porting/RE loop: run a CUDA kernel off-hardware | functional run completes; perf stats emitted; session dir | "no simulator" → static reading only, or QEMU-only | kernel suite 100% functional; build from clean checkout ≤30 min (N=3, this host) | build not reproducible; run non-deterministic | +| TOOL-2026-SIM-02 | TU104 model calibration | accuracy loop: answer "does the model match?" | C1–C6 metric table vs published bands | GPGPU-Sim SM75 config (uncalibrated) | latency bands ±15%, bandwidth ±20%, correlation ≥0.9 (N≥5 kernels) | metrics fail 2 consecutive releases or are tuned per-kernel | +| TOOL-2026-SIM-03 | GSP-in-sim (real firmware) | dynamic RE loop: what does firmware do before hardware exists? | session with `cycle` events + boot-progress milestone | QEMU probe (first fault at `0x4009014`) | reaches ≥1 milestone past probe deterministically (N=3 identical) | non-determinism; unexplained delta on replay | +| TOOL-2026-SIM-04 | MCU cross-controller model | RE/portability: cross-controller exchange | device-level session + latency table | none (no model) | one exchange reproduced with declared sources; latency within documented band | semantics unprovable in one milestone (kill per F8) | +| TOOL-2026-SIM-05 | sim debugger backend | debugging loop: hook/watch/Ghidra on simulated target | event-parity session vs QEMU on demo | QEMU workflow (minutes) | parity suite green; workflow ≤50% of QEMU baseline time (N=5) | parity broken or slower than baseline | + +## Value-axis mapping (to be confirmed at filing) + +| Record | Primary value axes | +|---|---| +| TOOL-2026-SIM-01 | development velocity on simulated GSP; analysis throughput | +| TOOL-2026-SIM-02 | performance analysis; correctness of analysis | +| TOOL-2026-SIM-03 | faster reverse engineering; new-GSP support time | +| TOOL-2026-SIM-04 | architectural understanding / SBOM; device support | +| TOOL-2026-SIM-05 | bug-analysis time; guest-compat readiness | + +P4 (fabric/system) maps to a fabric value axis only once a peer model exists; +it is deliberately not filed here. diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/BUILD-PORT.md b/debug/risc-v-gsp-rm-vm/gpgpu-sim/BUILD-PORT.md new file mode 100644 index 0000000..b767f8f --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/BUILD-PORT.md @@ -0,0 +1,222 @@ +# BUILD-PORT.md — GPGPU-Sim v4.0.1 on CUDA 12.x / gcc 13 (WP0.1) + +Status: **builds and runs** on this host; `vectorAdd` passes in functional and +performance modes. This is a development-tool port record, not a support +claim. Scope: get upstream GPGPU-Sim v4.0.1 to compile and execute a minimal +CUDA kernel with a modern distro toolchain, and record the minimal patch set. + +## 1. Upstream pin + +| Item | Value | +|---|---| +| Repository | `gpgpu-sim/gpgpu-sim_distribution` (public) | +| Commit | `90ec3399763d7c8512cfe7dc193473086c38ca38` | +| Tag | `v4.0.1` | +| License | BSD-3-Clause (upstream); development copy only | + +No upstream fork/history rewrite: reproduce by cloning the pin and applying +the two patches in `patches/`. + +## 2. Toolchain and CUDA used + +| Item | Value | +|---|---| +| OS | Ubuntu 24.04-class x86-64 | +| Compiler | gcc/g++ 13.3.0 | +| make | 4.3 | +| CUDA toolkit | **12.0 (V12.0.140)**, `/usr/bin/nvcc` (distro package), `CUDA_INSTALL_PATH=/usr` | +| Extra host package | `xutils-dev` (provides `makedepend`, required by the build) | +| GPU required | **No** — the simulator is CPU-only; no module load, no `/dev` access | + +Notes discovered during the port: + +* `/usr/local/cuda-12.9` exists but contains only `gds/` — it has **no + `bin/nvcc`**, so it cannot be used as `CUDA_INSTALL_PATH`. CUDA 12.0 from + `/usr` is the toolkit actually used. +* The application must be compiled against the **same CUDA major** as the + simulator (`get_app_cuda_version()` asserts major equality): CUDA 12 app + + CUDA 12 simulator. + +## 3. Reproduction (exact commands) + +```bash +# 1. Host prerequisites +sudo apt-get install -y build-essential xutils-dev # makedepend +# plus a CUDA toolkit (nvcc) — here /usr (CUDA 12.0) + +# 2. Pinned clone +git clone https://github.com/gpgpu-sim/gpgpu-sim_distribution gpgpu-sim +cd gpgpu-sim +git checkout 90ec339 # tag v4.0.1 + +# 3. Apply the tracked port patches (in order) +git apply patches/0001-cuda12-toolchain-and-soname.patch +git apply patches/0002-gcc13-cuda12-source-fixes.patch + +# 4. Build (release; default) +export CUDA_INSTALL_PATH=/usr +source setup_environment +make -j4 # use -j2 when a QEMU guest is running on the host + +# Artifacts: +# lib/gcc-13.3.0/cuda-12000/release/libcudart.so (+ .so.12 symlink) +# build/gcc-13.3.0/cuda-12000/release/ (objects, libs, mcpat) +``` + +`setup_environment` prepares `PATH` and `LD_LIBRARY_PATH` so that CUDA +applications load the simulator's `libcudart.so` instead of the real runtime. + +### 3.1 Test application + +```cuda +// vectorAdd.cu +#include <stdio.h> +#include <cuda_runtime.h> + +__global__ void vectorAdd(const float *A, const float *B, float *C, int n) { + int i = blockDim.x * blockIdx.x + threadIdx.x; + if (i < n) C[i] = A[i] + B[i]; +} + +int main() { + int n = 1024; + size_t sz = n * sizeof(float); + float *hA = (float *)malloc(sz), *hB = (float *)malloc(sz), *hC = (float *)malloc(sz); + for (int i = 0; i < n; i++) { hA[i] = 1.0f; hB[i] = 2.0f; hC[i] = 0.0f; } + float *dA, *dB, *dC; + cudaMalloc((void **)&dA, sz); + cudaMalloc((void **)&dB, sz); + cudaMalloc((void **)&dC, sz); + cudaMemcpy(dA, hA, sz, cudaMemcpyHostToDevice); + cudaMemcpy(dB, hB, sz, cudaMemcpyHostToDevice); + vectorAdd<<<4, 256>>>(dA, dB, dC, n); + cudaDeviceSynchronize(); + cudaMemcpy(hC, dC, sz, cudaMemcpyDeviceToHost); + int ok = 1; + for (int i = 0; i < n; i++) if (hC[i] != 3.0f) ok = 0; + printf("vectorAdd %s (first=%f)\n", ok ? "PASS" : "FAIL", hC[0]); + cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) printf("CUDA error: %s\n", cudaGetErrorString(e)); + return ok ? 0 : 1; +} +``` + +```bash +nvcc -arch=sm_75 --cudart shared -o vectorAdd vectorAdd.cu +``` + +`--cudart shared` makes the binary depend on `libcudart.so.12`; the +simulator's library is named/interposed accordingly (patches 0001). + +### 3.2 Run + +```bash +# run dir: app + a config; the entrypoint hardcodes "-config gpgpusim.config" +mkdir -p run-functional && cd run-functional +cp ../vectorAdd . +cp ../configs/tested-cfgs/SM75_RTX2060/gpgpusim.config . +cp ../configs/tested-cfgs/SM75_RTX2060/config_turing_islip.icnt . +sed -i 's/^-gpgpu_ptx_sim_mode 0/-gpgpu_ptx_sim_mode 1/' gpgpusim.config # functional + +export CUDA_INSTALL_PATH=/usr +source ../setup_environment +./vectorAdd + +# performance mode: use gpgpusim.config unmodified (-gpgpu_ptx_sim_mode 0) +``` + +Note: GPGPU-Sim 4.0.1 reads options **only** from the config file +(`gpgpusim_entrypoint.cc` hardcodes `-config gpgpusim.config`); to switch +functional/perf, edit that line (or the copied file) rather than passing app +argv. + +## 4. Patch set + +Two patches against `90ec339`, exported with `git diff`; both apply cleanly +to a pristine v4.0.1 tree (`git apply --check` verified). + +### 0001-cuda12-toolchain-and-soname.patch + +| File | Change | Why | +|---|---|---| +| `setup_environment` | CUDA upper bound `11100` → `12999` | version gate rejected CUDA 12.0 before anything else could run | +| `setup_environment`, `version_detection.mk` | gcc version regex `[0-9]\.[0-9]\.[0-9]` → `[0-9]+\.[0-9]+\.[0-9]+` | single-digit-only pattern failed on gcc 13.3.0 → empty `CC_VERSION` → bogus `gcc-/cuda-12000` build paths | +| `linux-so-version.txt` | add `libcudart.so.12{}` node | CUDA 12 apps reference symbols as `sym@libcudart.so.12` | +| `Makefile` | create `libcudart.so.12` / `.12.0` symlinks | CUDA 12 binary `DT_NEEDED` is `libcudart.so.12` | + +### 0002-gcc13-cuda12-source-fixes.patch + +| File | Change | Why | +|---|---|---| +| `src/abstract_hardware_model.h` | `textureReference` placeholder struct when `CUDART_VERSION >= 12000` | CUDA 12 removed the legacy texture-reference type from the runtime headers; the simulator's legacy texture path must still compile (unreachable for CUDA 12 apps) | +| `src/cuda-sim/cuda-sim.cc` | guard legacy `texref->...` field asserts with `#if CUDART_VERSION < 12000` | fields no longer exist in CUDA 12 | +| `src/cuda-sim/Makefile` | escape `"` in the generated `ptx_parser_decode.def` (both Linux/macOS rules) | bison comments contain quotes (`/* "end of file" */`) which became illegal C++11 user-defined-literal tokens (`""end`) under gcc 13 | +| `libcuda/cuda_runtime_api.cc` | `return (unsigned)cudaConfigureCallInternal(...)` in `__cudaPushCallConfiguration` | missing `return` is UB; gcc 13 `-O3` compiled it into a non-returning call/announce loop → stack-overflow SIGSEGV on the first kernel launch | + +`libcuda/cuobjdump.l` / `cuobjdump.y` needed **no change**: CUDA 12 +`cuobjdump` output is parsed and the embedded PTX is extracted (see §5). + +## 5. Observed results + +Kernel: `vectorAdd` (`__global__`, grid (4,1,1), block (256,1,1)), sm_75, +compiled with CUDA 12.0, config `SM75_RTX2060`. + +| Run | Result | +|---|---| +| Functional (`-gpgpu_ptx_sim_mode 1`) | `vectorAdd PASS (first=3.000000)`; `Done functional simulation (22528 instructions simulated)` | +| Performance (`-gpgpu_ptx_sim_mode 0`) | `vectorAdd PASS (first=3.000000)`; `gpu_tot_sim_cycle = 5569`, `gpu_tot_sim_insn = 21504`, `gpu_tot_ipc = 3.8614`, `gpu_silicon_slowdown = 245106x`; L2 and interconnect stats printed | + +Clean-build verification: a pristine v4.0.1 worktree + the two patches was +built from scratch (`make -j4`, exit 0, no errors) and both modes were +re-run there with the same PASS results. + +PTX path: `__cudaRegisterFatBinary` → `cuobjdump` extraction +(`vectorAdd.1.sm_75.ptx`) → PTX parser → functional/perf execution, all on +CUDA 12 output without grammar changes. + +## 6. Known failures / limitations + +* **CUDA 12.0.140 only.** 12.9/13.x untested; `/usr/local/cuda-12.9` has no + `nvcc` on this host. +* **Legacy texture API** (CUDA < 12 API) is a compile placeholder for + CUDA ≥ 12; texture execution is untested and unreachable for CUDA 12 apps. +* **Release build only.** `source setup_environment debug` was not built. +* **PTXPlus / SASS path** (sm_1x, CUDA ≤ 4.2) is untouched and not applicable + to CUDA 12. +* **OpenCL support not built** (no `libOpenCL` configured). +* Only `vectorAdd` was validated; no upstream test suite/cuBLAS/DNN kernels. +* Upstream limitations unchanged: no system-level I/O, no multi-GPU, no + device/firmware models (those are later program phases). +* `makedepend` is required; it is not installed by default on this distro. +* The host build uses `-O3`; the `__cudaPushCallConfiguration` fix is + required because gcc 13 miscompiles the missing-`return` body. If a future + compiler changes layout the guard remains correct (explicit return). + +## 7. Clean-host reproduction checklist + +- [ ] `gcc`/`g++` present (tested 13.3.0); `make` present. +- [ ] `xutils-dev` installed → `makedepend` on `PATH`. +- [ ] CUDA toolkit with `nvcc`, `cuobjdump`, `ptxas`; set + `CUDA_INSTALL_PATH` to its prefix (tested `/usr`, CUDA 12.0). Verify + `$CUDA_INSTALL_PATH/bin/nvcc` exists (a bare `cuda-12.9/` directory may + not contain the toolkit). +- [ ] Clone upstream and `git checkout 90ec339` (tag `v4.0.1`). +- [ ] `git apply` both patches under `patches/`; confirm `git apply --check` + passed on pristine tree. +- [ ] `export CUDA_INSTALL_PATH=...; source setup_environment; make -jN` + exits 0; `lib/gcc-<gccver>/cuda-<ver>/release/libcudart.so` exists and + `libcudart.so.12` symlinks to it. +- [ ] Compile the test app with `nvcc -arch=sm_75 --cudart shared`. +- [ ] Run functional and perf modes from a run dir containing + `gpgpusim.config` (+ copied icnt config); expect `vectorAdd PASS`. +- [ ] Do not run the app without `setup_environment` (would use the real + runtime / physical GPU); the simulator path is CPU-only. + +## 8. What is not verified + +* No debug build, no other configs (`SM7_QV100`, etc.), no CUDA 12.1–12.9/13. +* No CUDA regression suite, no multi-kernel or library workloads. +* No texture, surface, dynamic parallelism or OpenCL execution. +* No performance/accuracy claims beyond "the timing model runs and prints + stats"; the numbers above are one small kernel's simulator output. +* Patch set was validated on one host/toolchain combination. diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/README.md b/debug/risc-v-gsp-rm-vm/gpgpu-sim/README.md new file mode 100644 index 0000000..d9e4ff7 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/README.md @@ -0,0 +1,97 @@ +# GreenEye-GPGPU-Sim overlay (in development) + +> **Status: Phase 0 scaffold.** This directory is the tracked overlay that will +> turn GPGPU-Sim into the execution substrate of the +> [risc-v-gsp-rm-debugger](../README.md). Today it contains a reproducible +> build/setup path, a Turing TU104 (RTX 2080 SUPER) **configuration scaffold**, +> a run wrapper, a config-consistency test, and draft engineering-admission +> records. **It makes no accuracy, timing, performance or support claim**, and +> it is **not** calibrated to silicon. + +## 1. What this is + +GPGPU-Sim is an open-source, data-driven GPGPU performance/functional +simulator (BSD-3-Clause, University of British Columbia). The debugger program +needs an off-hardware execution substrate that can host the GPU model, the +GSP/microcontroller models and the debugger's hooks/watchpoints/session +recording on one deterministic clock. This overlay is how the project tracks +its changes to upstream: + +* `upstream/` — a pinned clone of upstream GPGPU-Sim (gitignored, never + committed; see [UPSTREAM.md](UPSTREAM.md)); +* `patches/` — our overlay patch set, applied in lexical order by `setup.sh` + (`0001` CUDA-12 toolchain/soname, `0002` gcc-13/CUDA-12 source fixes; the + build record is in [BUILD-PORT.md](BUILD-PORT.md)); +* `configs/` — tracked device configurations. `turing-tu104-2080super/` is the + first target (the local TU104 card); +* `scripts/` — `run_sim.sh` (run a CUDA binary against the simulator) and + `config_sanity.py` (config consistency test); +* `ADMISSION.md` — draft admission records TOOL-2026-SIM-01..05, pending + maintainer filing. + +**Development-only.** This is analysis tooling. It is never shipped, never +becomes a product dependency or guest component, and never touches GPU +hardware, the kernel module, mdev, or a guest. Real-firmware simulation is +Phase-1 work behind explicit gates; nothing here should be imported by +product code. + +## 2. Relationship to risc-v-gsp-rm-debugger + +The debugger already has two execution backends: QEMU (real image probe, +device-map harvest) and the native reference core under `../sim/` +(instruction-level, L0/L1). GPGPU-Sim becomes the third backend once the +Backend-ABI work lands (Phase 0/1.8): the same session format, hooks, +watchpoints, probes, Ghidra bridge and MCP surface drive all of them. The +TU104 config scaffold establishes the first GPU-side device profile; the GSP +and MCU models (Phase 1.5) are planned to run as clock-domain citizens of +this simulator. Nothing in this directory is on the critical path of the +existing tool: deleting it leaves the debugger fully functional. + +See `../docs/30-GPGPU-SIM-INTEGRATION.md` for the public integration note. + +## 3. Quick start + +```bash +cd open-iov/debug/risc-v-gsp-rm-vm/gpgpu-sim + +# 1. Clone the pinned upstream, apply patches/, build (see UPSTREAM.md). +# --no-build materialises the checkout only. Needs a CUDA toolkit; +# CUDA_INSTALL_PATH overrides the default /usr/local/cuda. +./setup.sh + +# 2. Check the TU104 scaffold config is internally consistent. +python3 scripts/config_sanity.py + +# 3. Run a CUDA binary (built with --cudart shared) against the simulator. +./scripts/run_sim.sh --help +./scripts/run_sim.sh ~/path/to/vectorAdd +``` + +Requirements: git, gcc/g++, make, bison, flex, zlib, and a CUDA toolkit. +`setup.sh` uses at most 4 compile jobs by default (`JOBS` overrides) to respect +the host budget. + +## 4. What is *not* claimed + +* **Not calibrated.** Every latency, timing and bandwidth parameter not tied + to a published TU104 number is marked `UNKNOWN:` in the config and is + inherited from the upstream SM75 Turing model. +* **Not a full GPU model yet.** No GSP core, no microcontroller models, no + fabric/PCIe/RDMA/NVMe models, no guest co-simulation. +* **No accuracy claim of any kind** until the Phase-1 calibration report + exists (admission TOOL-2026-SIM-02). Config-only modeling without + calibration evidence is an explicit anti-goal. +* **Not a product/guest component** and never a support claim. + +## 5. Layout + +| Path | Purpose | +|---|---| +| `UPSTREAM.md` | pinned upstream commit, provenance, patch policy | +| `setup.sh` | idempotent clone/checkout + patch + build | +| `patches/` | overlay patches applied in lexical order | +| `configs/turing-tu104-2080super/` | TU104/RTX 2080 SUPER scaffold config + Booksim interconnect file | +| `scripts/run_sim.sh` | LD_LIBRARY_PATH-substitution runner for CUDA binaries | +| `scripts/config_sanity.py` | stdlib-only config consistency test (also wired into `make test`) | +| `ADMISSION.md` | draft admission records (pending maintainer filing) | +| `build/` | build logs and run directories (gitignored) | diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/UPSTREAM.md b/debug/risc-v-gsp-rm-vm/gpgpu-sim/UPSTREAM.md new file mode 100644 index 0000000..32e1a09 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/UPSTREAM.md @@ -0,0 +1,71 @@ +# Upstream GPGPU-Sim: pin, provenance and patch policy + +> Public-safe reference for the upstream checkout used by this overlay. + +## 1. Pinned identity + +| Field | Value | +|---|---| +| Repository | `https://github.com/gpgpu-sim/gpgpu-sim_distribution` | +| Commit | `90ec3399763d7c8512cfe7dc193473086c38ca38` | +| Tag | `v4.0.1` | +| In-tree version string | `GPGPU-Sim Simulator Version 4.0.0` | +| License | BSD-3-Clause (Copyright 2009-2011 Tor M. Aamodt and The University of British Columbia) | +| Checkout location | `gpgpu-sim/upstream/` (gitignored; never committed) | + +The pin is reproduced by `setup.sh` (constant `PIN`). A fresh run clones into +`upstream/`, checks out the pin detached, applies `patches/`, and builds. +Re-running verifies the checkout is still at the pin and that every patch is +either applicable or already applied, so the process is idempotent. + +## 2. Clone source + +`setup.sh` clones from the upstream URL above by default. A local mirror is +used **only** when `GPGPUSIM_CLONE_SOURCE=<path-or-url>` is set explicitly +(unset by default); no host path is hardcoded or guessed. The clone source +never changes the pin: the checkout is always forced to `90ec339`. This is a +build convenience only; upstream code is never modified in place by hand. + +## 3. `origin/dev` (4.2.0): not checked out, tracked as a reference + +The repository's `origin/dev` branch is a later line (in-tree version string +`4.2.0`) that adds, among other things, AccelWattch power modeling, an Ampere +RTX 3060 (SM86) tested configuration, and Turing L1 fixes. It is **not** the +pin and is not used by Phase 0. It is recorded here because Phase 1/2 may +rebase selected changes (architecture config, L1 fixes) into our patch set +rather than adopting the whole branch. Any such rebase must be an explicit, +recorded decision with the same reproducibility guarantees (new pin + patch +set + test evidence), never a silent branch switch. + +## 4. How our changes are applied + +Design rule: upstream history is never forked. Our work lives entirely in this +overlay and reaches the upstream tree as **patches**, in this order: + +1. `setup.sh` clones/checks out the pin. +2. `patches/*.patch` are applied in lexical filename order + (`0001-*`, `0002-*`, ...) with `git apply`. +3. For each patch, `git apply --check` decides "apply"; otherwise + `git apply --reverse --check` decides "already applied"; if neither holds, + `setup.sh` fails loudly rather than guessing. +4. New source trees that are not clean patches ship as patches too (a patch + can add files). There is no separate overlay-copy step in Phase 0. + +The first expected patch set is the CUDA-12 build/runtime port (WP0.1), which +makes `setup_environment`, the PTX path and `cuobjdump` handling work on this +host's CUDA. Until those patches exist the default build correctly refuses to +proceed with a clear error. + +To reset the checkout: `./setup.sh --reclone` (destroys `upstream/` and clones +fresh). Nothing outside `gpgpu-sim/upstream/` and `gpgpu-sim/build/` is +touched by the build; both are gitignored. + +## 5. Reproducibility checks + +```bash +git -C gpgpu-sim/upstream rev-parse HEAD # must equal the pin above +git -C gpgpu-sim/upstream status --porcelain # patch-applied delta expected +sha256sum gpgpu-sim/patches/*.patch # record with any result +``` + +Build logs are written to `gpgpu-sim/build/build.log` and are not committed. diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/configs/turing-tu104-2080super/config_turing_islip.icnt b/debug/risc-v-gsp-rm-vm/gpgpu-sim/configs/turing-tu104-2080super/config_turing_islip.icnt new file mode 100644 index 0000000..74a7a30 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/configs/turing-tu104-2080super/config_turing_islip.icnt @@ -0,0 +1,89 @@ +// GreenEye-GPGPU-Sim -- Booksim/ISLIP interconnect scaffold for Turing TU104. +// +// Copied from the upstream SM75_RTX2060 `config_turing_islip.icnt` with the +// topology size adjusted for this config's device scale. Like the upstream +// Turing config, this file is NOT active by default: `gpgpusim.config` uses +// `-network_mode 2` (built-in local crossbar). Enable it only together with +// `-network_mode 1` and `-inter_config_file config_turing_islip.icnt`. +// +// Public specs encoded (RTX 2080 SUPER, TU104): 48 SMs, 256-bit GDDR6, +// 8 GB, ~496 GB/s. This interconnect model is uncalibrated. +// +// UNKNOWN: upstream SM75_RTX2060 uses k=52 while this config has 64 modeled +// nodes (48 SMs + 16 memory modules); the tested configs' k values are not +// simply SM+memory counts. k=64 is the nearest documented node count for this +// scaffold and MUST be validated in WP1.3 before any interconnect result is +// trusted. All other parameters are inherited verbatim from upstream. + +//64*1 fly with 32 flits per packet under gpgpusim injection mode +use_map = 0; +flit_size = 40; + +// currently we do not use this, see subnets below +network_count = 2; + +// Topology +topology = fly; +k = 64; +n = 1; + +// Routing + +routing_function = dest_tag; + +// Flow control + +num_vcs = 1; +vc_buf_size = 64; +input_buffer_size = 256; +ejection_buffer_size = 64; +boundary_buffer_size = 64; + +wait_for_tail_credit = 0; + +// Router architecture + +vc_allocator = islip; //separable_input_first; +sw_allocator = islip; //separable_input_first; +alloc_iters = 1; + +credit_delay = 0; +routing_delay = 0; +vc_alloc_delay = 1; +sw_alloc_delay = 1; + +input_speedup = 1; +output_speedup = 1; +internal_speedup = 2.0; + +// Traffic, GPGPU-Sim does not use this + +traffic = uniform; +packet_size ={{1,2,3,4},{10,20}}; +packet_size_rate={{1,1,1,1},{2,1}}; + +// Simulation - Don't change + +sim_type = gpgpusim; +//sim_type = latency; +injection_rate = 0.1; + +subnets = 2; + +// Always use read and write no matter following line +//use_read_write = 1; + + +read_request_subnet = 0; +read_reply_subnet = 1; +write_request_subnet = 0; +write_reply_subnet = 1; + +read_request_begin_vc = 0; +read_request_end_vc = 0; +write_request_begin_vc = 0; +write_request_end_vc = 0; +read_reply_begin_vc = 0; +read_reply_end_vc = 0; +write_reply_begin_vc = 0; +write_reply_end_vc = 0; diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/configs/turing-tu104-2080super/gpgpusim.config b/debug/risc-v-gsp-rm-vm/gpgpu-sim/configs/turing-tu104-2080super/gpgpusim.config new file mode 100644 index 0000000..641f8d3 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/configs/turing-tu104-2080super/gpgpusim.config @@ -0,0 +1,233 @@ +# GreenEye-GPGPU-Sim -- Turing TU104 config scaffold: GeForce RTX 2080 SUPER. +# +# STATUS: SCAFFOLD, NOT CALIBRATED. This file is derived from the upstream +# GPGPU-Sim SM75_RTX2060 Turing configuration and adjusts only the +# device-level parameters that public specifications distinguish between +# TU106 (RTX 2060) and TU104 (RTX 2080 SUPER). The SM75 microarchitecture +# parameters (issue widths, latencies, sub-core model, cache geometry) are +# inherited unchanged. It makes NO accuracy claim; see ../ADMISSION.md +# (TOOL-2026-SIM-02) and ../README.md. +# +# Public specifications encoded below (RTX 2080 SUPER, TU104): +# - 48 SMs, 3072 CUDA cores (64 FP32 per SM) [NVIDIA product specs] +# - 8 GB GDDR6, 256-bit bus, 15.5 Gbps effective, +# ~496 GB/s (496.1 GB/s nominal) [NVIDIA product specs] +# - 4 MB L2, 64 ROPs, 192 TMUs [TU104 die specs] +# - base clock ~1650 MHz, boost clock ~1815 MHz [NVIDIA product specs] +# - compute capability 7.5, PCIe 3.0 x16 [NVIDIA product specs] +# +# Machine-checkable spec markers (parsed by ../scripts/config_sanity.py): +# SPEC: sm_count=48 +# SPEC: memory_type=GDDR6 +# SPEC: vram_gib=8 +# SPEC: bus_width_bits=256 +# SPEC: bandwidth_gbps=496 +# SPEC: l2_mib=4 +# SPEC: rops=64 +# SPEC: tmus=192 +# SPEC: base_mhz=1650 +# SPEC: boost_mhz=1815 +# SPEC: compute_capability=7.5 +# +# Upstream references for the inherited Turing model: +# 1- https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/technologies/turing-architecture/NVIDIA-Turing-Architecture-Whitepaper.pdf +# 2- "RTX on--The NVIDIA Turing GPU", IEEE MICRO 2020 + +# functional simulator specification +-gpgpu_ptx_instruction_classification 0 +-gpgpu_ptx_sim_mode 0 +-gpgpu_ptx_force_max_capability 75 + +# Device Limits +-gpgpu_stack_size_limit 1024 +-gpgpu_heap_size_limit 8388608 +-gpgpu_runtime_sync_depth_limit 2 +-gpgpu_runtime_pending_launch_count_limit 2048 +-gpgpu_kernel_launch_latency 5000 + +# Compute Capability +-gpgpu_compute_capability_major 7 +-gpgpu_compute_capability_minor 5 + +# PTX execution-driven +-gpgpu_ptx_convert_to_ptxplus 0 +-gpgpu_ptx_save_converted_ptxplus 0 + +# high level architecture configuration +# 48 SMs for TU104 (RTX 2080 SUPER). One shader core per cluster. +-gpgpu_n_clusters 48 +-gpgpu_n_cores_per_cluster 1 +# TU104 has a 256-bit GDDR6 bus. GPGPU-Sim's model counts memory modules; +# following the upstream Turing convention (SM75_RTX2060 uses 12 modules for +# a 192-bit bus, i.e. one modeled module per 16-bit channel) we use 16. +# -gpgpu_n_mem x -gpgpu_n_sub_partition_per_mchannel x 128 KiB = L2 size +# and buswidth x freq_ratio x DRAM clock x n_mem = modeled bandwidth, both +# checked by config_sanity.py. +-gpgpu_n_mem 16 +-gpgpu_n_sub_partition_per_mchannel 2 + +# Turing TU104 clock domains +# <Core Clock>:<Interconnect Clock>:<L2 Clock>:<DRAM Clock> (MHz) +# Base clock 1650 MHz; DRAM command clock = 15.5 Gbps / 4 (GDDR6 QDR). +-gpgpu_clock_domains 1650.0:1650.0:1650.0:3875.0 +# boost mode (published boost clock 1815 MHz) +# -gpgpu_clock_domains 1815.0:1815.0:1815.0:3875.0 + +# shader core pipeline config +-gpgpu_shader_registers 65536 +-gpgpu_registers_per_block 65536 +-gpgpu_occupancy_sm_number 75 + +# This implies a maximum of 32 warps/SM +-gpgpu_shader_core_pipeline 1024:32 +-gpgpu_shader_cta 32 +-gpgpu_simd_model 1 + +# Pipeline widths and number of FUs +# ID_OC_SP,ID_OC_DP,ID_OC_INT,ID_OC_SFU,ID_OC_MEM,OC_EX_SP,OC_EX_DP,OC_EX_INT,OC_EX_SFU,OC_EX_MEM,EX_WB,ID_OC_TENSOR_CORE,OC_EX_TENSOR_CORE +## Turing has 4 SP SIMD units, 4 INT units, 4 SFU units, 8 Tensor core units +## We need to scale the number of pipeline registers to be equal to the number of SP units +-gpgpu_pipeline_widths 4,0,4,4,4,4,0,4,4,4,8,4,4 +-gpgpu_num_sp_units 4 +-gpgpu_num_sfu_units 4 +-gpgpu_num_int_units 4 +-gpgpu_tensor_core_avail 1 +-gpgpu_num_tensor_core_units 4 + +# Instruction latencies and initiation intervals +# "ADD,MAX,MUL,MAD,DIV" +# All Div operations are executed on SFU unit +-ptx_opcode_latency_int 4,13,4,5,145,32 +-ptx_opcode_initiation_int 2,2,2,2,8,4 +-ptx_opcode_latency_fp 4,13,4,5,39 +-ptx_opcode_initiation_fp 2,2,2,2,4 +-ptx_opcode_latency_dp 8,19,8,8,330 +-ptx_opcode_initiation_dp 4,4,4,4,130 +-ptx_opcode_latency_sfu 100 +-ptx_opcode_initiation_sfu 8 +-ptx_opcode_latency_tesnor 64 +-ptx_opcode_initiation_tensor 64 +# UNKNOWN: instruction latencies/initiation intervals are inherited from the +# upstream SM75_RTX2060 model; they are not measured on TU104. Calibration +# is Phase-1 work (WP1.1/WP1.4, admission TOOL-2026-SIM-02). + +# Turing has four schedulers per core +-gpgpu_num_sched_per_core 4 +# Greedy then oldest scheduler +-gpgpu_scheduler gto +## In Turing, a warp scheduler can issue 1 inst per cycle +-gpgpu_max_insn_issue_per_warp 1 +-gpgpu_dual_issue_diff_exec_units 1 + +# shared memory bankconflict detection +-gpgpu_shmem_num_banks 32 +-gpgpu_shmem_limited_broadcast 0 +-gpgpu_shmem_warp_parts 1 +-gpgpu_coalesce_arch 75 + +# Turing has sub core model, in which each scheduler has its own register file and EUs +# i.e. schedulers are isolated +-gpgpu_sub_core_model 1 +# disable specialized operand collectors and use generic operand collectors instead +-gpgpu_enable_specialized_operand_collector 0 +-gpgpu_operand_collector_num_units_gen 8 +-gpgpu_operand_collector_num_in_ports_gen 8 +-gpgpu_operand_collector_num_out_ports_gen 8 +# turing has 8 banks dual-port, 4 schedulers, two banks per scheduler +# we increase #banks to 16 to mitigate the effect of Register File Cache (RFC) which we do not implement in the current version +-gpgpu_num_reg_banks 16 +-gpgpu_reg_file_port_throughput 2 + +# <nsets>:<bsize>:<assoc>,<rep>:<wr>:<alloc>:<wr_alloc>:<set_index_fn>,<mshr>:<N>:<merge>,<mq>:**<fifo_entry> +# ** Optional parameter - Required when mshr_type==Texture Fifo +-gpgpu_adaptive_cache_config 0 +-gpgpu_l1_banks 4 +-gpgpu_cache:dl1 S:1:128:512,L:L:s:N:L,A:256:8,16:0,32 +-gpgpu_shmem_size 65536 +-gpgpu_shmem_sizeDefault 65536 +-gpgpu_shmem_per_block 65536 +-gpgpu_gmem_skip_L1D 0 +-gpgpu_n_cluster_ejection_buffer_size 32 +-gpgpu_l1_latency 20 +-gpgpu_smem_latency 20 +-gpgpu_flush_l1_cache 1 +# UNKNOWN: L1/shmem latencies are inherited from upstream; not TU104-measured. + +# 64 sets, each 128 bytes 16-way for each memory sub partition (128 KB per +# memory sub partition). With 16 memory modules and 2 sub-partitions each this +# gives us 4 MB L2 cache, matching the published TU104 L2 size. +-gpgpu_cache:dl2 S:64:128:16,L:B:m:L:P,A:192:4,32:0,32 +-gpgpu_cache:dl2_texture_only 0 +-gpgpu_dram_partition_queues 64:64:64:64 +-gpgpu_perf_sim_memcpy 1 +-gpgpu_memory_partition_indexing 0 + +# 128 KB Inst. +-gpgpu_cache:il1 N:64:128:16,L:R:f:N:L,S:2:48,4 +-gpgpu_inst_fetch_throughput 4 +# 128 KB Tex +# Note, TEX is deprecated in Volta, It is used for legacy apps only. Use L1D cache instead with .nc modifier or __ldg method +-gpgpu_tex_cache:l1 N:4:128:256,L:R:m:N:L,T:512:8,128:2 +# 64 KB Const +-gpgpu_const_cache:l1 N:128:64:8,L:R:f:N:L,S:2:64,4 +-gpgpu_perfect_inst_const_cache 1 + +# interconnection +#-network_mode 1 +#-inter_config_file config_turing_islip.icnt +# use built-in local xbar +-network_mode 2 +-icnt_in_buffer_limit 512 +-icnt_out_buffer_limit 512 +-icnt_subnets 2 +-icnt_arbiter_algo 1 +-icnt_flit_size 40 + +# memory partition latency config +-gpgpu_l2_rop_latency 160 +-dram_latency 100 +# UNKNOWN: L2/ROP and DRAM latencies are inherited from upstream SM75_RTX2060 +# and not calibrated to TU104 GDDR6. WP1.3/WP1.4 work. + +# dram model config +-gpgpu_dram_scheduler 1 +-gpgpu_frfcfs_dram_sched_queue_size 64 +-gpgpu_dram_return_queue_size 192 + +# Turing has GDDR6 +# http://monitorinsider.com/GDDR6.html +-gpgpu_n_mem_per_ctrlr 1 +-gpgpu_dram_buswidth 2 +-gpgpu_dram_burst_length 16 +-dram_data_command_freq_ratio 4 +-gpgpu_mem_address_mask 1 +# UNKNOWN: the DRAM address mapping string is inherited from the 12-module +# RTX 2060 config (4 channel bits). It has not been re-derived for the +# 16-module TU104 layout; recalibrate in WP1.3 before trusting bandwidth. +-gpgpu_mem_addr_mapping dramid@8;00000000.00000000.00000000.00000000.0000RRRR.RRRRRRRR.RBBBCCCC.BCCSSSSS + +# Use the same GDDR5-derived timing set as upstream, scaled to 3875 MHz +# (15.5 Gbps / 4). UNKNOWN: true TU104 GDDR6 timings are not calibrated. +-gpgpu_dram_timing_opt "nbk=16:CCD=4:RRD=10:RCD=20:RAS=50:RP=20:RC=62: + CL=20:WL=8:CDLR=9:WR=20:nbkgrp=4:CCDL=4:RTPL=4" + +# select lower bits for bnkgrp to increase bnkgrp parallelism +-dram_bnk_indexing_policy 0 +-dram_bnkgrp_indexing_policy 1 + +#-dram_seperate_write_queue_enable 1 +#-dram_write_queue_size 64:56:32 + +# stat collection +-gpgpu_memlatency_stat 14 +-gpgpu_runtime_stat 500 +-enable_ptx_file_line_stats 1 +-visualizer_enabled 0 + +# power model configs, disable until we create a real energy model for Turing +-power_simulation_enabled 0 + +# tracing functionality +#-trace_enabled 1 +#-trace_components WARP_SCHEDULER,SCOREBOARD +#-trace_sampling_core 0 diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/.gitkeep b/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/.gitkeep new file mode 100644 index 0000000..c93f122 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/.gitkeep @@ -0,0 +1,2 @@ +# Overlay patches are applied by ../setup.sh in lexical filename order. +# Empty is valid in Phase 0; the CUDA 12 build/runtime port (WP0.1) lands here. diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/0001-cuda12-toolchain-and-soname.patch b/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/0001-cuda12-toolchain-and-soname.patch new file mode 100644 index 0000000..af1fb31 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/0001-cuda12-toolchain-and-soname.patch @@ -0,0 +1,56 @@ +diff --git a/Makefile b/Makefile +index d248211..1ab7c0c 100644 +--- a/Makefile ++++ b/Makefile +@@ -168,6 +168,8 @@ $(SIM_LIB_DIR)/libcudart.so: makedirs $(LIBS) cudalib + if [ ! -f $(SIM_LIB_DIR)/libcudart.so.10.0 ]; then ln -s libcudart.so $(SIM_LIB_DIR)/libcudart.so.10.0; fi + if [ ! -f $(SIM_LIB_DIR)/libcudart.so.10.1 ]; then ln -s libcudart.so $(SIM_LIB_DIR)/libcudart.so.10.1; fi + if [ ! -f $(SIM_LIB_DIR)/libcudart.so.11.0 ]; then ln -s libcudart.so $(SIM_LIB_DIR)/libcudart.so.11.0; fi ++ if [ ! -f $(SIM_LIB_DIR)/libcudart.so.12 ]; then ln -s libcudart.so $(SIM_LIB_DIR)/libcudart.so.12; fi ++ if [ ! -f $(SIM_LIB_DIR)/libcudart.so.12.0 ]; then ln -s libcudart.so $(SIM_LIB_DIR)/libcudart.so.12.0; fi + + $(SIM_LIB_DIR)/libcudart.dylib: makedirs $(LIBS) cudalib + g++ -dynamiclib -Wl,-headerpad_max_install_names,-undefined,dynamic_lookup,-compatibility_version,1.1,-current_version,1.1\ +diff --git a/linux-so-version.txt b/linux-so-version.txt +index 3db07ac..55d3727 100644 +--- a/linux-so-version.txt ++++ b/linux-so-version.txt +@@ -10,5 +10,7 @@ libcudart.so.10.1{ + }; + libcudart.so.11.0{ + }; ++libcudart.so.12{ ++}; + libcuda.so.1{ + }; +diff --git a/setup_environment b/setup_environment +index 07d0788..cd054ea 100644 +--- a/setup_environment ++++ b/setup_environment +@@ -44,11 +44,11 @@ if [ $? = 1 ]; then + return; + fi + +-CC_VERSION=`gcc --version | head -1 | awk '{for(i=1;i<=NF;i++){ if(match($i,/^[0-9]\.[0-9]\.[0-9]$/)) {print $i; exit 0}}}'` ++CC_VERSION=`gcc --version | head -1 | awk '{for(i=1;i<=NF;i++){ if(match($i,/^[0-9]+\.[0-9]+\.[0-9]+$/)) {print $i; exit 0}}}'` + + CUDA_VERSION_STRING=`$CUDA_INSTALL_PATH/bin/nvcc --version | awk '/release/ {print $5;}' | sed 's/,//'`; + export CUDA_VERSION_NUMBER=`echo $CUDA_VERSION_STRING | sed 's/\./ /' | awk '{printf("%02u%02u", 10*int($1), 10*$2);}'` +-if [ $CUDA_VERSION_NUMBER -gt 11100 -o $CUDA_VERSION_NUMBER -lt 2030 ]; then ++if [ $CUDA_VERSION_NUMBER -gt 12999 -o $CUDA_VERSION_NUMBER -lt 2030 ]; then + echo "ERROR ** GPGPU-Sim version $GPGPUSIM_VERSION_STRING not tested with CUDA version $CUDA_VERSION_STRING (please see README)"; + echo $CUDA_VERSION_NUMBER + return +diff --git a/version_detection.mk b/version_detection.mk +index ee71a62..693e913 100644 +--- a/version_detection.mk ++++ b/version_detection.mk +@@ -43,7 +43,7 @@ CUDA_VERSION_STRING:=$(shell $(CUDA_INSTALL_PATH)/bin/nvcc --version | awk '/rel + CUDART_VERSION:=$(shell echo $(CUDA_VERSION_STRING) | sed 's/\./ /' | awk '{printf("%02u%02u", 10*int($$1), 10*$$2);}') + + # Detect GCC Version +-CC_VERSION := $(shell gcc --version | head -1 | awk '{for(i=1;i<=NF;i++){ if(match($$i,/^[0-9]\.[0-9]\.[0-9]$$/)) {print $$i; exit 0 }}}') ++CC_VERSION := $(shell gcc --version | head -1 | awk '{for(i=1;i<=NF;i++){ if(match($$i,/^[0-9]+\.[0-9]+\.[0-9]+$$/)) {print $$i; exit 0 }}}') + + # Detect Support for C++11 (C++0x) from GCC Version + GNUC_CPP0X := $(shell gcc --version | perl -ne 'if (/gcc\s+\(.*\)\s+([0-9.]+)/){ if($$1 >= 4.3) {$$n=1} else {$$n=0;} } END { print $$n; }') diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/0002-gcc13-cuda12-source-fixes.patch b/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/0002-gcc13-cuda12-source-fixes.patch new file mode 100644 index 0000000..a9367f8 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/patches/0002-gcc13-cuda12-source-fixes.patch @@ -0,0 +1,77 @@ +diff --git a/libcuda/cuda_runtime_api.cc b/libcuda/cuda_runtime_api.cc +index fd05f55..c6475c1 100644 +--- a/libcuda/cuda_runtime_api.cc ++++ b/libcuda/cuda_runtime_api.cc +@@ -3595,7 +3595,10 @@ unsigned CUDARTAPI __cudaPushCallConfiguration(dim3 gridDim, dim3 blockDim, + if (g_debug_execution >= 3) { + announce_call(__my_func__); + } +- cudaConfigureCallInternal(gridDim, blockDim, sharedMem, stream); ++ // Must return: falling off the end of this non-void function is undefined ++ // behaviour (gcc 13 -O3 miscompiles it into a non-returning loop). ++ return (unsigned)cudaConfigureCallInternal(gridDim, blockDim, sharedMem, ++ stream); + } + + cudaError_t CUDARTAPI __cudaPopCallConfiguration(dim3 *gridDim, dim3 *blockDim, +diff --git a/src/abstract_hardware_model.h b/src/abstract_hardware_model.h +index 49f3e9f..722dd15 100644 +--- a/src/abstract_hardware_model.h ++++ b/src/abstract_hardware_model.h +@@ -486,6 +486,18 @@ struct cudaArray { + + // Struct that record other attributes in the textureReference declaration + // - These attributes are passed thru __cudaRegisterTexture() ++#if defined(CUDART_VERSION) && CUDART_VERSION >= 12000 ++// CUDA 12 removed the legacy cudaBindTexture/textureReference runtime API. ++// Provide a placeholder layout for the simulator's legacy texture path so ++// that host-side emulation code still compiles; CUDA 12 apps cannot call the ++// removed API, so this path is unreachable for them. ++struct textureReference { ++ int normalized; ++ int filterMode; ++ int addressMode[3]; ++}; ++#endif ++ + struct textureReferenceAttr { + const struct textureReference *m_texref; + int m_dim; +diff --git a/src/cuda-sim/Makefile b/src/cuda-sim/Makefile +index 85d1c8c..1a7454b 100644 +--- a/src/cuda-sim/Makefile ++++ b/src/cuda-sim/Makefile +@@ -129,9 +129,9 @@ $(OUTPUT_DIR)/instructions.h: instructions.cc + + $(OUTPUT_DIR)/ptx_parser_decode.def: $(OUTPUT_DIR)/ptx.tab.c + ifeq ($(shell uname),Linux) +- cat $(OUTPUT_DIR)/ptx.tab.h | grep "=" | sed 's/^[ ]\+//' | sed 's/[=,]//g' | sed 's/\([_A-Z1-9]\+\)[ ]\+\([0-9]\+\)/\1 \1/' | sed 's/^/DEF(/' | sed 's/ /,"/' | sed 's/$$/")/' > $(OUTPUT_DIR)/ptx_parser_decode.def ++ cat $(OUTPUT_DIR)/ptx.tab.h | grep "=" | sed 's/^[ ]\+//' | sed 's/[=,]//g' | sed 's/"/\\"/g' | sed 's/\([_A-Z1-9]\+\)[ ]\+\([0-9]\+\)/\1 \1/' | sed 's/^/DEF(/' | sed 's/ /,"/' | sed 's/$$/")/' > $(OUTPUT_DIR)/ptx_parser_decode.def + else +- cat $(OUTPUT_DIR)/ptx.tab.h | grep "=" | sed -E 's/^ +//' | sed 's/[=,]//g' | sed -E 's/([_A-Z1-9]+).*/\1 \1/' | sed 's/^/DEF(/' | sed 's/ /,"/' | sed 's/$$/")/' > $(OUTPUT_DIR)/ptx_parser_decode.def ++ cat $(OUTPUT_DIR)/ptx.tab.h | grep "=" | sed -E 's/^ +//' | sed 's/[=,]//g' | sed 's/"/\\"/g' | sed -E 's/([_A-Z1-9]+).*/\1 \1/' | sed 's/^/DEF(/' | sed 's/ /,"/' | sed 's/$$/")/' > $(OUTPUT_DIR)/ptx_parser_decode.def + endif + + $(OUTPUT_DIR)/instructions.o: $(OUTPUT_DIR)/instructions.h $(OUTPUT_DIR)/ptx.tab.c +diff --git a/src/cuda-sim/cuda-sim.cc b/src/cuda-sim/cuda-sim.cc +index 71f0703..084a889 100644 +--- a/src/cuda-sim/cuda-sim.cc ++++ b/src/cuda-sim/cuda-sim.cc +@@ -128,7 +128,9 @@ void gpgpu_t::gpgpu_ptx_sim_bindNameToTexture( + } else { + const struct textureReference *tr = *m_NameToTextureRef[texname].begin(); + assert(tr != NULL); ++#if CUDART_VERSION < 12000 + // asserts that all texrefs in set have same fields ++ // (textureReference is opaque from CUDA 12 onward) + assert(tr->normalized == texref->normalized && + tr->filterMode == texref->filterMode && + tr->addressMode[0] == texref->addressMode[0] && +@@ -139,6 +141,7 @@ void gpgpu_t::gpgpu_ptx_sim_bindNameToTexture( + tr->channelDesc.z == texref->channelDesc.z && + tr->channelDesc.w == texref->channelDesc.w && + tr->channelDesc.f == texref->channelDesc.f); ++#endif + } + m_NameToTextureRef[texname].insert(texref); + m_TextureRefToName[texref] = texname; diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/scripts/config_sanity.py b/debug/risc-v-gsp-rm-vm/gpgpu-sim/scripts/config_sanity.py new file mode 100755 index 0000000..052695e --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/scripts/config_sanity.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +"""config_sanity.py -- stdlib-only sanity check for the TU104 scaffold config. + +Parses gpgpu-sim/configs/turing-tu104-2080super/gpgpusim.config and asserts +that the key device knobs and the machine-checkable `# SPEC:` markers are +present and internally consistent: + + * SM count : n_clusters x n_cores_per_cluster == 48 + * compute capability : major.minor == 7.5, PTX force max capability == 75 + * memory type : GDDR6 (burst length 16, QDR freq ratio 4) + * memory capacity : SPEC vram_gib == 8 (documentation-only; GPGPU-Sim + 4.x has no VRAM capacity knob) + * L2 size : sets x bsize x assoc x n_mem x n_sub == 4 MiB + * bandwidth : buswidth x freq_ratio x DRAM clock x n_mem == 496 GB/s + * clocks : base 1650 MHz, DRAM 3875 MHz (15.5 Gbps / 4) + +This is a scaffold consistency test, not a calibration: passing says the file +internally encodes the published TU104 numbers, NOT that the simulator models +TU104 accurately. Exit code 0 = all checks pass; 1 = at least one mismatch; +2 = usage/parse error. + +Usage: + python3 gpgpu-sim/scripts/config_sanity.py [--config PATH] [--quiet] +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +EXPECTED_SPECS = { + "sm_count": "48", + "memory_type": "GDDR6", + "vram_gib": "8", + "bus_width_bits": "256", + "bandwidth_gbps": "496", + "l2_mib": "4", + "rops": "64", + "tmus": "192", + "base_mhz": "1650", + "boost_mhz": "1815", + "compute_capability": "7.5", +} + +DEFAULT_CONFIG = ( + Path(__file__).resolve().parents[1] + / "configs" + / "turing-tu104-2080super" + / "gpgpusim.config" +) +ICNT_NAME = "config_turing_islip.icnt" + + +def parse_config(path: Path) -> tuple[dict[str, str], dict[str, str]]: + """Return (options, spec_markers) parsed from a gpgpusim.config file.""" + opts: dict[str, str] = {} + specs: dict[str, str] = {} + for raw in path.read_text(encoding="utf-8").splitlines(): + spec = re.match(r"^\s*#\s*SPEC:\s*([A-Za-z0-9_]+)\s*=\s*(\S+)\s*$", raw) + if spec: + specs[spec.group(1)] = spec.group(2) + line = raw.split("#", 1)[0].strip() + if not line or not line.startswith("-"): + continue + parts = line.split(None, 1) + opts[parts[0]] = parts[1].strip() if len(parts) > 1 else "" + return opts, specs + + +class Checker: + def __init__(self, quiet: bool = False) -> None: + self.quiet = quiet + self.passed = 0 + self.failures: list[str] = [] + + def check(self, name: str, condition: bool, detail: str) -> None: + if condition: + self.passed += 1 + if not self.quiet: + print(f"PASS: {name}: {detail}") + else: + self.failures.append(f"{name}: {detail}") + print(f"FAIL: {name}: {detail}") + + def note(self, text: str) -> None: + if not self.quiet: + print(f"NOTE: {text}") + + +def parse_int(opts: dict[str, str], key: str) -> int | None: + try: + return int(opts[key], 0) + except (KeyError, ValueError): + return None + + +def parse_l2(opts: dict[str, str]) -> tuple[int, int, int] | None: + """Parse '<nsets>:<bsize>:<assoc>' from -gpgpu_cache:dl2 S:... .""" + m = re.match(r"^S:(\d+):(\d+):(\d+)", opts.get("-gpgpu_cache:dl2", "")) + if not m: + return None + return int(m.group(1)), int(m.group(2)), int(m.group(3)) + + +def run_checks(config_path: Path, quiet: bool = False) -> int: + if not config_path.is_file(): + print(f"config_sanity: ERROR: config not found: {config_path}", file=sys.stderr) + return 2 + + opts, specs = parse_config(config_path) + c = Checker(quiet=quiet) + + # 1. spec markers ------------------------------------------------------ + for key, expected in EXPECTED_SPECS.items(): + actual = specs.get(key) + c.check( + f"SPEC {key}", + actual == expected, + f"expected {expected}, got {actual!r}", + ) + + # 2. SMs / compute capability ----------------------------------------- + clusters = parse_int(opts, "-gpgpu_n_clusters") + cores = parse_int(opts, "-gpgpu_n_cores_per_cluster") + sm_count = None if clusters is None or cores is None else clusters * cores + c.check( + "SM count", + sm_count == int(EXPECTED_SPECS["sm_count"]), + f"n_clusters({clusters}) x n_cores_per_cluster({cores}) = {sm_count}," + f" expected {EXPECTED_SPECS['sm_count']}", + ) + cc = f"{opts.get('-gpgpu_compute_capability_major', '?')}." \ + f"{opts.get('-gpgpu_compute_capability_minor', '?')}" + c.check( + "compute capability", + cc == EXPECTED_SPECS["compute_capability"], + f"{cc}, expected {EXPECTED_SPECS['compute_capability']}", + ) + c.check( + "PTX force max capability", + parse_int(opts, "-gpgpu_ptx_force_max_capability") == 75, + f"{opts.get('-gpgpu_ptx_force_max_capability', '(missing)')}, expected 75 (sm_75)", + ) + + # 3. clocks / GDDR6 / memory geometry --------------------------------- + clocks = opts.get("-gpgpu_clock_domains", "").split(":") + dram_mhz = None + if len(clocks) == 4: + try: + dram_mhz = float(clocks[3]) + except ValueError: + dram_mhz = None + c.check( + "DRAM clock", + dram_mhz == 3875.0, + f"{clocks[3] if len(clocks) == 4 else clocks}, expected 3875.0 MHz (15.5 Gbps / 4)", + ) + c.check( + "base core clock", + len(clocks) == 4 and clocks[0] == "1650.0", + f"{clocks[0] if clocks else '(missing)'}, expected 1650.0 MHz", + ) + c.check( + "memory type GDDR6", + specs.get("memory_type") == "GDDR6" + and parse_int(opts, "-gpgpu_dram_burst_length") == 16 + and parse_int(opts, "-dram_data_command_freq_ratio") == 4, + "burst_length=" + f"{opts.get('-gpgpu_dram_burst_length', '(missing)')}, " + "freq_ratio=" + f"{opts.get('-dram_data_command_freq_ratio', '(missing)')}" + " (GDDR6 expected: 16 / 4)", + ) + c.check( + "memory chips per controller", + parse_int(opts, "-gpgpu_n_mem_per_ctrlr") == 1, + f"{opts.get('-gpgpu_n_mem_per_ctrlr', '(missing)')}, expected 1 (upstream Turing convention)", + ) + c.check( + "VRAM capacity marker (documentation-only)", + specs.get("vram_gib") == "8", + "SPEC vram_gib=" + f"{specs.get('vram_gib')!r}; GPGPU-Sim 4.x has no VRAM capacity knob," + " so 8 GiB is recorded as a spec marker only", + ) + + # 4. L2 size consistency ---------------------------------------------- + n_mem = parse_int(opts, "-gpgpu_n_mem") + n_sub = parse_int(opts, "-gpgpu_n_sub_partition_per_mchannel") + l2 = parse_l2(opts) + l2_mib = None + if l2 and n_mem and n_sub: + sets, bsize, assoc = l2 + l2_mib = sets * bsize * assoc * n_mem * n_sub / (1024 * 1024) + spec_l2 = float(EXPECTED_SPECS["l2_mib"]) + c.check( + "L2 size", + l2_mib == spec_l2, + f"dl2({l2}) x n_mem({n_mem}) x n_sub({n_sub}) = {l2_mib} MiB," + f" expected {spec_l2} MiB", + ) + + # 5. bandwidth consistency -------------------------------------------- + buswidth = parse_int(opts, "-gpgpu_dram_buswidth") + ratio = parse_int(opts, "-dram_data_command_freq_ratio") + bw_gbps = None + if None not in (buswidth, ratio, dram_mhz, n_mem): + bw_gbps = buswidth * ratio * dram_mhz * 1e6 * n_mem / 1e9 + spec_bw = float(EXPECTED_SPECS["bandwidth_gbps"]) + c.check( + "modeled bandwidth", + bw_gbps == spec_bw, + f"buswidth({buswidth} B) x ratio({ratio}) x DRAM({dram_mhz} MHz)" + f" x n_mem({n_mem}) = {bw_gbps} GB/s, expected {spec_bw} GB/s", + ) + bw_bits_ok = False + if n_mem and buswidth: + bw_bits_ok = ( + specs.get("bus_width_bits") == "256" + and int(EXPECTED_SPECS["bus_width_bits"]) // (8 * n_mem) == buswidth + ) + c.check( + "bus width marker", + bw_bits_ok, + f"SPEC bus_width_bits={specs.get('bus_width_bits')!r}, modeled" + f" buswidth={buswidth} B x n_mem={n_mem} (expected {EXPECTED_SPECS['bus_width_bits']} bits" + f" = {buswidth} B x {n_mem} modules)", + ) + + # 6. informational markers not modeled by GPGPU-Sim 4.x --------------- + for key, label in (("rops", "ROPs"), ("tmus", "TMUs"), ("boost_mhz", "boost clock")): + c.check( + f"SPEC {key} present", + specs.get(key) == EXPECTED_SPECS[key], + f"{label}: {specs.get(key)!r} (expected {EXPECTED_SPECS[key]});" + " recorded for documentation, not a simulator knob", + ) + + # 7. sibling interconnect file (not used with -network_mode 2) ------- + icnt = config_path.parent / ICNT_NAME + c.check( + "interconnect config present", + icnt.is_file(), + f"{icnt}", + ) + + total = c.passed + len(c.failures) + print( + f"config_sanity: {c.passed}/{total} checks passed" + + (f", {len(c.failures)} failed" if c.failures else "") + ) + if c.failures: + for failure in c.failures: + print(f"config_sanity: FAILED -> {failure}", file=sys.stderr) + return 1 + print("config_sanity: OK (scaffold consistency only; no accuracy claim)") + return 0 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Sanity-check the GreenEye-GPGPU-Sim TU104 scaffold config." + ) + parser.add_argument( + "--config", + type=Path, + default=DEFAULT_CONFIG, + help=f"path to gpgpusim.config (default: {DEFAULT_CONFIG})", + ) + parser.add_argument("--quiet", action="store_true", help="only print failures and summary") + args = parser.parse_args(argv) + return run_checks(args.config, quiet=args.quiet) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/scripts/run_sim.sh b/debug/risc-v-gsp-rm-vm/gpgpu-sim/scripts/run_sim.sh new file mode 100755 index 0000000..85dd11e --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/scripts/run_sim.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# run_sim.sh -- run a CUDA binary on the GreenEye-GPGPU-Sim TU104 scaffold. +# +# Substitutes the CUDA runtime with GPGPU-Sim's libcudart.so via +# LD_LIBRARY_PATH (the mechanism documented in the upstream README, "Step 3: +# Run"), points the simulator at the tracked TU104 scaffold config, and +# executes the binary from a dedicated run directory so simulator output does +# not pollute the source tree. +# +# Development-only tooling: it simulates; it never touches GPU hardware, +# kernel modules, mdev, or a guest. +# +# Usage: +# ./run_sim.sh [--workdir DIR] [--build-type release|debug] <cuda-binary> [args...] +# +# Prerequisites: ./setup.sh has completed a build, and the binary was linked +# with `--cudart shared` so it resolves libcudart.so dynamically. +# +# Outputs (in the work directory, default gpgpu-sim/build/run/): +# gpgpusim.config -> symlink to the tracked TU104 config +# config_turing_islip.icnt -> symlink to the tracked interconnect config +# gpgpusim_out.txt, gpgpu_inst_stats.txt, ... (upstream simulator outputs) +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +UPSTREAM="$HERE/upstream" +DEFAULT_CONFIG_DIR="$HERE/configs/turing-tu104-2080super" +CONFIG_NAME="gpgpusim.config" +ICNT_NAME="config_turing_islip.icnt" +BUILD_TYPE="${GPGPUSIM_BUILD_TYPE:-release}" +WORKDIR="${GPGPUSIM_RUN_DIR:-$HERE/build/run}" +CONFIG_DIR="$DEFAULT_CONFIG_DIR" + +die() { echo "run_sim.sh: ERROR: $*" >&2; exit 1; } + +usage() { + cat <<'EOF' +Usage: run_sim.sh [--workdir DIR] [--build-type release|debug] \ + [--config-dir DIR] <cuda-binary> [args...] + + --workdir DIR directory the binary runs in (default gpgpu-sim/build/run) + --build-type TYPE release (default) or debug + --config-dir DIR config directory to use (default configs/turing-tu104-2080super) + -h, --help this help + +The binary must be dynamically linked against libcudart (link with +"--cudart shared"); LD_LIBRARY_PATH is replaced with the GPGPU-Sim runtime +so no recompilation for the simulator is needed. +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + --workdir) WORKDIR="${2:-}"; shift ;; + --build-type) BUILD_TYPE="${2:-}"; shift ;; + --config-dir) CONFIG_DIR="${2:-}"; shift ;; + -h|--help) usage; exit 0 ;; + --) shift; break ;; + -*) usage >&2; exit 2 ;; + *) break ;; + esac + shift +done + +[ $# -ge 1 ] || { usage >&2; exit 2; } +[ -n "$WORKDIR" ] || die "--workdir requires a value" +[ -n "$CONFIG_DIR" ] || die "--config-dir requires a value" +case "$BUILD_TYPE" in release|debug) ;; *) die "unknown build type '$BUILD_TYPE'" ;; esac + +BIN="$1"; shift + +case "$BIN" in + */*) + [ -e "$BIN" ] || die "binary not found: $BIN" + BIN="$(cd "$(dirname "$BIN")" && pwd)/$(basename "$BIN")" + ;; + *) + resolved="$(command -v "$BIN" || true)" + [ -n "$resolved" ] || die "binary '$BIN' not found in PATH" + BIN="$resolved" + ;; +esac + +[ -d "$CONFIG_DIR" ] || die "config dir not found: $CONFIG_DIR" +[ -f "$CONFIG_DIR/$CONFIG_NAME" ] || die "missing $CONFIG_DIR/$CONFIG_NAME" + +LIBDIR="$(find "$UPSTREAM/lib" -maxdepth 3 -type d -name "$BUILD_TYPE" 2>/dev/null | sort | head -n 1 || true)" +[ -n "$LIBDIR" ] || die "no GPGPU-Sim runtime library dir under $UPSTREAM/lib; run ./setup.sh first" +if ! ls "$LIBDIR"/libcudart.so* >/dev/null 2>&1; then + echo "run_sim.sh: WARNING: no libcudart.so in $LIBDIR; the binary may load the system CUDA runtime" >&2 +fi + +mkdir -p "$WORKDIR" +ln -sfn "$CONFIG_DIR/$CONFIG_NAME" "$WORKDIR/$CONFIG_NAME" +if [ -f "$CONFIG_DIR/$ICNT_NAME" ]; then + ln -sfn "$CONFIG_DIR/$ICNT_NAME" "$WORKDIR/$ICNT_NAME" +fi + +export GPGPUSIM_ROOT="$UPSTREAM" +export LD_LIBRARY_PATH="$LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +export CUDA_INSTALL_PATH="${CUDA_INSTALL_PATH:-/usr/local/cuda}" + +echo "run_sim.sh: binary : $BIN" +echo "run_sim.sh: config : $CONFIG_DIR/$CONFIG_NAME" +echo "run_sim.sh: workdir : $WORKDIR" +echo "run_sim.sh: runtime libs: $LIBDIR" + +cd "$WORKDIR" +exec "$BIN" "$@" diff --git a/debug/risc-v-gsp-rm-vm/gpgpu-sim/setup.sh b/debug/risc-v-gsp-rm-vm/gpgpu-sim/setup.sh new file mode 100755 index 0000000..56e18ad --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/gpgpu-sim/setup.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# setup.sh -- materialise and build the pinned GPGPU-Sim upstream checkout. +# +# This is the Phase-0 entry point of the GreenEye-GPGPU-Sim overlay: +# 1. clone upstream GPGPU-Sim at the pinned commit into ./upstream/ (gitignored), +# 2. apply ./patches/*.patch in lexical order (empty for now is fine), +# 3. build with the CUDA toolkit selected by CUDA_INSTALL_PATH. +# +# Idempotent: re-running is safe (clone is reused, already-applied patches are +# skipped, the build is incremental). Fails loudly on any ambiguity. Never +# uses sudo. This is development-only tooling: it never touches GPU hardware, +# kernel modules, mdev, a guest, or the product tree. +# +# Usage: +# ./setup.sh [--no-build] [--reclone] [--build-type release|debug] +# +# Environment: +# CUDA_INSTALL_PATH CUDA toolkit to build against (default /usr/local/cuda) +# JOBS compile jobs (default 4; host budget cap) +# GPGPUSIM_BUILD_TYPE release (default) or debug +# GPGPUSIM_CLONE_SOURCE explicit local mirror path or URL (unset by +# default; when unset the upstream URL is used) +# +# Exit codes: 0 success; 1 precondition/build failure; 2 usage error. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UPSTREAM="$HERE/upstream" +PATCH_DIR="$HERE/patches" +BUILD_DIR="$HERE/build" +PIN="90ec3399763d7c8512cfe7dc193473086c38ca38" +UPSTREAM_URL="https://github.com/gpgpu-sim/gpgpu-sim_distribution.git" + +BUILD=1 +RECLONE=0 +BUILD_TYPE="${GPGPUSIM_BUILD_TYPE:-release}" +JOBS="${JOBS:-4}" + +die() { echo "setup.sh: ERROR: $*" >&2; exit 1; } + +usage() { + cat <<'EOF' +Usage: setup.sh [--no-build] [--reclone] [--build-type release|debug] + + --no-build clone/checkout + apply patches only + --reclone delete ./upstream and clone fresh + --build-type TYPE release (default) or debug + +Environment: + CUDA_INSTALL_PATH CUDA toolkit to build against (default /usr/local/cuda) + JOBS compile jobs (default 4; host budget cap) + GPGPUSIM_BUILD_TYPE release (default) or debug + GPGPUSIM_CLONE_SOURCE explicit local mirror path or URL (unset by default; + the upstream URL is always the default source) + +Idempotent; fails loudly; never uses sudo. +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + --no-build) BUILD=0 ;; + --reclone) RECLONE=1 ;; + --build-type) BUILD_TYPE="${2:-}"; shift ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; exit 2 ;; + esac + shift +done + +case "$BUILD_TYPE" in + release|debug) ;; + *) die "unknown build type '$BUILD_TYPE' (use release or debug)" ;; +esac + +if [ "$RECLONE" = 1 ] && [ -e "$UPSTREAM" ]; then + echo "== --reclone: removing $UPSTREAM" + rm -rf "$UPSTREAM" +fi + +# --------------------------------------------------------------------------- +# 1. upstream checkout at the pinned commit +# --------------------------------------------------------------------------- +if [ -d "$UPSTREAM/.git" ]; then + echo "== upstream checkout present: $UPSTREAM" + head_commit="$(git -C "$UPSTREAM" rev-parse HEAD)" + if [ "$head_commit" != "$PIN" ]; then + if [ -n "$(git -C "$UPSTREAM" status --porcelain)" ]; then + die "upstream is at $head_commit (expected $PIN) and has local changes; --reclone to reset" + fi + echo "== checking out pinned commit $PIN" + git -C "$UPSTREAM" checkout --quiet --detach "$PIN" \ + || die "pinned commit $PIN not present locally; run with --reclone" + else + echo "== upstream already at pinned commit ($PIN)" + fi +elif [ -e "$UPSTREAM" ]; then + die "$UPSTREAM exists but is not a git checkout; use --reclone" +else + # Default source is the upstream URL. A local mirror is used only when + # GPGPUSIM_CLONE_SOURCE is set explicitly; nothing on this host is guessed. + if [ -n "${GPGPUSIM_CLONE_SOURCE:-}" ]; then + src="$GPGPUSIM_CLONE_SOURCE" + echo "== using explicit clone source from GPGPUSIM_CLONE_SOURCE: $src" + else + src="$UPSTREAM_URL" + echo "== using upstream URL: $UPSTREAM_URL" + fi + git clone --quiet "$src" "$UPSTREAM" || die "git clone failed from $src" + git -C "$UPSTREAM" checkout --quiet --detach "$PIN" \ + || die "pinned commit $PIN not found in clone from $src" +fi + +# --------------------------------------------------------------------------- +# 2. overlay patches (lexical order; empty directory is tolerated) +# --------------------------------------------------------------------------- +shopt -s nullglob +patches=("$PATCH_DIR"/*.patch) +shopt -u nullglob + +if [ "${#patches[@]}" -eq 0 ]; then + echo "== patches/: empty (Phase 0; the CUDA 12 port patch set lands here)" +else + for p in "${patches[@]}"; do + name="$(basename "$p")" + if git -C "$UPSTREAM" apply --check "$p" >/dev/null 2>&1; then + echo "== applying patch $name" + git -C "$UPSTREAM" apply "$p" || die "failed to apply $name" + elif git -C "$UPSTREAM" apply --reverse --check "$p" >/dev/null 2>&1; then + echo "== patch already applied: $name" + else + die "patch neither applies cleanly nor is already applied: $name (resolve or --reclone)" + fi + done +fi + +# --------------------------------------------------------------------------- +# 3. build (skipped with --no-build) +# --------------------------------------------------------------------------- +if [ "$BUILD" = 0 ]; then + echo "== --no-build: checkout + patches complete, build skipped" + exit 0 +fi + +cuda="${CUDA_INSTALL_PATH:-/usr/local/cuda}" +[ -d "$cuda" ] || die "CUDA_INSTALL_PATH=$cuda does not exist (set the env var to a CUDA toolkit)" +[ -x "$cuda/bin/nvcc" ] || die "no executable $cuda/bin/nvcc (set CUDA_INSTALL_PATH to a CUDA toolkit)" + +mkdir -p "$BUILD_DIR" +LOG="$BUILD_DIR/build.log" +echo "== building GPGPU-Sim ($BUILD_TYPE, -j$JOBS, CUDA_INSTALL_PATH=$cuda)" +echo "== build log: $LOG" + +( + cd "$UPSTREAM" + export CUDA_INSTALL_PATH="$cuda" + export PATH="$cuda/bin:$PATH" + set +u + # shellcheck disable=SC1091 + source ./setup_environment "$BUILD_TYPE" + set -u + if [ -z "${GPGPUSIM_CONFIG:-}" ]; then + echo "setup.sh: ERROR: setup_environment did not configure a build for CUDA at $cuda." >&2 + echo " This host's CUDA may not be ported yet (WP0.1); see UPSTREAM.md." >&2 + exit 1 + fi + make -j"$JOBS" +) 2>&1 | tee "$LOG" + +echo "== setup complete" +echo "== upstream: $UPSTREAM" +echo "== runtime libs: $UPSTREAM/lib/<gcc>/<cuda>/$BUILD_TYPE" +echo "== next: ./scripts/run_sim.sh <cuda-binary> [args...]" diff --git a/debug/risc-v-gsp-rm-vm/greeneye/inject.py b/debug/risc-v-gsp-rm-vm/greeneye/inject.py index eb4b16a..fc4d5c0 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/inject.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/inject.py @@ -20,6 +20,9 @@ from .errors import GreenEyeError #: Symbol the injector calls in a service blob. ENTRY_SYMBOL = "ge_demo_service_dispatch" +#: Session attribute holding the per-session service registry (load order). +SERVICE_REGISTRY_ATTR = "_injected_services" + #: Demo uproc control-block field offsets (see firmware/uproc_demo). CTRL_HANDLERS = 0x150 CTRL_MAILBOX = 0x20 @@ -46,6 +49,36 @@ class InjectedService: f"entry {self.entry_addr:#x} (offset {self.dispatch_offset:#x}), slot {self.slot}" ) + def to_dict(self) -> dict: + """Plain-data view for MCP/reporting (no behavior).""" + return { + "path": self.path, + "name": self.name, + "load_addr": self.load_addr, + "entry_addr": self.entry_addr, + "dispatch_offset": self.dispatch_offset, + "slot": self.slot, + "size": self.size, + } + + +def services(session) -> list["InjectedService"]: + """Services loaded into ``session`` this process, in load order. + + The registry lives on the session object (``SERVICE_REGISTRY_ATTR``) and is + populated by :func:`load_service`; it is process-local and not part of the + on-disk session format. + """ + return list(getattr(session, SERVICE_REGISTRY_ATTR, ()) or ()) + + +def _register(session, service: InjectedService) -> None: + registry = getattr(session, SERVICE_REGISTRY_ATTR, None) + if registry is None: + registry = [] + setattr(session, SERVICE_REGISTRY_ATTR, registry) + registry.append(service) + def load_service( session, @@ -110,6 +143,7 @@ def load_service( slot=slot, size=total, ) + _register(session, service) return service diff --git a/debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py b/debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py index a2d2c6a..852536e 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py @@ -313,6 +313,7 @@ def launch_native( *, kernel: str | None = None, memory_mb: int = 256, + ram_base: int = 0x80000000, record=None, symbols: SymbolDB | None = None, name: str = "session", @@ -324,6 +325,7 @@ def launch_native( unknown_mmio: str = "fault", learn_mmio: bool = False, bias: int = 0, + cycle_model=None, ) -> Session: """Boot the native simulator and return a ready Session. @@ -340,6 +342,11 @@ def launch_native( ``learn_mmio=True`` writes the observed values back into the map. ``bias`` relocates PT_LOAD segments and the entry for images linked for GPU physical addresses (e.g. ``0x80000000`` for the real GSP fwimage). + + ``cycle_model`` selects the modeled instruction-class cycle accounting + (:mod:`sim.cycles`): ``None`` (default) uses the default table, ``False`` + disables it (``machine.cycles`` stays 0), or pass a ``CycleModel``. The + model never changes execution. """ try: tracer = make_trace(trace) @@ -353,14 +360,17 @@ def launch_native( device_map = DeviceMap.load(os.fspath(device_map)) machine = Machine( memory_mb=memory_mb, + ram_base=ram_base, device_map=device_map, unknown_mmio=unknown_mmio, learn_mmio=learn_mmio, bias=bias, + cycle_model=cycle_model, ) target = SimTarget( machine, memory_mb=memory_mb, + ram_base=ram_base, sim_ips=sim_ips, keep_alive=keep_alive, trace=tracer, diff --git a/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.json b/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.json new file mode 100644 index 0000000..4df3593 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.json @@ -0,0 +1,289 @@ +{ + "above_qemu_line": true, + "features": { + "features": { + "analyze_compare": { + "detail": { + "analyze_chars": 444, + "analyze_session": "harness/qemu_line/sessions/native-1", + "compare_chars": 526, + "compare_headers": [ + "Breakpoint profile delta (A vs B)", + "Watchpoint delta", + "Written pages delta" + ], + "compare_pair": [ + "harness/qemu_line/sessions/native-1", + "harness/qemu_line/sessions/native-2" + ] + }, + "status": "pass" + }, + "breakpoints": { + "detail": { + "attached_then_removed": true, + "kind": "sw", + "run_until_hit": 2147483912 + }, + "status": "pass" + }, + "hooks": { + "detail": { + "cmds": [ + 1, + 2 + ], + "rpc_dispatch_hits": 1 + }, + "status": "pass" + }, + "injection": { + "detail": { + "blob": "sdk/build/injected_demo.elf", + "dir": "harness/qemu_line/sessions/features-inject", + "expected": 48814, + "hashes": { + "events.jsonl": "08ab0874cfad71e0e4565b569d9b0ea5251bf158eaab3cdf17798182e4c66a95", + "session.json": "f34b677be0b0b453cdade4487118e059c3dfb5a1010d4e6c2fc8b37a867fb25e", + "summary.md": "59a1aa4f2b616d5266d565c4df4b0257d11aab95b09cec80f380e9aca8bfc22f" + }, + "ping_result": 48814, + "slot": 6 + }, + "status": "pass" + }, + "mcp": { + "reason": "native MCP surface is S3's deliverable (plugins/mcp_server.py); S4 must not race that work. Re-run after S3 lands (docs/37).", + "status": "not_run" + }, + "memory": { + "detail": { + "ctrl": 2147485760, + "magic": 1196642353 + }, + "status": "pass" + }, + "sessions": { + "detail": { + "dir": "harness/qemu_line/sessions/features", + "event_counts": { + "bp_hit": 2, + "hook": 1, + "image_loaded": 1, + "mem_read": 2, + "mem_write": 4, + "session_end": 1, + "session_start": 1, + "stop": 2, + "watch": 1, + "watch_hit": 1 + }, + "hashes": { + "events.jsonl": "a6328cb00287bdf678d9b62d496929fc59190a08af3910b059c840c6bc98462e", + "session.json": "2372eea627f31d11f114019759d8d6dc665683bbc88e834f158f56b16fa5c0d4", + "summary.md": "6dfca9e61a4f23374b90dbef995d86771130fcf4c2b61dc40aac6d379d4b8706" + } + }, + "status": "pass" + }, + "struct": { + "detail": { + "fields": 14, + "schema": "gsp_ctrl.schema.json" + }, + "status": "pass" + }, + "trace": { + "detail": { + "blob": "harness/qemu_line/sessions/features-trace/blobs/trace.jsonl", + "dir": "harness/qemu_line/sessions/features-trace", + "events": 4001, + "hashes": { + "events.jsonl": "36b80ed88205daa8e1bd58536eb274ed4849c3d4b1a05daa0f2c44382c7b764a", + "session.json": "a09ec38202c291a5b7a3719434ea785725e291cba2dc877d94cd0caf7fcd1a4e", + "summary.md": "10686e9f247862f4b0f2c68a2353821f4039befdd76846f8daefa72ae79f4b44" + }, + "max_events": 4000 + }, + "status": "pass" + }, + "watch": { + "detail": { + "pcs": [ + 2147484066 + ], + "result_field_stores": 1 + }, + "status": "pass" + } + }, + "ran": true, + "reason": "ok" + }, + "firmware": { + "name": "gsp-570.144.fwimage.bin", + "sha256": "2d1bb5c60b208cfd115f015b17dd04a6fece05fd2c0dcba3625d9d48cce3ceea" + }, + "harness": "harness/sim_qemu_line.py", + "line_definition": "docs/35-GREENEYE-SIMULATOR-DESIGN.md 1.1 + 3.3", + "native": { + "backend": "native", + "compare": { + "chars": 526, + "headers": [ + "Breakpoint profile delta (A vs B)", + "Watchpoint delta", + "Written pages delta" + ], + "ran": true + }, + "device_model_used": true, + "identical": true, + "instructions_budget": 200000, + "pair_wall_s": 0.3093, + "profile": "sim/profiles/tu104-570.144.json", + "ran": true, + "reason": "ok", + "repeat": 2, + "rung": "B2", + "rung_basis": "observed S1 boot-ladder rung from sim.gsp_boot (milestones=['bootargs_consumer', 'relocation_copy'], mmio_unknown=0, device_unhandled=0)", + "runs": [ + { + "cycle_model": "rv64imac-modeled/1", + "cycles": 27294, + "device_accesses": 1, + "device_state_sha256": "3fa264b9f81081e8455a9cd26c0c2a721e3ac272e1079105a0eb191b1497f83d", + "device_unhandled": 0, + "first_unknown_mmio": null, + "instructions": 14682, + "milestones": [ + "bootargs_consumer", + "relocation_copy" + ], + "mmio_unknown": 0, + "rung": "B2", + "rung_basis": "observed S1 boot-ladder rung from sim.gsp_boot (milestones=['bootargs_consumer', 'relocation_copy'], mmio_unknown=0, device_unhandled=0)", + "session": "harness/qemu_line/sessions/native-1", + "session_hashes": { + "events.jsonl": "f1f336af84b750b0c8bf8f8e9a7a3952161eb884418f5580f08cb3e26a070713", + "session.json": "c756f8078812aa1ed03628e097ee517320d7de3664089f1133861c96130d5b5b", + "summary.md": "c0aa5d5b2cb306fda703a7144af455c36fe72b9d1e2c73ddcdab905d36cdc8a7" + }, + "stop": { + "pc": 67168780, + "pc_fw": 67168780, + "reason": "fault" + }, + "wall_s": 0.1561 + }, + { + "cycle_model": "rv64imac-modeled/1", + "cycles": 27294, + "device_accesses": 1, + "device_state_sha256": "3fa264b9f81081e8455a9cd26c0c2a721e3ac272e1079105a0eb191b1497f83d", + "device_unhandled": 0, + "first_unknown_mmio": null, + "instructions": 14682, + "milestones": [ + "bootargs_consumer", + "relocation_copy" + ], + "mmio_unknown": 0, + "rung": "B2", + "rung_basis": "observed S1 boot-ladder rung from sim.gsp_boot (milestones=['bootargs_consumer', 'relocation_copy'], mmio_unknown=0, device_unhandled=0)", + "session": "harness/qemu_line/sessions/native-2", + "session_hashes": { + "events.jsonl": "d0d15816dbde8c85c30ba2576f97c9d5e3f470e6c38d9b4762d266a612cafe12", + "session.json": "8c7f0ea958a930edb814690bc0b233be01fcab3f747332abff39989e66642c2e", + "summary.md": "d2d6154ec22f4ee0d942543a32c29699ccb8031188df955c415d836462d04fd3" + }, + "stop": { + "pc": 67168780, + "pc_fw": 67168780, + "reason": "fault" + }, + "wall_s": 0.1524 + } + ] + }, + "not_verified": [ + "lenient MMIO (B0) is observation, not device semantics", + "B3/B4 are not reached: the S1 device model stops at the B2 DMEM-runspace unknown", + "the boot shim values (bootargs/WPR meta/DMEM mirror) are declared assumptions", + "MCP on native is S3-owned and not exercised here", + "L2 timing: cycles are modeled (docs/32), never silicon claims", + "no hardware/GPU/guest/module involvement by design" + ], + "qemu": { + "backend": "qemu", + "device_map_entries": 2, + "fault_count_window": 5269, + "first_fault": { + "access_addr": 2305843009213696512, + "cause": 5, + "desc": "fault_load", + "epc": 2214629396, + "epc_fw": 67145748, + "reason": "rejected", + "rw": "read", + "size": 4 + }, + "first_touch_match": true, + "log_window_capped": true, + "ran": true, + "reason": "ok", + "rung": "pre-B0", + "session": "harness/qemu_line/sessions/qemu-probe", + "session_hashes": { + "events.jsonl": "cdba69c63e306f4c3de220b171e2ea7c1de4bd4abf18cdba1fb51bc681d5d172", + "session.json": "7e3f0f8b24c0403d114e59f68ed1b923e44abbfbf19f4865bd566a1b631c64ac", + "summary.md": "d141fa06d3e2d0603f3b99ad7603758e1fc14627aed74c6fe8a6ad22c56eb14b" + }, + "stopped_pc": 0, + "wall_s": 0.9519 + }, + "rerun_when_s1_lands": [ + "python3 harness/sim_qemu_line.py", + "S1 landed (2026-10-03): sim/profiles/*.json is discovered and the native side runs sim.gsp_boot", + "sim/devices/*.json with greeneye-device-map/1 (or a device_map key) is still picked up as an observational map" + ], + "rung_order": [ + "pre-B0", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5" + ], + "rung_progress": true, + "s1_integration": { + "declared_rung": null, + "notes": [ + "using sim/profiles/tu104-570.144.json (greeneye-sim-profile/1, firmware_sha256 matched)" + ], + "profile": "sim/profiles/tu104-570.144.json", + "source": "sim/profiles/tu104-570.144.json" + }, + "schema": "greeneye-sim-qemu-line/1", + "threshold_checks": { + "all": true, + "device_model_documented": true, + "identical_runs": true, + "native_above_qemu": true, + "native_pair_wall_s": true, + "qemu_first_fault_reproduced": true + }, + "thresholds": { + "device_model_required": true, + "frozen_on": "2026-10-03", + "identical_runs": 2, + "native_pair_wall_s": 20.0, + "native_rungs_above_qemu": 1, + "qemu_first_fault_fw": 67145748, + "qemu_first_touch": 2305843009213696512 + }, + "verdict": "above", + "verdict_explanation": "all frozen checks passed: QEMU fault reproduced, native >= QEMU + 1 rung, identical runs, wall bound, documented device model", + "x86_vms_running": 0 +} diff --git a/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.md b/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.md new file mode 100644 index 0000000..4e546b3 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.md @@ -0,0 +1,102 @@ +# Simulator vs the QEMU line - measured report + +* schema: `greeneye-sim-qemu-line/1` +* harness: `harness/sim_qemu_line.py` +* line: docs/35-GREENEYE-SIMULATOR-DESIGN.md 1.1 + 3.3 +* firmware: `gsp-570.144.fwimage.bin` sha256 `2d1bb5c60b208cfd...` +* verdict: **above** (above_qemu_line=True) +* verdict meaning: all frozen checks passed: QEMU fault reproduced, native >= QEMU + 1 rung, identical runs, wall bound, documented device model + +## Rung table + +| side | ran | rung | instructions | cycles (modeled) | stop | wall s | evidence | +|---|---|---|---:|---:|---|---:|---| +| QEMU probe | True | pre-B0 | - | - | fault | 0.9519 | `harness/qemu_line/sessions/qemu-probe` | +| native run 1 | True | B2 | 14682 | 27294 | fault@0x400ea0c | 0.1561 | `harness/qemu_line/sessions/native-1` | +| native run 2 | True | B2 | 14682 | 27294 | fault@0x400ea0c | 0.1524 | `harness/qemu_line/sessions/native-2` | + +### QEMU first fault + +* fault_load at fw vaddr `0x4009014` accessing `0x2000000000000a00` (4 bytes, read) +* first-touch matches the documented reference: True +* fault records in the capped log window: 5269 + +## Frozen thresholds + +| check | pass | +|---|---| +| qemu_first_fault_reproduced | True | +| native_above_qemu | True | +| identical_runs | True | +| native_pair_wall_s | True | +| device_model_documented | True | +| all | True | + +Thresholds (frozen 2026-10-03): native >= QEMU + 1 rung(s); 2 identical runs; pair wall <= 20.0 s. + +## Determinism + +* identical across 2 native runs: True +* session compare ran: True (['Breakpoint profile delta (A vs B)', 'Watchpoint delta', 'Written pages delta']) + +## Session hashes (sha256) + +| session | file | sha256 | +|---|---|---| +| qemu-probe | events.jsonl | `cdba69c63e306f4c3de220b171e2ea7c1de4bd4abf18cdba1fb51bc681d5d172` | +| qemu-probe | session.json | `7e3f0f8b24c0403d114e59f68ed1b923e44abbfbf19f4865bd566a1b631c64ac` | +| qemu-probe | summary.md | `d141fa06d3e2d0603f3b99ad7603758e1fc14627aed74c6fe8a6ad22c56eb14b` | +| native-1 | events.jsonl | `f1f336af84b750b0c8bf8f8e9a7a3952161eb884418f5580f08cb3e26a070713` | +| native-1 | session.json | `c756f8078812aa1ed03628e097ee517320d7de3664089f1133861c96130d5b5b` | +| native-1 | summary.md | `c0aa5d5b2cb306fda703a7144af455c36fe72b9d1e2c73ddcdab905d36cdc8a7` | +| native-2 | events.jsonl | `d0d15816dbde8c85c30ba2576f97c9d5e3f470e6c38d9b4762d266a612cafe12` | +| native-2 | session.json | `8c7f0ea958a930edb814690bc0b233be01fcab3f747332abff39989e66642c2e` | +| native-2 | summary.md | `d2d6154ec22f4ee0d942543a32c29699ccb8031188df955c415d836462d04fd3` | +| sessions | events.jsonl | `a6328cb00287bdf678d9b62d496929fc59190a08af3910b059c840c6bc98462e` | +| sessions | session.json | `2372eea627f31d11f114019759d8d6dc665683bbc88e834f158f56b16fa5c0d4` | +| sessions | summary.md | `6dfca9e61a4f23374b90dbef995d86771130fcf4c2b61dc40aac6d379d4b8706` | +| trace | events.jsonl | `36b80ed88205daa8e1bd58536eb274ed4849c3d4b1a05daa0f2c44382c7b764a` | +| trace | session.json | `a09ec38202c291a5b7a3719434ea785725e291cba2dc877d94cd0caf7fcd1a4e` | +| trace | summary.md | `10686e9f247862f4b0f2c68a2353821f4039befdd76846f8daefa72ae79f4b44` | +| injection | events.jsonl | `08ab0874cfad71e0e4565b569d9b0ea5251bf158eaab3cdf17798182e4c66a95` | +| injection | session.json | `f34b677be0b0b453cdade4487118e059c3dfb5a1010d4e6c2fc8b37a867fb25e` | +| injection | summary.md | `59a1aa4f2b616d5266d565c4df4b0257d11aab95b09cec80f380e9aca8bfc22f` | + +## Native feature matrix (demo firmware) + +| feature | status | detail | +|---|---|---| +| analyze_compare | pass | {'analyze_session': 'harness/qemu_line/sessions/native-1', 'analyze_chars': 444, 'compare_pair': ['harness/qemu_line/sessions/native-1', 'harness/qemu_line/sessions/native-2'], 'compare_chars': 526, 'compare_headers': ['Breakpoint profile delta (A vs B)', 'Watchpoint delta', 'Written pages delta']} | +| breakpoints | pass | {'run_until_hit': 2147483912, 'attached_then_removed': True, 'kind': 'sw'} | +| hooks | pass | {'rpc_dispatch_hits': 1, 'cmds': [1, 2]} | +| injection | pass | {'dir': 'harness/qemu_line/sessions/features-inject', 'blob': 'sdk/build/injected_demo.elf', 'slot': 6, 'ping_result': 48814, 'expected': 48814, 'hashes': {'session.json': 'f34b677be0b0b453cdade4487118e059c3dfb5a1010d4e6c2fc8b37a867fb25e', 'events.jsonl': '08ab0874cfad71e0e4565b569d9b0ea5251bf158eaab3cdf17798182e4c66a95', 'summary.md': '59a1aa4f2b616d5266d565c4df4b0257d11aab95b09cec80f380e9aca8bfc22f'}} | +| mcp | not_run | native MCP surface is S3's deliverable (plugins/mcp_server.py); S4 must not race that work. Re-run after S3 lands (docs/37). | +| memory | pass | {'ctrl': 2147485760, 'magic': 1196642353} | +| sessions | pass | {'dir': 'harness/qemu_line/sessions/features', 'event_counts': {'bp_hit': 2, 'hook': 1, 'image_loaded': 1, 'mem_read': 2, 'mem_write': 4, 'session_end': 1, 'session_start': 1, 'stop': 2, 'watch': 1, 'watch_hit': 1}, 'hashes': {'session.json': '2372eea627f31d11f114019759d8d6dc665683bbc88e834f158f56b16fa5c0d4', 'events.jsonl': 'a6328cb00287bdf678d9b62d496929fc59190a08af3910b059c840c6bc98462e', 'summary.md': '6dfca9e61a4f23374b90dbef995d86771130fcf4c2b61dc40aac6d379d4b8706'}} | +| struct | pass | {'schema': 'gsp_ctrl.schema.json', 'fields': 14} | +| trace | pass | {'dir': 'harness/qemu_line/sessions/features-trace', 'blob': 'harness/qemu_line/sessions/features-trace/blobs/trace.jsonl', 'events': 4001, 'max_events': 4000, 'hashes': {'session.json': 'a09ec38202c291a5b7a3719434ea785725e291cba2dc877d94cd0caf7fcd1a4e', 'events.jsonl': '36b80ed88205daa8e1bd58536eb274ed4849c3d4b1a05daa0f2c44382c7b764a', 'summary.md': '10686e9f247862f4b0f2c68a2353821f4039befdd76846f8daefa72ae79f4b44'}} | +| watch | pass | {'result_field_stores': 1, 'pcs': [2147484066]} | + +## S1 integration + +* device source: sim/profiles/tu104-570.144.json +* profile: sim/profiles/tu104-570.144.json +* declared rung: None +* note: using sim/profiles/tu104-570.144.json (greeneye-sim-profile/1, firmware_sha256 matched) + +## Not verified + +* lenient MMIO (B0) is observation, not device semantics +* B3/B4 are not reached: the S1 device model stops at the B2 DMEM-runspace unknown +* the boot shim values (bootargs/WPR meta/DMEM mirror) are declared assumptions +* MCP on native is S3-owned and not exercised here +* L2 timing: cycles are modeled (docs/32), never silicon claims +* no hardware/GPU/guest/module involvement by design + +## S1 integration notes + +* `python3 harness/sim_qemu_line.py` +* S1 landed (2026-10-03): sim/profiles/*.json is discovered and the native side runs sim.gsp_boot +* sim/devices/*.json with greeneye-device-map/1 (or a device_map key) is still picked up as an observational map + +* other agents' x86 QEMU VMs observed (never touched): 0 diff --git a/debug/risc-v-gsp-rm-vm/harness/re-demo-report.md b/debug/risc-v-gsp-rm-vm/harness/re-demo-report.md new file mode 100644 index 0000000..aa7a116 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/re-demo-report.md @@ -0,0 +1,116 @@ +# Real RE demo - what the GSP does between entry and the first fault + +* schema: `greeneye-re-demo/1` +* script: `harness/re_demo.py` (stdlib-only, no hardware/guest/module) +* firmware: `gsp-570.144.fwimage.bin` sha256 `2d1bb5c60b208cfd...` +* verdict: **PASS** + +## Scenario + +> What happens on the real GSP image between entry and the first fault/milestone, and what does the state tell us about boot? + +## Answer + +**q1_first_touch: the first MMIO is a 4-byte read of PA 0x2000000000000a00 at fw PC 0x4009014; it is the QEMU-documented first fault** + +**q2_path: entry state is all-zero (no bootloader handoff modelled); the bootstrap builds the device PA with 5 arithmetic instructions (li/slli/addi/slli/addi), reads it, then runs the chip-ID comparison chain: 25 instructions recorded from entry until the halt at fw 0x4009070** + +**q3_id_field: the read value's bits [28:20] are extracted (srli 20, andi 0x1ff) and compared against 354..360 (Turing) and 368 (Ampere); the image itself defines this as a chip-ID field** + +**q4_dispatch: ID 354..360 dispatches to .tu10x_text at 0x4007000; ID 368 dispatches to .ga100_text at 0x4005000; any other value (including the lenient zero) falls to the fail-closed `j .` loop at 0x4009070** + +**q5_next_blocker: with a Turing ID served, .tu10x_text programs CSRs 0x7c8-0x7cc (region descriptors) and enables mstatus FP/SIMD state, then builds the absolute link-time address 0x40070f8 and jumps to it; because the loader relocates segments but not absolute immediates, that jump lands outside RAM and faults - a relocation artifact, so the next rung needs a chip-ID device model plus load-at-link-address/alias translation** + +## Baseline vs emulator (measured) + +| side | method | wall s | what it answers | +|---|---|---:|---| +| baseline | `python3 -m greeneye.cli probe <image> --duration 1.0 (here run via harness.run_qemu_baseline, 0.5 s bound)` | 0.9495 | first fault only | +| baseline | manual static reading (estimate, floor estimate: ~90 instructions across .bootstrap + .tu10x_text + .ga100_text read by hand, dispatch table identified and correlated with the QEMU first-fault report; not measured in this run) | 600.0 | entry->fault path, by hand | +| baseline | total (conservative) | 600.9495 | full question | +| emulator | three recorded native sessions | 0.1251 | full question + evidence | + +Per-session `wall_s` in the milestone table is the recorded run only; the emulator total includes session launch, repeat and report writing. + +## Rung / milestone table + +| run | first touch | stop/milestone | instructions | wall s | evidence | +|---|---|---|---:|---:|---| +| QEMU probe | 0x2000000000000a00 (fault) | fault, then mtvec=0 loop | - | 0.9495 | `harness/re_demo/sessions/qemu-probe` | +| native lenient (B0) | 0x2000000000000a00 | halt loop fw 0x4009070 | 25 | 0.0006 | `harness/re_demo/sessions/native-lenient` | +| native modeled chip-ID (B1 candidate) | 0x2000000000000a00 | instruction fault at fw 0x40070f8 (outside RAM) | 74 | 0.0004 | `harness/re_demo/sessions/native-modeled-id` | + +## Frozen thresholds + +| check | pass | +|---|---| +| answers_complete | True | +| deterministic_lenient_runs | True | +| emulator_wall_s | True | +| milestone_past_qemu | True | +| baseline_beat | True | +| pass | True | + +Thresholds frozen 2026-10-03 before the run: emulator wall <= 120.0 s; 2 identical lenient runs; all 5 answers present; milestone past the QEMU point. Failure signal: any check false -> the run is a failed experiment and the feature is not claimed. + +## Sessions (evidence) + +| session | file | sha256 | +|---|---|---| +| qemu-probe | events.jsonl | `8c4dba8006a496d853c81ce9e2b16944cd27b8d6171c01777daa6e5738be02bc` | +| qemu-probe | session.json | `c7bf5e4d29f534160d660dadb81f81942faa17cbfb1941587c60dcf78180f56c` | +| qemu-probe | summary.md | `d141fa06d3e2d0603f3b99ad7603758e1fc14627aed74c6fe8a6ad22c56eb14b` | +| native-lenient | events.jsonl | `ccb568588738c0609071ee542c9b04f7bb2868ee8fae12c28b2bfc5d9343721b` | +| native-lenient | session.json | `ec5944adce4b823a3d6fd7d5aa6385fb5b61c2f9542c7221f46f328601c12c66` | +| native-lenient | summary.md | `b50820afa60c35da0d4701e54f9907d4b7090c52721e1ecf656942a41bcf44db` | +| native-lenient-repeat | events.jsonl | `2cd28b4da009447d70505cfbba59e9c1bf8bd277e3a7ea696e2690f54f66f756` | +| native-lenient-repeat | session.json | `fe21ed52c05436d0f7886dfeb0886ec1a11b25678e4d827db3b9e4009d2187e4` | +| native-lenient-repeat | summary.md | `357d95a664e33128a2a48da7ecfc058bdbbea56fae130557593fd59ff2efa597` | +| native-modeled-id | events.jsonl | `fdf0756c3543741b74c9267dc9e4d22156f43a4b54463ffbfb1671c327444515` | +| native-modeled-id | session.json | `5ae7eeff56a19ed7eea093a164a3a3738965b56c39f35a563db3ee93a2c90d39` | +| native-modeled-id | summary.md | `bd670d207d23079173b494a9be9f9a0e7d31a10a11f9dc48fb8a134888442d0b` | + +## Disassembly of the entry prologue (from the lenient session memory read) + +``` +0x84009000: 0010029b addiw t0,zero,1 +0x84009004: 03129293 slli t0,t0,49 +0x84009008: 00128293 addi t0,t0,1 +0x8400900c: 00c29293 slli t0,t0,12 +0x84009010: a0028293 addi t0,t0,-1536 +0x84009014: 0002a283 lw t0,0(t0) +0x84009018: 0142d293 srli t0,t0,20 +0x8400901c: 1ff2f293 andi t0,t0,511 +``` + +### Turing dispatch tail (from the modeled session memory read) + +``` +0x840070e4: 1e0002b7 lui t0,0x1e000 +0x840070e8: 3002a073 csrs mstatus,t0 +0x840070ec: 040072b7 lui t0,0x4007 +0x840070f0: 0f828293 addi t0,t0,248 +0x840070f4: 00028067 jalr zero,0(t0) +``` + +## Not verified (honest limits) + +* the modeled chip-ID value is an assumption (the QEMU baseline faults before reading it); only the field position and the compared IDs are firmware evidence +* no timing/cycle claim: cycles are modeled (L2 not modelled, docs/06) and none are used in this answer +* L3 device environment is absent: the register's on-silicon semantics are inferred from firmware comparisons, not from a device model +* load-at-link-address and absolute-immediate relocation behaviour are not implemented; the B1 candidate fault is a relocation artifact of this harness +* watchpoints are not exercised on this path: the bootstrap performs no RAM data access before the fault (RAM watchpoints are exercised by harness/sim_qemu_line.py on the demo firmware) +* no hardware, GPU, mdev, module, guest or network involvement by design + +## Reproduce + +```bash +cd open-iov/debug/risc-v-gsp-rm-vm +python3 harness/re_demo.py +``` + +## Re-run contract + +* thresholds live in harness/re_demo.py (FROZEN_THRESHOLDS) and are not moved after the run +* the QEMU baseline needs qemu-system-riscv64; without it the emulator columns still reproduce and the baseline is marked unverified +* sessions are under harness/re_demo/sessions/ (hash-stamped); the report regenerates with the same command diff --git a/debug/risc-v-gsp-rm-vm/harness/re_demo.py b/debug/risc-v-gsp-rm-vm/harness/re_demo.py new file mode 100644 index 0000000..3f9c317 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/re_demo.py @@ -0,0 +1,832 @@ +#!/usr/bin/env python3 +"""Real reverse-engineering demo: GSP entry -> first fault/milestone (U51). + +Answers one concrete question on the real TU104 570.144 image with the +simulator's own primitives (``launch_native`` session, hooks, memory reads, +snapshots, exec trace, recorded sessions): + + What happens on the real GSP image between entry and the first + fault/milestone, and what does the state tell us about boot? + +Method (both sides measured, thresholds frozen in this file before any run): + +* **baseline** - the QEMU ``probe`` path (bounded 0.5 s, measured) plus a + conservative manual static-reading estimate (``MANUAL_ESTIMATE_S``; the + method note says how the estimate is derived). QEMU reports the first + fault; the "what happens between entry and the fault" part is hand work + in a disassembler. +* **emulator** - three recorded native sessions: (A) lenient MMIO (B0), which + reproduces the QEMU first touch and follows the firmware to its fail-closed + halt; (A') the same run repeated for determinism; (B) the same run with the + chip-ID register served a Turing value from a one-entry device map, which + advances past the QEMU point to the next (relocation) blocker. + +Everything is stdlib-only, bounded (<= 2M instructions, <= 10 s QEMU), and +touches no hardware, GPU, guest, module or network. This is simulator +evidence at L0/L1 (docs/06): no timing or silicon claim. + +CLI:: + + python3 harness/re_demo.py + python3 harness/re_demo.py --json + python3 harness/re_demo.py --out harness/re_demo --report harness/re-demo-report.md +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOL_ROOT = os.path.dirname(HERE) +if TOOL_ROOT not in sys.path: + sys.path.insert(0, TOOL_ROOT) + +from greeneye.device_map import DeviceEntry, DeviceMap, QEMU_REFERENCE # noqa: E402 +from greeneye.sim_backend import launch_native # noqa: E402 +from harness.sim_qemu_line import run_qemu_baseline, session_hashes # noqa: E402 +from sim.decode import ( # noqa: E402 + K_AUIPC, + K_BRANCH, + K_CSR, + K_JAL, + K_JALR, + K_LOAD, + K_LUI, + K_OP, + K_OP32, + K_OPI, + K_OPI32, + decode16, + decode32, +) + +DEFAULT_IMAGE = os.path.normpath( + os.path.join( + TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", "gsp-570.144.fwimage.bin" + ) +) +DEFAULT_OUT = os.path.join(HERE, "re_demo") +DEFAULT_REPORT = os.path.join(HERE, "re-demo-report.md") + +#: GPU physical addresses are relocated into simulator RAM at this bias +#: (same convention as the probe/device-map/gsp-profile paths). +BIAS = 0x80000000 + +ENTRY = 0x4009000 +FIRST_TOUCH_PC = 0x4009014 +HALT_PC = 0x4009070 +TU10X_ENTRY = 0x4007000 +GA100_ENTRY = 0x4005000 +TU10X_ABS_TARGET = 0x40070F8 + +#: The chip-ID field is bits [28:20] of the register read at the first touch; +#: these are the comparison values the image itself uses (session A trace). +TU10X_IDS = (354, 355, 356, 357, 358, 359, 360) +GA100_ID = 368 +#: Exploratory modeled register value: Turing TU10x ID in the field position. +#: This is an *assumption* chosen to exercise the dispatch path, not an +#: observed hardware value (the QEMU baseline faults before reading it). +MODELED_ID = TU10X_IDS[0] << 20 + +#: Frozen thresholds - recorded 2026-10-03, before the first measurement. +#: Never move silently; if a threshold changes, date the change in docs/37. +FROZEN_THRESHOLDS = { + "frozen_on": "2026-10-03", + "emulator_wall_s_max": 120.0, + "manual_static_estimate_s": 600.0, + "answers_required": 5, + "identical_lenient_runs": 2, + "milestone_past_qemu": True, +} + +#: How the manual estimate is derived (a floor, not a ceiling): read the +#: 6-instruction .bootstrap prologue + the two per-architecture text sections +#: (~90 instructions), identify the pointer construction, the chip-ID bit +#: field, the two dispatch targets and the halt path, then correlate against +#: the QEMU fault report. No human fast path is measured; 10 minutes is the +#: conservative floor for one engineer with the artifacts already open. +MANUAL_METHOD_NOTE = ( + "floor estimate: ~90 instructions across .bootstrap + .tu10x_text + " + ".ga100_text read by hand, dispatch table identified and correlated " + "with the QEMU first-fault report; not measured in this run" +) + + +def _relpath(path: str | None) -> str | None: + if path is None: + return None + return os.path.relpath(os.path.abspath(path), TOOL_ROOT) + + +def _sha256_file(path: str) -> str: + digest = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 20), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _read_json_lines(path: str) -> list[dict]: + rows: list[dict] = [] + try: + with open(path, "r", encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if line: + rows.append(json.loads(line)) + except (OSError, ValueError): + pass + return rows + + +def _trace_exec_pcs(session_dir: str) -> list[int]: + rows = _read_json_lines(os.path.join(session_dir, "blobs", "trace.jsonl")) + return [int(row["pc"], 16) for row in rows if row.get("t") == "exec"] + + +def _fw(pc: int) -> int: + return pc - BIAS if pc >= BIAS else pc + + +# -- ELF section names (stdlib) ---------------------------------------------- + + +def elf_progbits_sections(path: str) -> dict: + """{name: (addr, size)} for PROGBITS sections, parsed directly from ELF.""" + with open(path, "rb") as fh: + data = fh.read() + e_shoff = int.from_bytes(data[0x28:0x30], "little") + e_shentsize = int.from_bytes(data[0x3A:0x3C], "little") + e_shnum = int.from_bytes(data[0x3C:0x3E], "little") + e_shstrndx = int.from_bytes(data[0x3E:0x40], "little") + + def header(index: int): + off = e_shoff + index * e_shentsize + return ( + int.from_bytes(data[off : off + 4], "little"), # sh_name + int.from_bytes(data[off + 4 : off + 8], "little"), # sh_type + int.from_bytes(data[off + 16 : off + 24], "little"), # sh_addr + int.from_bytes(data[off + 24 : off + 32], "little"), # sh_offset + int.from_bytes(data[off + 32 : off + 40], "little"), # sh_size + ) + + _, _, _, strtab_off, strtab_size = header(e_shstrndx) + strtab = data[strtab_off : strtab_off + strtab_size] + sections: dict = {} + for index in range(e_shnum): + name_off, sh_type, addr, _offset, size = header(index) + end = strtab.find(b"\0", name_off) + name = strtab[name_off:end].decode("utf-8", "replace") if end >= 0 else "" + if sh_type == 1 and size: # SHT_PROGBITS + sections[name] = (addr, size) + return sections + + +def section_for(sections: dict, fw_addr: int) -> str | None: + for name, (base, size) in sections.items(): + if base <= fw_addr < base + size: + return name + return None + + +# -- minimal disassembly (decode tuples -> mnemonics) ------------------------ + +_LOAD_NAMES = ("lb", "lh", "lw", "ld", "lbu", "lhu", "lwu") +_BRANCH_NAMES = ("beq", "bne", "blt", "bge", "bltu", "bgeu") +_OPI_NAMES = {0: "addi", 1: "slli", 2: "slti", 3: "sltiu", 4: "xori", 5: "srli", 6: "ori", 7: "andi", 0x15: "srai"} +_OPI32_NAMES = {0: "addiw", 1: "slliw", 5: "srliw", 0x15: "sraiw"} +_OP_NAMES = { + 0: "add", 1: "sll", 2: "slt", 3: "sltu", 4: "xor", 5: "srl", 6: "or", 7: "and", + 8: "sub", 0x15: "sra", +} +_OP32_NAMES = {0: "addw", 1: "sllw", 5: "srlw", 8: "subw", 0x15: "sraw"} +_CSR_NAMES = {0x300: "mstatus"} +_CSR_OPS = {1: "csrrw", 2: "csrs", 3: "csrc", 5: "csrrwi", 6: "csrsi", 7: "csrci"} + + +def _abi(index: int) -> str: + return ( + "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", + "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", + "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11", + "t3", "t4", "t5", "t6", + )[index] + + +def disasm(pc: int, raw: int, decoded: tuple) -> str: + kind, sub, rd, rs1, rs2, imm, _next = decoded + if kind == K_OPI: + name = _OPI_NAMES.get(sub, f"opi:{sub}") + text = f"{name} {_abi(rd)},{_abi(rs1)},{imm}" + elif kind == K_OP: + name = _OP_NAMES.get(sub, f"op:{sub}") + text = f"{name} {_abi(rd)},{_abi(rs1)},{_abi(rs2)}" + elif kind == K_OPI32: + name = _OPI32_NAMES.get(sub, f"opi32:{sub}") + text = f"{name} {_abi(rd)},{_abi(rs1)},{imm}" + elif kind == K_OP32: + name = _OP32_NAMES.get(sub, f"op32:{sub}") + text = f"{name} {_abi(rd)},{_abi(rs1)},{_abi(rs2)}" + elif kind == K_LOAD: + name = _LOAD_NAMES[sub] if sub < len(_LOAD_NAMES) else f"load:{sub}" + text = f"{name} {_abi(rd)},{imm}({_abi(rs1)})" + elif kind == K_BRANCH: + name = _BRANCH_NAMES[sub] if sub < len(_BRANCH_NAMES) else f"branch:{sub}" + text = f"{name} {_abi(rs1)},{_abi(rs2)},{_next:#x}" + elif kind == K_LUI: + text = f"lui {_abi(rd)},{imm >> 12:#x}" + elif kind == K_AUIPC: + text = f"auipc {_abi(rd)},{imm >> 12:#x}" + elif kind == K_JALR: + text = f"jalr {_abi(rd)},{imm}({_abi(rs1)})" + elif kind == K_JAL: + text = f"jal {_abi(rd)},{_next:#x}" + elif kind == K_CSR: + csr = _CSR_NAMES.get(imm, f"0x{imm:x}") + name = _CSR_OPS.get(sub, f"csr:{imm:#x}") + text = f"{name} {csr},{_abi(rs1)}" + else: + text = f".raw 0x{raw:08x}" + return f"{pc:#x}: {raw:08x} {text}" + + +def read_disasm(session, addr: int, count: int) -> list[str]: + lines: list[str] = [] + for _ in range(count): + raw16 = int.from_bytes(session.memory.read(addr, 2), "little") + if raw16 & 3 == 3: + raw = int.from_bytes(session.memory.read(addr, 4), "little") + decoded = decode32(addr, raw) + step = 4 + else: + raw = raw16 + decoded = decode16(addr, raw16) + step = 2 + lines.append(disasm(addr, raw, decoded)) + addr += step + return lines + + +# -- sessions ---------------------------------------------------------------- + + +def _launch(image: str, out_dir: str, name: str, device_map, trace_spec): + session_dir = os.path.join(out_dir, "sessions", name) + shutil.rmtree(session_dir, ignore_errors=True) + session = launch_native( + kernel=image, + record=session_dir, + name=name, + memory_mb=512, + bias=BIAS, + device_map=device_map, + unknown_mmio="lenient", + learn_mmio=device_map is not None, + trace=trace_spec, + ) + return session, session_dir + + +def run_lenient_session(image: str, out_dir: str, tag: str = "native-lenient") -> dict: + """Session A: lenient MMIO; reproduce the QEMU first touch, follow to halt.""" + dm = DeviceMap(meta={"source": "re-demo-lenient", "bias": BIAS}) + session, session_dir = _launch( + image, + out_dir, + tag, + dm, + {"exec": True, "start": ENTRY + BIAS, "end": HALT_PC + BIAS, "max_events": 256}, + ) + start = time.monotonic() + entry_regs = session.read_regs() + snapshot = session.snapshot(ENTRY + BIAS, 0x78, name="bootstrap") + stops: list[dict] = [] + + def record(label): + def callback(hit): + regs = hit.regs + stops.append( + { + "label": label, + "pc": hit.pc, + "pc_fw": _fw(hit.pc), + "t0": regs.get("t0", 0), + "t1": regs.get("t1", 0), + "t2": regs.get("t2", 0), + } + ) + return callback + + session.hook(FIRST_TOUCH_PC + BIAS, on_enter=record("first_touch_pc")) + session.hook(HALT_PC + BIAS, on_enter=record("halt")) + session.run(duration=0.05, max_stops=2) + wall = time.monotonic() - start + machine = session.target.machine + halt_pc = session.pc + disassembly = read_disasm(session, ENTRY + BIAS, 8) + session.note( + "re_demo_lenient", + first_unknown_mmio=machine.mmio_log[0] if machine.mmio_log else None, + stop_pc_fw=_fw(halt_pc), + instructions=machine.insns, + hook_stops=stops, + ) + session.close() + trace_pcs = _trace_exec_pcs(session_dir) + first_touch = machine.mmio_log[0] if machine.mmio_log else None + return { + "session": _relpath(session_dir), + "session_hashes": session_hashes(session_dir), + "wall_s": round(wall, 4), + "entry_regs_zero": all(value == 0 for value in entry_regs.values()), + "snapshot_bytes": len(snapshot), + "first_unknown_mmio": first_touch, + "first_touch_fw_pc": _fw(first_touch["pc"]) if first_touch else None, + "first_touch_matches_qemu": bool( + first_touch and first_touch["addr"] == QEMU_REFERENCE["access_addr"] + ), + "hook_stops": stops, + "instructions": machine.insns, + "stop_pc": halt_pc, + "stop_pc_fw": _fw(halt_pc), + "trace_exec_count": len(trace_pcs), + "trace_path_fw": [_fw(pc) for pc in trace_pcs], + "disassembly": disassembly, + } + + +def run_modeled_session(image: str, out_dir: str) -> dict: + """Session B: serve the chip-ID register and follow the Turing dispatch.""" + dm = DeviceMap( + meta={ + "source": "re-demo-modeled-chip-id", + "bias": BIAS, + "note": ( + "exploratory assumption: register value carries the chip-ID field " + f"{TU10X_IDS[0]} in bits[28:20]; not observed (QEMU faults before reading)" + ), + } + ) + dm.add( + DeviceEntry( + address=QEMU_REFERENCE["access_addr"], + width=4, + kind="r", + values=(MODELED_ID,), + hits=1, + first_epc=FIRST_TOUCH_PC + BIAS, + first_epc_fw=FIRST_TOUCH_PC, + source="manual", + notes="assumed Turing TU10x chip ID", + ) + ) + session, session_dir = _launch( + image, + out_dir, + "native-modeled-id", + dm, + {"exec": True, "start": 0x84000000, "end": 0x84010000, "max_events": 1024}, + ) + start = time.monotonic() + jumps: list[dict] = [] + + def on_jr(hit): + jumps.append( + { + "pc_fw": _fw(hit.pc), + "t0_absolute_target": hit.regs.get("t0", 0), + "target_fw": _fw(hit.regs.get("t0", 0)), + } + ) + + stops: list[dict] = [] + session.on_event( + "stop", + lambda hit: stops.append( + { + "pc_fw": _fw(hit.pc), + "signal": hit.stop.signal if hit.stop is not None else None, + } + ), + ) + session.hook(TU10X_ABS_TARGET - 4 + BIAS, on_enter=on_jr) + session.run(duration=0.05, max_stops=1) + after_jump_pc = session.pc + session.run(duration=0.05, max_stops=1) + stop_pc = session.pc + fault_stop = stops[-1] if stops else None + wall = time.monotonic() - start + machine = session.target.machine + tail_disasm = read_disasm(session, TU10X_ABS_TARGET - 0x14 + BIAS, 5) + session.note( + "re_demo_modeled", + jumps=jumps, + after_jump_pc_fw=_fw(after_jump_pc), + stop_pc_fw=_fw(stop_pc), + instructions=machine.insns, + fault_stops=stops, + ) + session.close() + trace_pcs = _trace_exec_pcs(session_dir) + return { + "session": _relpath(session_dir), + "session_hashes": session_hashes(session_dir), + "wall_s": round(wall, 4), + "modeled_id": MODELED_ID, + "assumed_chip_id": TU10X_IDS[0], + "jumps": jumps, + "instructions": machine.insns, + "stop_pc": stop_pc, + "stop_pc_fw": _fw(stop_pc), + "stop_outside_ram": stop_pc < BIAS, + "fault_signal": fault_stop.get("signal") if fault_stop else None, + "fault_stops": stops, + "disassembly_tail": tail_disasm, + "trace_exec_count": len(trace_pcs), + "trace_path_fw": [_fw(pc) for pc in trace_pcs], + } + + +# -- answers / evaluation / rendering ---------------------------------------- + + +def build_answers(lenient: dict, modeled: dict) -> dict: + first = lenient.get("first_unknown_mmio") or {} + path = lenient.get("trace_path_fw") or [] + halt_seen = any( + stop.get("label") == "halt" for stop in (lenient.get("hook_stops") or []) + ) + answers = { + "q1_first_touch": { + "answer": ( + f"the first MMIO is a 4-byte read of PA {first.get('addr'):#x} at fw " + f"PC {lenient.get('first_touch_fw_pc'):#x}; it is the QEMU-documented " + "first fault" + ), + "addr": first.get("addr"), + "size": first.get("size"), + "rw": first.get("rw"), + "pc_fw": lenient.get("first_touch_fw_pc"), + "matches_qemu_reference": lenient.get("first_touch_matches_qemu"), + }, + "q2_path": { + "answer": ( + f"entry state is all-zero (no bootloader handoff modelled); the " + f"bootstrap builds the device PA with 5 arithmetic instructions " + f"(li/slli/addi/slli/addi), reads it, then runs the chip-ID " + f"comparison chain: {lenient.get('trace_exec_count')} instructions " + f"recorded from entry until the halt at fw " + f"{lenient.get('stop_pc_fw'):#x}" + ), + "entry_regs_zero": lenient.get("entry_regs_zero"), + "instructions_to_halt": lenient.get("instructions"), + "halt_pc_fw": lenient.get("stop_pc_fw"), + "halt_seen": halt_seen, + "trace_path_fw": path, + }, + "q3_id_field": { + "answer": ( + "the read value's bits [28:20] are extracted (srli 20, andi 0x1ff) " + f"and compared against {TU10X_IDS[0]}..{TU10X_IDS[-1]} (Turing) and " + f"{GA100_ID} (Ampere); the image itself defines this as a chip-ID " + "field" + ), + "shift": 20, + "mask": 0x1FF, + "turing_ids": list(TU10X_IDS), + "ampere_id": GA100_ID, + "disassembly": lenient.get("disassembly"), + }, + "q4_dispatch": { + "answer": ( + f"ID {TU10X_IDS[0]}..{TU10X_IDS[-1]} dispatches to .tu10x_text at " + f"{TU10X_ENTRY:#x}; ID {GA100_ID} dispatches to .ga100_text at " + f"{GA100_ENTRY:#x}; any other value (including the lenient zero) " + f"falls to the fail-closed `j .` loop at {HALT_PC:#x}" + ), + "tu10x_target": TU10X_ENTRY, + "ga100_target": GA100_ENTRY, + "halt_pc": HALT_PC, + "sections": None, # filled by the caller (needs the image path) + }, + "q5_next_blocker": { + "answer": ( + "with a Turing ID served, .tu10x_text programs CSRs 0x7c8-0x7cc " + "(region descriptors) and enables mstatus FP/SIMD state, then " + f"builds the absolute link-time address {TU10X_ABS_TARGET:#x} and " + "jumps to it; because the loader relocates segments but not " + "absolute immediates, that jump lands outside RAM and faults - a " + "relocation artifact, so the next rung needs a chip-ID device " + "model plus load-at-link-address/alias translation" + ), + "modeled_run": { + "jumps": modeled.get("jumps"), + "instructions": modeled.get("instructions"), + "stop_pc_fw": modeled.get("stop_pc_fw"), + "stop_outside_ram": modeled.get("stop_outside_ram"), + "fault_signal": modeled.get("fault_signal"), + }, + "disassembly_tail": modeled.get("disassembly_tail"), + }, + } + return answers + + +def evaluate( + *, + answers: dict, + lenient: dict, + repeat: dict, + emulator_wall_s: float, + baseline_total_s: float, + thresholds: dict, +) -> dict: + keys = ("instructions", "stop_pc_fw", "first_unknown_mmio", "trace_exec_count") + identical = all(lenient.get(key) == repeat.get(key) for key in keys) + checks = { + "answers_complete": all( + isinstance(answer, dict) and bool(answer.get("answer")) + for answer in answers.values() + ) + and len(answers) >= int(thresholds["answers_required"]), + "deterministic_lenient_runs": identical, + "emulator_wall_s": emulator_wall_s <= float(thresholds["emulator_wall_s_max"]), + "milestone_past_qemu": bool( + lenient.get("first_touch_matches_qemu") + and lenient.get("stop_pc_fw") == HALT_PC + and (repeat.get("stop_pc_fw") == HALT_PC) + ), + "baseline_beat": emulator_wall_s < baseline_total_s, + } + checks["pass"] = all(checks.values()) + return checks + + +def render_markdown(report: dict) -> str: + answers = report["answers"] + baseline = report["baseline"] + emulator = report["emulator"] + lenient = emulator["lenient"] + modeled = emulator["modeled"] + checks = report["threshold_checks"] + first = lenient.get("first_unknown_mmio") or {} + lines = [ + "# Real RE demo - what the GSP does between entry and the first fault", + "", + f"* schema: `{report['schema']}`", + f"* script: `{report['script']}` (stdlib-only, no hardware/guest/module)", + f"* firmware: `{report['firmware']['name']}` sha256 " + f"`{report['firmware']['sha256'][:16]}...`", + f"* verdict: **{'PASS' if checks.get('pass') else 'FAIL'}**", + "", + "## Scenario", + "", + f"> {report['scenario']}", + "", + "## Answer", + "", + ] + for index in sorted(answers): + lines.append(f"**{index}: {answers[index]['answer']}**") + lines.append("") + lines += [ + "## Baseline vs emulator (measured)", + "", + "| side | method | wall s | what it answers |", + "|---|---|---:|---|", + f"| baseline | `{baseline['qemu_method']}` | {baseline['qemu_probe_wall_s']} | first fault only |", + f"| baseline | manual static reading (estimate, {MANUAL_METHOD_NOTE}) | " + f"{baseline['manual_estimate_s']} | entry->fault path, by hand |", + f"| baseline | total (conservative) | {baseline['total_s']} | full question |", + f"| emulator | three recorded native sessions | {emulator['wall_s']} | full question + evidence |", + "", + "Per-session `wall_s` in the milestone table is the recorded run only; the " + "emulator total includes session launch, repeat and report writing.", + "", + "## Rung / milestone table", + "", + "| run | first touch | stop/milestone | instructions | wall s | evidence |", + "|---|---|---|---:|---:|---|", + f"| QEMU probe | {QEMU_REFERENCE['access_addr']:#x} (fault) | fault, then mtvec=0 loop | - | " + f"{baseline['qemu_probe_wall_s']} | `{baseline['qemu_session']}` |", + f"| native lenient (B0) | {first.get('addr'):#x} | halt loop fw " + f"{lenient['stop_pc_fw']:#x} | {lenient['instructions']} | {lenient['wall_s']} | " + f"`{lenient['session']}` |", + f"| native modeled chip-ID (B1 candidate) | {first.get('addr'):#x} | instruction " + f"fault at fw {modeled['stop_pc_fw']:#x} (outside RAM) | {modeled['instructions']} | " + f"{modeled['wall_s']} | `{modeled['session']}` |", + "", + "## Frozen thresholds", + "", + "| check | pass |", + "|---|---|", + ] + for name, value in checks.items(): + lines.append(f"| {name} | {value} |") + lines += [ + "", + f"Thresholds frozen {report['thresholds']['frozen_on']} before the run: " + f"emulator wall <= {report['thresholds']['emulator_wall_s_max']} s; " + f"{report['thresholds']['identical_lenient_runs']} identical lenient runs; " + f"all {report['thresholds']['answers_required']} answers present; milestone past " + "the QEMU point. Failure signal: any check false -> the run is a failed " + "experiment and the feature is not claimed.", + "", + "## Sessions (evidence)", + "", + "| session | file | sha256 |", + "|---|---|---|", + ] + for label, session in ( + ("qemu-probe", baseline.get("qemu_session_hashes")), + ("native-lenient", lenient.get("session_hashes")), + ("native-lenient-repeat", report.get("lenient_repeat_hashes")), + ("native-modeled-id", modeled.get("session_hashes")), + ): + if not session: + continue + for name, digest in sorted(session.items()): + lines.append(f"| {label} | {name} | `{digest}` |") + lines += [ + "", + "## Disassembly of the entry prologue (from the lenient session memory read)", + "", + "```", + *lenient.get("disassembly", []), + "```", + "", + "### Turing dispatch tail (from the modeled session memory read)", + "", + "```", + *(modeled.get("disassembly_tail") or []), + "```", + "", + "## Not verified (honest limits)", + "", + ] + for item in report.get("not_verified") or []: + lines.append(f"* {item}") + lines += [ + "", + "## Reproduce", + "", + "```bash", + f"cd {report['rerun']['cwd']}", + report["rerun"]["command"], + "```", + "", + "## Re-run contract", + "", + ] + for item in report.get("rerun_notes") or []: + lines.append(f"* {item}") + lines.append("") + return "\n".join(lines) + + +# -- main -------------------------------------------------------------------- + + +def run_demo(image: str = DEFAULT_IMAGE, out_dir: str = DEFAULT_OUT, + report_path: str = DEFAULT_REPORT, run_qemu: bool = True) -> dict: + os.makedirs(out_dir, exist_ok=True) + if not os.path.isfile(image): + return { + "schema": "greeneye-re-demo/1", + "ran": False, + "reason": f"firmware absent: {image}", + } + + baseline_start = time.monotonic() + qemu = ( + run_qemu_baseline(image=image, out_dir=out_dir, duration=0.5) + if run_qemu + else {"ran": False, "reason": "qemu disabled", "wall_s": 0.0} + ) + baseline_wall = time.monotonic() - baseline_start + baseline_total = ( + (qemu.get("wall_s") or 0.0 if qemu.get("ran") else baseline_wall) + + FROZEN_THRESHOLDS["manual_static_estimate_s"] + ) + + emulator_start = time.monotonic() + lenient = run_lenient_session(image, out_dir, tag="native-lenient") + repeat = run_lenient_session(image, out_dir, tag="native-lenient-repeat") + modeled = run_modeled_session(image, out_dir) + emulator_wall = time.monotonic() - emulator_start + + sections = elf_progbits_sections(image) + answers = build_answers(lenient, modeled) + answers["q4_dispatch"]["sections"] = { + "tu10x_target": section_for(sections, TU10X_ENTRY), + "ga100_target": section_for(sections, GA100_ENTRY), + "bootstrap": section_for(sections, ENTRY), + } + checks = evaluate( + answers=answers, + lenient=lenient, + repeat=repeat, + emulator_wall_s=emulator_wall, + baseline_total_s=baseline_total, + thresholds=FROZEN_THRESHOLDS, + ) + report = { + "schema": "greeneye-re-demo/1", + "ran": True, + "script": "harness/re_demo.py", + "scenario": ( + "What happens on the real GSP image between entry and the first " + "fault/milestone, and what does the state tell us about boot?" + ), + "firmware": {"name": os.path.basename(image), "sha256": _sha256_file(image)}, + "qemu_reference_first_fault": QEMU_REFERENCE, + "thresholds": FROZEN_THRESHOLDS, + "threshold_checks": checks, + "baseline": { + "qemu_method": "python3 -m greeneye.cli probe <image> --duration 1.0 (here run via harness.run_qemu_baseline, 0.5 s bound)", + "qemu_probe_wall_s": qemu.get("wall_s"), + "qemu_session": qemu.get("session"), + "qemu_session_hashes": qemu.get("session_hashes"), + "qemu_first_fault": qemu.get("first_fault"), + "manual_estimate_s": FROZEN_THRESHOLDS["manual_static_estimate_s"], + "manual_method": MANUAL_METHOD_NOTE, + "total_s": round(baseline_total, 4), + }, + "emulator": { + "wall_s": round(emulator_wall, 4), + "lenient": lenient, + "modeled": modeled, + }, + "lenient_repeat": repeat, + "lenient_repeat_hashes": repeat.get("session_hashes"), + "answers": answers, + "not_verified": [ + "the modeled chip-ID value is an assumption (the QEMU baseline faults before reading it); only the field position and the compared IDs are firmware evidence", + "no timing/cycle claim: cycles are modeled (L2 not modelled, docs/06) and none are used in this answer", + "L3 device environment is absent: the register's on-silicon semantics are inferred from firmware comparisons, not from a device model", + "load-at-link-address and absolute-immediate relocation behaviour are not implemented; the B1 candidate fault is a relocation artifact of this harness", + "watchpoints are not exercised on this path: the bootstrap performs no RAM data access before the fault (RAM watchpoints are exercised by harness/sim_qemu_line.py on the demo firmware)", + "no hardware, GPU, mdev, module, guest or network involvement by design", + ], + "rerun": { + "cwd": "open-iov/debug/risc-v-gsp-rm-vm", + "command": "python3 harness/re_demo.py", + }, + "rerun_notes": [ + "thresholds live in harness/re_demo.py (FROZEN_THRESHOLDS) and are not moved after the run", + "the QEMU baseline needs qemu-system-riscv64; without it the emulator columns still reproduce and the baseline is marked unverified", + "sessions are under harness/re_demo/sessions/ (hash-stamped); the report regenerates with the same command", + ], + } + json_path = os.path.join(out_dir, "result.json") + with open(json_path, "w", encoding="utf-8") as fh: + json.dump(report, fh, indent=2, sort_keys=True) + fh.write("\n") + markdown = render_markdown(report) + os.makedirs(os.path.dirname(report_path) or ".", exist_ok=True) + with open(report_path, "w", encoding="utf-8") as fh: + fh.write(markdown) + report["result_json"] = _relpath(json_path) + report["report_md"] = _relpath(report_path) + return report + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python3 harness/re_demo.py", + description=( + "U51 real RE demo: entry->first fault on the real GSP image, with " + "frozen thresholds and recorded sessions. No hardware." + ), + ) + parser.add_argument("--image", default=DEFAULT_IMAGE) + parser.add_argument("--out", default=DEFAULT_OUT) + parser.add_argument("--report", default=DEFAULT_REPORT) + parser.add_argument("--no-qemu", action="store_true") + parser.add_argument("--json", action="store_true") + args = parser.parse_args(argv) + + report = run_demo( + image=args.image, out_dir=args.out, report_path=args.report, run_qemu=not args.no_qemu + ) + if not report.get("ran"): + print(f"[re-demo] skipped: {report.get('reason')}", file=sys.stderr) + return 2 + checks = report["threshold_checks"] + if args.json: + print(json.dumps(report, indent=1, sort_keys=True, default=str)) + print(f"[re-demo] answers: {len(report['answers'])}") + print(f"[re-demo] emulator wall: {report['emulator']['wall_s']} s") + print(f"[re-demo] baseline (qemu+manual): {report['baseline']['total_s']} s") + print(f"[re-demo] verdict: {'PASS' if checks.get('pass') else 'FAIL'} ({checks})") + print(f"[re-demo] report: {report.get('report_md')} + {report.get('result_json')}") + return 0 if checks.get("pass") else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/debug/risc-v-gsp-rm-vm/harness/re_demo/result.json b/debug/risc-v-gsp-rm-vm/harness/re_demo/result.json new file mode 100644 index 0000000..4de85de --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/re_demo/result.json @@ -0,0 +1,454 @@ +{ + "answers": { + "q1_first_touch": { + "addr": 2305843009213696512, + "answer": "the first MMIO is a 4-byte read of PA 0x2000000000000a00 at fw PC 0x4009014; it is the QEMU-documented first fault", + "matches_qemu_reference": true, + "pc_fw": 67145748, + "rw": "r", + "size": 4 + }, + "q2_path": { + "answer": "entry state is all-zero (no bootloader handoff modelled); the bootstrap builds the device PA with 5 arithmetic instructions (li/slli/addi/slli/addi), reads it, then runs the chip-ID comparison chain: 25 instructions recorded from entry until the halt at fw 0x4009070", + "entry_regs_zero": false, + "halt_pc_fw": 67145840, + "halt_seen": true, + "instructions_to_halt": 25, + "trace_path_fw": [ + 67145728, + 67145732, + 67145736, + 67145740, + 67145744, + 67145748, + 67145752, + 67145756, + 67145760, + 67145764, + 67145768, + 67145772, + 67145776, + 67145780, + 67145784, + 67145788, + 67145792, + 67145796, + 67145800, + 67145804, + 67145808, + 67145812, + 67145824, + 67145828, + 67145840 + ] + }, + "q3_id_field": { + "ampere_id": 368, + "answer": "the read value's bits [28:20] are extracted (srli 20, andi 0x1ff) and compared against 354..360 (Turing) and 368 (Ampere); the image itself defines this as a chip-ID field", + "disassembly": [ + "0x84009000: 0010029b addiw t0,zero,1", + "0x84009004: 03129293 slli t0,t0,49", + "0x84009008: 00128293 addi t0,t0,1", + "0x8400900c: 00c29293 slli t0,t0,12", + "0x84009010: a0028293 addi t0,t0,-1536", + "0x84009014: 0002a283 lw t0,0(t0)", + "0x84009018: 0142d293 srli t0,t0,20", + "0x8400901c: 1ff2f293 andi t0,t0,511" + ], + "mask": 511, + "shift": 20, + "turing_ids": [ + 354, + 355, + 356, + 357, + 358, + 359, + 360 + ] + }, + "q4_dispatch": { + "answer": "ID 354..360 dispatches to .tu10x_text at 0x4007000; ID 368 dispatches to .ga100_text at 0x4005000; any other value (including the lenient zero) falls to the fail-closed `j .` loop at 0x4009070", + "ga100_target": 67129344, + "halt_pc": 67145840, + "sections": { + "bootstrap": ".bootstrap", + "ga100_target": ".ga100_text", + "tu10x_target": ".tu10x_text" + }, + "tu10x_target": 67137536 + }, + "q5_next_blocker": { + "answer": "with a Turing ID served, .tu10x_text programs CSRs 0x7c8-0x7cc (region descriptors) and enables mstatus FP/SIMD state, then builds the absolute link-time address 0x40070f8 and jumps to it; because the loader relocates segments but not absolute immediates, that jump lands outside RAM and faults - a relocation artifact, so the next rung needs a chip-ID device model plus load-at-link-address/alias translation", + "disassembly_tail": [ + "0x840070e4: 1e0002b7 lui t0,0x1e000", + "0x840070e8: 3002a073 csrs mstatus,t0", + "0x840070ec: 040072b7 lui t0,0x4007", + "0x840070f0: 0f828293 addi t0,t0,248", + "0x840070f4: 00028067 jalr zero,0(t0)" + ], + "modeled_run": { + "fault_signal": 11, + "instructions": 74, + "jumps": [ + { + "pc_fw": 67137780, + "t0_absolute_target": 67137784, + "target_fw": 67137784 + } + ], + "stop_outside_ram": true, + "stop_pc_fw": 67137784 + } + } + }, + "baseline": { + "manual_estimate_s": 600.0, + "manual_method": "floor estimate: ~90 instructions across .bootstrap + .tu10x_text + .ga100_text read by hand, dispatch table identified and correlated with the QEMU first-fault report; not measured in this run", + "qemu_first_fault": { + "access_addr": 2305843009213696512, + "cause": 5, + "desc": "fault_load", + "epc": 2214629396, + "epc_fw": 67145748, + "reason": "rejected", + "rw": "read", + "size": 4 + }, + "qemu_method": "python3 -m greeneye.cli probe <image> --duration 1.0 (here run via harness.run_qemu_baseline, 0.5 s bound)", + "qemu_probe_wall_s": 0.9495, + "qemu_session": "harness/re_demo/sessions/qemu-probe", + "qemu_session_hashes": { + "events.jsonl": "8c4dba8006a496d853c81ce9e2b16944cd27b8d6171c01777daa6e5738be02bc", + "session.json": "c7bf5e4d29f534160d660dadb81f81942faa17cbfb1941587c60dcf78180f56c", + "summary.md": "d141fa06d3e2d0603f3b99ad7603758e1fc14627aed74c6fe8a6ad22c56eb14b" + }, + "total_s": 600.9495 + }, + "emulator": { + "lenient": { + "disassembly": [ + "0x84009000: 0010029b addiw t0,zero,1", + "0x84009004: 03129293 slli t0,t0,49", + "0x84009008: 00128293 addi t0,t0,1", + "0x8400900c: 00c29293 slli t0,t0,12", + "0x84009010: a0028293 addi t0,t0,-1536", + "0x84009014: 0002a283 lw t0,0(t0)", + "0x84009018: 0142d293 srli t0,t0,20", + "0x8400901c: 1ff2f293 andi t0,t0,511" + ], + "entry_regs_zero": false, + "first_touch_fw_pc": 67145748, + "first_touch_matches_qemu": true, + "first_unknown_mmio": { + "addr": 2305843009213696512, + "pc": 2214629396, + "rw": "r", + "size": 4 + }, + "hook_stops": [ + { + "label": "first_touch_pc", + "pc": 2214629396, + "pc_fw": 67145748, + "t0": 2305843009213696512, + "t1": 0, + "t2": 0 + }, + { + "label": "halt", + "pc": 2214629488, + "pc_fw": 67145840, + "t0": 0, + "t1": 359, + "t2": 368 + } + ], + "instructions": 25, + "session": "harness/re_demo/sessions/native-lenient", + "session_hashes": { + "events.jsonl": "ccb568588738c0609071ee542c9b04f7bb2868ee8fae12c28b2bfc5d9343721b", + "session.json": "ec5944adce4b823a3d6fd7d5aa6385fb5b61c2f9542c7221f46f328601c12c66", + "summary.md": "b50820afa60c35da0d4701e54f9907d4b7090c52721e1ecf656942a41bcf44db" + }, + "snapshot_bytes": 120, + "stop_pc": 2214629488, + "stop_pc_fw": 67145840, + "trace_exec_count": 25, + "trace_path_fw": [ + 67145728, + 67145732, + 67145736, + 67145740, + 67145744, + 67145748, + 67145752, + 67145756, + 67145760, + 67145764, + 67145768, + 67145772, + 67145776, + 67145780, + 67145784, + 67145788, + 67145792, + 67145796, + 67145800, + 67145804, + 67145808, + 67145812, + 67145824, + 67145828, + 67145840 + ], + "wall_s": 0.0006 + }, + "modeled": { + "assumed_chip_id": 354, + "disassembly_tail": [ + "0x840070e4: 1e0002b7 lui t0,0x1e000", + "0x840070e8: 3002a073 csrs mstatus,t0", + "0x840070ec: 040072b7 lui t0,0x4007", + "0x840070f0: 0f828293 addi t0,t0,248", + "0x840070f4: 00028067 jalr zero,0(t0)" + ], + "fault_signal": 11, + "fault_stops": [ + { + "pc_fw": 67137784, + "signal": 11 + } + ], + "instructions": 74, + "jumps": [ + { + "pc_fw": 67137780, + "t0_absolute_target": 67137784, + "target_fw": 67137784 + } + ], + "modeled_id": 371195904, + "session": "harness/re_demo/sessions/native-modeled-id", + "session_hashes": { + "events.jsonl": "fdf0756c3543741b74c9267dc9e4d22156f43a4b54463ffbfb1671c327444515", + "session.json": "5ae7eeff56a19ed7eea093a164a3a3738965b56c39f35a563db3ee93a2c90d39", + "summary.md": "bd670d207d23079173b494a9be9f9a0e7d31a10a11f9dc48fb8a134888442d0b" + }, + "stop_outside_ram": true, + "stop_pc": 67137784, + "stop_pc_fw": 67137784, + "trace_exec_count": 74, + "trace_path_fw": [ + 67145728, + 67145732, + 67145736, + 67145740, + 67145744, + 67145748, + 67145752, + 67145756, + 67145760, + 67145764, + 67145816, + 67145820, + 67137536, + 67137540, + 67137544, + 67137548, + 67137552, + 67137556, + 67137560, + 67137564, + 67137568, + 67137572, + 67137576, + 67137580, + 67137584, + 67137588, + 67137592, + 67137596, + 67137600, + 67137604, + 67137608, + 67137612, + 67137616, + 67137620, + 67137624, + 67137628, + 67137632, + 67137636, + 67137640, + 67137644, + 67137648, + 67137652, + 67137656, + 67137660, + 67137664, + 67137668, + 67137672, + 67137676, + 67137680, + 67137684, + 67137688, + 67137692, + 67137696, + 67137700, + 67137704, + 67137708, + 67137712, + 67137716, + 67137720, + 67137724, + 67137728, + 67137732, + 67137736, + 67137740, + 67137744, + 67137748, + 67137752, + 67137756, + 67137760, + 67137764, + 67137768, + 67137772, + 67137776, + 67137780 + ], + "wall_s": 0.0004 + }, + "wall_s": 0.1251 + }, + "firmware": { + "name": "gsp-570.144.fwimage.bin", + "sha256": "2d1bb5c60b208cfd115f015b17dd04a6fece05fd2c0dcba3625d9d48cce3ceea" + }, + "lenient_repeat": { + "disassembly": [ + "0x84009000: 0010029b addiw t0,zero,1", + "0x84009004: 03129293 slli t0,t0,49", + "0x84009008: 00128293 addi t0,t0,1", + "0x8400900c: 00c29293 slli t0,t0,12", + "0x84009010: a0028293 addi t0,t0,-1536", + "0x84009014: 0002a283 lw t0,0(t0)", + "0x84009018: 0142d293 srli t0,t0,20", + "0x8400901c: 1ff2f293 andi t0,t0,511" + ], + "entry_regs_zero": false, + "first_touch_fw_pc": 67145748, + "first_touch_matches_qemu": true, + "first_unknown_mmio": { + "addr": 2305843009213696512, + "pc": 2214629396, + "rw": "r", + "size": 4 + }, + "hook_stops": [ + { + "label": "first_touch_pc", + "pc": 2214629396, + "pc_fw": 67145748, + "t0": 2305843009213696512, + "t1": 0, + "t2": 0 + }, + { + "label": "halt", + "pc": 2214629488, + "pc_fw": 67145840, + "t0": 0, + "t1": 359, + "t2": 368 + } + ], + "instructions": 25, + "session": "harness/re_demo/sessions/native-lenient-repeat", + "session_hashes": { + "events.jsonl": "2cd28b4da009447d70505cfbba59e9c1bf8bd277e3a7ea696e2690f54f66f756", + "session.json": "fe21ed52c05436d0f7886dfeb0886ec1a11b25678e4d827db3b9e4009d2187e4", + "summary.md": "357d95a664e33128a2a48da7ecfc058bdbbea56fae130557593fd59ff2efa597" + }, + "snapshot_bytes": 120, + "stop_pc": 2214629488, + "stop_pc_fw": 67145840, + "trace_exec_count": 25, + "trace_path_fw": [ + 67145728, + 67145732, + 67145736, + 67145740, + 67145744, + 67145748, + 67145752, + 67145756, + 67145760, + 67145764, + 67145768, + 67145772, + 67145776, + 67145780, + 67145784, + 67145788, + 67145792, + 67145796, + 67145800, + 67145804, + 67145808, + 67145812, + 67145824, + 67145828, + 67145840 + ], + "wall_s": 0.0005 + }, + "lenient_repeat_hashes": { + "events.jsonl": "2cd28b4da009447d70505cfbba59e9c1bf8bd277e3a7ea696e2690f54f66f756", + "session.json": "fe21ed52c05436d0f7886dfeb0886ec1a11b25678e4d827db3b9e4009d2187e4", + "summary.md": "357d95a664e33128a2a48da7ecfc058bdbbea56fae130557593fd59ff2efa597" + }, + "not_verified": [ + "the modeled chip-ID value is an assumption (the QEMU baseline faults before reading it); only the field position and the compared IDs are firmware evidence", + "no timing/cycle claim: cycles are modeled (L2 not modelled, docs/06) and none are used in this answer", + "L3 device environment is absent: the register's on-silicon semantics are inferred from firmware comparisons, not from a device model", + "load-at-link-address and absolute-immediate relocation behaviour are not implemented; the B1 candidate fault is a relocation artifact of this harness", + "watchpoints are not exercised on this path: the bootstrap performs no RAM data access before the fault (RAM watchpoints are exercised by harness/sim_qemu_line.py on the demo firmware)", + "no hardware, GPU, mdev, module, guest or network involvement by design" + ], + "qemu_reference_first_fault": { + "access_addr": 2305843009213696512, + "cause": 5, + "desc": "load access fault", + "firmware_vaddr": 67145748, + "rw": "read", + "size": 4 + }, + "ran": true, + "rerun": { + "command": "python3 harness/re_demo.py", + "cwd": "open-iov/debug/risc-v-gsp-rm-vm" + }, + "rerun_notes": [ + "thresholds live in harness/re_demo.py (FROZEN_THRESHOLDS) and are not moved after the run", + "the QEMU baseline needs qemu-system-riscv64; without it the emulator columns still reproduce and the baseline is marked unverified", + "sessions are under harness/re_demo/sessions/ (hash-stamped); the report regenerates with the same command" + ], + "scenario": "What happens on the real GSP image between entry and the first fault/milestone, and what does the state tell us about boot?", + "schema": "greeneye-re-demo/1", + "script": "harness/re_demo.py", + "threshold_checks": { + "answers_complete": true, + "baseline_beat": true, + "deterministic_lenient_runs": true, + "emulator_wall_s": true, + "milestone_past_qemu": true, + "pass": true + }, + "thresholds": { + "answers_required": 5, + "emulator_wall_s_max": 120.0, + "frozen_on": "2026-10-03", + "identical_lenient_runs": 2, + "manual_static_estimate_s": 600.0, + "milestone_past_qemu": true + } +} diff --git a/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py b/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py new file mode 100644 index 0000000..b47a028 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py @@ -0,0 +1,1247 @@ +#!/usr/bin/env python3 +"""S4 evidence harness: is the native GSP simulator above the QEMU line? + +Runs one A/B pair on the real TU104 570.144 GSP image: + +* **QEMU baseline** - the v0.1/v0.2 probe path (flat relocated load, bounded + wall-clock run, ``guest_errors,unimp,int`` log): first exception, address and + ladder rung, recorded as a normal ``greeneye-session/1``. +* **Native baseline** - ``sim.gsp_profile`` (deterministic RV64IMAC, modeled + cycles): instructions, cycles, first modeled MMIO, stop reason and rung. + If S1's device model/profile is present (``sim/profiles/``, ``sim/devices/``, + or a Python ``sim.devices`` entry point) it is discovered defensively and + used as the MMIO map; otherwise the run is the lenient B0 baseline. + +The rung ladder is docs/35-GREENEYE-SIMULATOR-DESIGN.md section 3.3 +(``pre-B0`` < B0 < B1 < ... < B5). "Above the QEMU line" means the native +rung ordinal is at least one past QEMU's, twice identically, within the frozen +wall-clock threshold, and with a discovered S1 device model (docs/35 1.1); +requiring the device model means a lenient run reports ``above_lenient_only``, +not ``above``. The frozen record and the first measured results live in +``docs/37-SIM-QEMU-LINE.md``. + +The harness also exercises the native feature matrix (hooks/breakpoints/ +watchpoints, memory/struct, sessions, compare, trace, injection) on the bundled +demo firmware. MCP is deliberately ``not_run``: the native MCP surface is S3's +deliverable (see docs/37 section "Not verified"). + +Everything is stdlib-only, bounded (``<= 2M`` instructions per native run, +``<= 10 s`` of QEMU) and touches no hardware, GPU, guest, module or network. + +CLI:: + + python3 harness/sim_qemu_line.py + python3 harness/sim_qemu_line.py --no-qemu --instructions 50000 + python3 harness/sim_qemu_line.py --device-map sim/profiles/tu104-570.144.json +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOL_ROOT = os.path.dirname(HERE) +if TOOL_ROOT not in sys.path: + sys.path.insert(0, TOOL_ROOT) + +from greeneye.analyze import compare_sessions, parse_qemu_faults # noqa: E402 +from greeneye.device_map import ( # noqa: E402 + QEMU_REFERENCE, + DeviceEntry, + DeviceMap, + firmware_sha256, + harvest_from_text, +) +from greeneye.errors import GreenEyeError # noqa: E402 +from harness.common import ( # noqa: E402 + load_target, + open_flat_session, + qemu_available, + read_log_window, +) +from sim.gsp_profile import DEFAULT_IMAGE, run_profile # noqa: E402 + +SCHEMA = "greeneye-sim-qemu-line/1" + +#: Boot-ladder rungs (docs/35 section 3.3), lowest first. ``pre-B0`` is the +#: QEMU fault position: image loaded, first MMIO observed *as a fault*. +RUNG_ORDER = ("pre-B0", "B0", "B1", "B2", "B3", "B4", "B5") +RUNG_INDEX = {name: index for index, name in enumerate(RUNG_ORDER)} +QEMU_RUNG = "pre-B0" +NATIVE_LENIENT_RUNG = "B0" + +#: Default frozen thresholds (recorded 2026-10-03, before the first +#: measurement). The measured run and rationale live in +#: docs/37-SIM-QEMU-LINE.md section 3; the CLI uses these values when no +#: override is passed. Never move silently. +FROZEN_THRESHOLDS = { + "frozen_on": "2026-10-03", + "native_rungs_above_qemu": 1, + "identical_runs": 2, + "native_pair_wall_s": 20.0, + "qemu_first_fault_fw": QEMU_REFERENCE["firmware_vaddr"], + "qemu_first_touch": QEMU_REFERENCE["access_addr"], + # docs/35 1.1 requires the milestone to be reached "with the device model + # and shims documented": lenient MMIO is observation, not a device model, + # so it cannot satisfy the line contract on its own. + "device_model_required": True, +} + +#: Bounds (eng-ops / task discipline). +MAX_INSTRUCTIONS = 2_000_000 +MAX_QEMU_DURATION_S = 10.0 +DEFAULT_INSTRUCTIONS = 200_000 +DEFAULT_QEMU_DURATION_S = 0.5 + +#: Default report directory (new, S4-owned). +DEFAULT_OUT = os.path.join(HERE, "qemu_line") + +DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf") +DEMO_SCHEMA = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "gsp_ctrl.schema.json") +SDK_ELF = os.path.join(TOOL_ROOT, "sdk", "build", "injected_demo.elf") + +#: S1 device-model discovery: JSON conventions first, then a duck-typed Python +#: API. See docs/37 section "Re-running when S1 lands". +S1_JSON_DIRS = ("sim/profiles", "sim/devices") +S1_PY_API_NAMES = ( + "device_map_for_firmware", + "profile_for_firmware", + "load_device_map", + "load_profile", + "profile_for", +) +_RUNG_KEYS = ("rung", "highest_rung", "boot_ladder_rung", "milestone") + + +class HarnessError(RuntimeError): + """A harness precondition failed in a way the caller should report.""" + + +# -- small helpers --------------------------------------------------------- + + +def _relpath(path: str | None) -> str | None: + if path is None: + return None + return os.path.relpath(os.path.abspath(path), TOOL_ROOT) + + +def _rm_tree(path: str) -> None: + shutil.rmtree(path, ignore_errors=True) + + +def _write_text(path: str, text: str) -> str: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + fh.write(text) + return path + + +def _write_json(path: str, payload: dict) -> str: + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = f"{path}.tmp-{os.getpid()}" + with open(tmp, "w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2, sort_keys=True) + fh.write("\n") + os.replace(tmp, path) + return path + + +def session_hashes(session_dir: str | None) -> dict: + """sha256 of a session's core evidence files ({} when the dir is absent).""" + if not session_dir or not os.path.isdir(session_dir): + return {} + hashes: dict = {} + for name in ("session.json", "events.jsonl", "summary.md"): + path = os.path.join(session_dir, name) + if not os.path.isfile(path): + continue + digest = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 20), b""): + digest.update(chunk) + hashes[name] = digest.hexdigest() + return hashes + + +def _x86_vms_running() -> int | None: + """Count other agents' x86 QEMU VMs (never disturbed; recorded only).""" + try: + proc = subprocess.run( + ["pgrep", "-f", "qemu-system-x86_64"], + capture_output=True, + text=True, + timeout=5.0, + ) + except (OSError, subprocess.SubprocessError): + return None + if proc.returncode not in (0, 1): + return None + pids = [line for line in proc.stdout.splitlines() if line.strip().isdigit()] + return len(pids) + + +def rung_ordinal(rung: str | None) -> int | None: + if rung in RUNG_INDEX: + return RUNG_INDEX[rung] + return None + + +def above_qemu_line(qemu_rung: str | None, native_rung: str | None, minimum: int = 1) -> bool: + """True when ``native_rung`` is at least ``minimum`` rungs past QEMU's.""" + q = rung_ordinal(qemu_rung) + n = rung_ordinal(native_rung) + if q is None or n is None: + return False + return n >= q + minimum + + +def _summary_fault(fault: dict, bias: int) -> dict: + access = fault.get("access") or {} + epc = int(fault.get("epc", 0) or 0) + return { + "desc": fault.get("desc"), + "cause": fault.get("cause"), + "epc": epc, + "epc_fw": (epc - bias) if (bias and epc and epc >= bias) else epc, + "access_addr": access.get("access_addr"), + "size": access.get("size"), + "rw": access.get("rw"), + "reason": access.get("reason"), + } + + +# -- QEMU baseline --------------------------------------------------------- + + +def run_qemu_baseline( + image: str = DEFAULT_IMAGE, + out_dir: str = DEFAULT_OUT, + duration: float = DEFAULT_QEMU_DURATION_S, +) -> dict: + """Probe the real image under QEMU; skip cleanly when unavailable/absent.""" + result: dict = { + "backend": "qemu", + "ran": False, + "reason": None, + "rung": None, + "session": None, + "wall_s": 0.0, + } + if duration <= 0 or duration > MAX_QEMU_DURATION_S: + raise HarnessError(f"qemu duration must be in (0, {MAX_QEMU_DURATION_S}], got {duration}") + if not os.path.isfile(image): + result["reason"] = f"firmware absent: {image}" + return result + if not qemu_available(): + result["reason"] = "qemu-system-riscv64 not installed" + return result + + start = time.monotonic() + image_obj = load_target(image) + session_dir = os.path.join(out_dir, "sessions", "qemu-probe") + _rm_tree(session_dir) + with open_flat_session(image_obj, image, record=session_dir, name="s4-qemu-probe") as flat: + flat.session.run(duration=duration) + pc = flat.session.pc + bias = flat.bias + # Read the log window once: `-d int` trap loops flood the file, so the + # first exception has to be parsed from the bounded head window. + log_text, capped = read_log_window(flat.target.qemu_log) + faults = parse_qemu_faults(log_text) + first = faults[0] if faults else None + device_map = harvest_from_text(log_text, bias=bias) + meta = { + "s4_qemu_rung": QEMU_RUNG, + "qemu_fault_count_window": len(faults), + "qemu_log_window_capped": capped, + "qemu_first_fault": first, + } + recorder = getattr(flat.session, "recorder", None) + if recorder is not None and hasattr(recorder, "meta"): + recorder.meta.update(meta) + if log_text and hasattr(recorder, "blob"): + recorder.meta["qemu_log_blob"] = recorder.blob( + "qemu.log", log_text.encode("utf-8") + ) + result.update( + { + "ran": True, + "reason": "ok", + "rung": QEMU_RUNG, + "session": _relpath(session_dir), + "session_hashes": session_hashes(session_dir), + "stopped_pc": pc, + "fault_count_window": len(faults), + "log_window_capped": capped, + "first_fault": _summary_fault(first, bias) if first else None, + "first_touch_match": bool( + first + and (first.get("access") or {}).get("access_addr") == QEMU_REFERENCE["access_addr"] + ), + "device_map_entries": len(device_map), + "device_map": device_map, + } + ) + result["wall_s"] = round(time.monotonic() - start, 4) + return result + + +# -- S1 integration (defensive) ------------------------------------------- + + +def _coerce_device_map(obj) -> DeviceMap | None: + """Accept DeviceMap objects, paths, or device-map/profile dicts; else None.""" + if isinstance(obj, DeviceMap): + return obj + if isinstance(obj, (str, os.PathLike)): + path = os.fspath(obj) + if os.path.isfile(path): + try: + return DeviceMap.load(path) + except GreenEyeError: + return None + return None + if isinstance(obj, dict): + if obj.get("schema") == "greeneye-device-map/1": + try: + return DeviceMap.from_dict(obj) + except (KeyError, TypeError, ValueError, GreenEyeError): + return None + if "device_map" in obj: + return _coerce_device_map(obj["device_map"]) + entries = obj.get("entries") + if isinstance(entries, list): + dm = DeviceMap(meta=dict(obj.get("meta") or {})) + try: + for raw in entries: + dm.add(DeviceEntry.from_dict(raw)) + except (KeyError, TypeError, ValueError): + return None + return dm + return None + + +def _json_rung(data) -> str | None: + if not isinstance(data, dict): + return None + for key in _RUNG_KEYS: + value = data.get(key) + if isinstance(value, str) and value in RUNG_INDEX: + return value + if isinstance(value, dict): + inner = value.get("highest") or value.get("rung") + if isinstance(inner, str) and inner in RUNG_INDEX: + return inner + return None + + +def discover_s1_device_model(image: str, sha: str | None = None) -> dict: + """Find S1's device model/profile without assuming it exists. + + Returns ``{"device_map": DeviceMap|None, "profile": path|None, + "declared_rung": str|None, "source": str|None, "notes": [...]}``. + Discovery order: an explicit ``sim.devices`` Python entry point, then JSON + files under ``sim/profiles/`` and ``sim/devices/`` (a + ``greeneye-sim-profile/1`` profile, a ``greeneye-device-map/1`` file, a + profile with a ``device_map`` key, or a raw ``entries`` list). A profile + is the semantic device model (``sim.gsp_boot`` runs it); a device map only + replays observed accesses. A candidate is preferred when its firmware + hash matches the image; otherwise a single candidate is used and noted as + unverified pairing. + """ + notes: list[str] = [] + declared_rung: str | None = None + + try: + import sim.devices as devices # type: ignore + + for name in S1_PY_API_NAMES: + fn = getattr(devices, name, None) + if not callable(fn): + continue + for args in ((sha,), (image,), ()): + if sha is None and args == (sha,): + continue + try: + candidate = fn(*args) + except TypeError: + continue + except (OSError, UnicodeError): + # A path-taking helper handed a digest (or a helper handed + # the binary firmware) simply does not apply; not an error. + break + except Exception as exc: # discovery must never break the run + notes.append(f"sim.devices.{name}{args!r} failed: {exc}") + break + if candidate is None: + continue + dm = _coerce_device_map(candidate) + rung = getattr(candidate, "rung", None) + if isinstance(rung, str) and rung in RUNG_INDEX: + declared_rung = rung + if dm is not None: + notes.append(f"device map via sim.devices.{name}{args!r}") + return { + "device_map": dm, + "profile": None, + "declared_rung": declared_rung, + "source": f"sim.devices.{name}", + "notes": notes, + } + except ImportError: + notes.append("sim.devices package absent") + except Exception as exc: + notes.append(f"sim.devices import failed: {exc}") + + candidates: list[tuple[str, DeviceMap, str | None, str | None]] = [] + profiles: list[tuple[str, str, str | None]] = [] + for rel in S1_JSON_DIRS: + root = os.path.join(TOOL_ROOT, rel) + if not os.path.isdir(root): + notes.append(f"{rel}/ absent") + continue + for name in sorted(os.listdir(root)): + if not name.endswith(".json"): + continue + path = os.path.join(root, name) + try: + with open(path, "r", encoding="utf-8") as fh: + data = json.load(fh) + except (OSError, ValueError) as exc: + notes.append(f"{rel}/{name}: unreadable ({exc})") + continue + if isinstance(data, dict) and data.get("schema") == "greeneye-sim-profile/1": + firmware = data.get("firmware") or {} + profiles.append((f"{rel}/{name}", path, + str(firmware.get("sha256") or "") or None)) + continue + dm = _coerce_device_map(data) + rung = _json_rung(data) + if rung and declared_rung is None: + declared_rung = rung + if dm is not None: + candidates.append((f"{rel}/{name}", dm, rung, None)) + + exact_profiles = [p for p in profiles if sha and p[2] == sha] + if exact_profiles: + rel, path, _digest = exact_profiles[0] + notes.append(f"using {rel} (greeneye-sim-profile/1, firmware_sha256 matched)") + return { + "device_map": None, + "profile": path, + "declared_rung": declared_rung, + "source": rel, + "notes": notes, + } + + if not candidates: + if len(profiles) == 1: + rel, path, _digest = profiles[0] + notes.append(f"using {rel} (single profile, pairing unverified)") + return { + "device_map": None, + "profile": path, + "declared_rung": declared_rung, + "source": rel, + "notes": notes, + } + notes.append("no S1 device-map/profile JSON found") + return {"device_map": None, "profile": None, "declared_rung": declared_rung, + "source": None, "notes": notes} + + exact = [ + c for c in candidates + if sha and str((c[1].meta or {}).get("firmware_sha256", "")) == sha + ] + chosen = exact[0] if exact else candidates[0] + note = "firmware_sha256 matched" if exact else "single candidate, pairing unverified" + notes.append(f"using {chosen[0]} ({note})") + return { + "device_map": chosen[1], + "profile": None, + "declared_rung": chosen[2] or declared_rung, + "source": chosen[0], + "notes": notes, + } + + +# -- native baseline ------------------------------------------------------- + + +def estimate_native_rung(run: dict, *, device_model_used: bool, + declared_rung: str | None = None) -> tuple[str, str]: + """Conservative rung estimate from a bounded native run. + + * matching lenient first touch -> B0 (observed, no device semantics); + * a device model that serves the QEMU first-touch and lets execution + continue -> B1 (frontier moved past the QEMU fault); + * a higher rung only when S1 declares it in profile data (honoured as + ``rung_source: declared``). + + B2+ need boot-stage milestones, not just progress; absence of a fault is + not evidence of a boot stage. See docs/37 sections 3-4. + """ + stop = run.get("stop") or {} + stop_reason = stop.get("reason") + first = run.get("first_unknown_mmio") + reached_b0 = bool( + (first and first.get("addr") == QEMU_REFERENCE["access_addr"]) + or run.get("instructions", 0) > 0 + ) + + inferred = None + basis = None + if device_model_used and reached_b0: + unmodelled_at_qemu = bool(first and first.get("addr") == QEMU_REFERENCE["access_addr"]) + if not unmodelled_at_qemu: + inferred = "B1" + basis = ( + "device model served the QEMU first-touch and execution continued " + "past the QEMU fault to the run bound" + ) + else: + inferred = "B0" + basis = "device model present but the QEMU first-touch was still served leniently" + elif reached_b0: + inferred = NATIVE_LENIENT_RUNG + basis = "lenient first-touch observation; execution continued past the QEMU fault" + + if inferred is None: + return "pre-B0", "no native progress observed" + + rung, source = inferred, "inferred" + declared_index = rung_ordinal(declared_rung) + if declared_index is not None and declared_index > RUNG_INDEX[inferred]: + rung, source = declared_rung, "declared" + basis = f"S1 profile declares {declared_rung}; inference was {inferred} ({basis})" + return rung, f"{basis} [rung_source={source}, stop={stop_reason}]" + + +def run_native_baseline( + image: str = DEFAULT_IMAGE, + out_dir: str = DEFAULT_OUT, + instructions: int = DEFAULT_INSTRUCTIONS, + repeat: int = 2, + device_map=None, + declared_rung: str | None = None, + profile: str | None = None, + use_shim: bool = True, +) -> dict: + """Run the bounded deterministic native baseline ``repeat`` times. + + ``profile`` selects the S1 semantic device model (``sim.gsp_boot`` with + the versioned profile and the recorded boot shim); without it the run is + the lenient ``sim.gsp_profile`` baseline. ``device_map`` remains a + legacy/observational MMIO map for the lenient path. + """ + if instructions <= 0 or instructions > MAX_INSTRUCTIONS: + raise HarnessError(f"instructions must be in (0, {MAX_INSTRUCTIONS}], got {instructions}") + if repeat < 1: + raise HarnessError(f"repeat must be >= 1, got {repeat}") + if not os.path.isfile(image): + return { + "backend": "native", + "ran": False, + "reason": f"firmware absent: {image}", + "runs": [], + } + + s1_model = bool(profile) or device_map is not None + runs: list[dict] = [] + start_all = time.monotonic() + for index in range(repeat): + session_dir = os.path.join(out_dir, "sessions", f"native-{index + 1}") + _rm_tree(session_dir) + start = time.monotonic() + if profile: + from sim.gsp_boot import run_boot # deferred: only the S1 path needs it + + boot = run_boot( + image=image, + profile_path=profile, + instructions=instructions, + record=session_dir, + name=f"s4-native-{index + 1}", + use_shim=use_shim, + ) + wall = time.monotonic() - start + if not boot.ran: + raise HarnessError(f"native boot refused to run: {boot.reason}") + run = { + "instructions": boot.instructions, + "cycles": boot.cycles, + "cycle_model": boot.cycle_model, + "stop": { + "reason": boot.stop_reason, + "pc": boot.stop_pc, + "pc_fw": boot.stop_pc_fw, + }, + "first_unknown_mmio": boot.first_unknown_mmio, + "mmio_unknown": boot.mmio_unknown, + "device_unhandled": boot.device_unhandled, + "device_accesses": boot.device_accesses, + "device_state_sha256": boot.device_state_sha256, + "milestones": list(boot.milestones), + "session": _relpath(session_dir), + "session_hashes": session_hashes(session_dir), + "wall_s": round(wall, 4), + } + run["rung"] = boot.rung + run["rung_basis"] = ( + "observed S1 boot-ladder rung from sim.gsp_boot " + f"(milestones={list(boot.milestones)}, mmio_unknown={boot.mmio_unknown}, " + f"device_unhandled={boot.device_unhandled})" + ) + else: + result = run_profile( + image=image, + instructions=instructions, + record=session_dir, + device_map=device_map, + name=f"s4-native-{index + 1}", + ) + wall = time.monotonic() - start + if not result.ran: + raise HarnessError(f"native profile refused to run: {result.reason}") + run = { + "instructions": result.instructions, + "cycles": result.cycles, + "cycle_model": result.cycle_model, + "stop": { + "reason": result.stop_reason, + "pc": result.stop_pc, + "pc_fw": result.stop_pc_fw, + }, + "first_unknown_mmio": result.first_unknown_mmio, + "mmio_unknown": result.mmio_unknown, + "session": _relpath(session_dir), + "session_hashes": session_hashes(session_dir), + "wall_s": round(wall, 4), + } + run["rung"], run["rung_basis"] = estimate_native_rung( + run, device_model_used=s1_model, declared_rung=declared_rung + ) + runs.append(run) + + keys = ( + "instructions", + "cycles", + "stop", + "first_unknown_mmio", + "mmio_unknown", + "rung", + "device_state_sha256", + "milestones", + ) + identical = all( + all(a.get(key) == runs[0].get(key) for key in keys) for a in runs[1:] + ) + compare = {"ran": False} + if repeat >= 2: + try: + text = compare_sessions( + os.path.join(out_dir, "sessions", "native-1"), + os.path.join(out_dir, "sessions", "native-2"), + ) + compare = { + "ran": True, + "chars": len(text), + "headers": [ + line[3:].strip() + for line in text.splitlines() + if line.startswith("## ") + ], + } + except Exception as exc: # compare is evidence, never a fatal step + compare = {"ran": False, "error": f"{type(exc).__name__}: {exc}"} + + return { + "backend": "native", + "ran": True, + "reason": "ok", + "instructions_budget": instructions, + "repeat": repeat, + "identical": identical, + "rung": runs[0]["rung"], + "rung_basis": runs[0]["rung_basis"], + "runs": runs, + "pair_wall_s": round(time.monotonic() - start_all, 4), + "compare": compare, + "device_model_used": s1_model, + "profile": _relpath(profile), + } + + +# -- native feature matrix ------------------------------------------------- + + +def _events_summary(session_dir: str) -> dict: + counts: dict[str, int] = {} + path = os.path.join(session_dir, "events.jsonl") + try: + with open(path, "r", encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if not line: + continue + try: + kind = json.loads(line).get("type") + except ValueError: + continue + if kind: + counts[kind] = counts.get(kind, 0) + 1 + except OSError: + pass + return dict(sorted(counts.items())) + + +def run_feature_matrix(out_dir: str = DEFAULT_OUT) -> dict: + """Exercise the native debugger feature set on the bundled demo firmware.""" + import greeneye as ge + + matrix: dict = {} + if not os.path.isfile(DEMO_ELF): + return { + "ran": False, + "reason": f"demo firmware absent: {DEMO_ELF} (run make demo)", + "features": {}, + } + + session_dir = os.path.join(out_dir, "sessions", "features") + _rm_tree(session_dir) + session = ge.launch(kernel=DEMO_ELF, backend="native", record=session_dir, name="s4-features") + try: + ctrl = session.symbols.lookup("gsp_ctrl") + session.run(duration=0.2) + magic = session.memory.read_u32(ctrl) + matrix["memory"] = { + "status": "pass" if magic == 0x47535031 else "fail", + "detail": {"ctrl": ctrl, "magic": magic}, + } + + with open(DEMO_SCHEMA, "r", encoding="utf-8") as fh: + schema = ge.schema_from_json(json.load(fh)) + decoded = schema.decode(session.memory.read(ctrl, schema.size), base=0) + matrix["struct"] = { + "status": "pass" if decoded.get("magic") == 0x47535031 else "fail", + "detail": {"schema": os.path.basename(DEMO_SCHEMA), "fields": len(decoded)}, + } + + dispatches: list[int] = [] + stores: list[int] = [] + session.hook("rpc_dispatch", on_enter=lambda hit: dispatches.append(hit.arg(0))) + session.watch(ctrl + 0x40, size=8, kind="w", callback=lambda hit: stores.append(hit.pc)) + session.memory.write_u64(ctrl + 0x28, 0x1234) + session.memory.write_u64(ctrl + 0x20, 1) + session.run(duration=0.25) + matrix["hooks"] = { + "status": "pass" if dispatches else "fail", + "detail": {"rpc_dispatch_hits": len(dispatches), "cmds": dispatches}, + } + matrix["watch"] = { + "status": "pass" if stores else "fail", + "detail": {"result_field_stores": len(stores), "pcs": stores[:4]}, + } + + session.memory.write_u64(ctrl + 0x28, 0x55) + session.memory.write_u64(ctrl + 0x20, 2) + hit = session.run_until("rpc_dispatch", timeout=1.0) + bp = session.breakpoint("rpc_dispatch") + attached = bool(bp.attached) + session.remove_breakpoint(bp) + matrix["breakpoints"] = { + "status": "pass" if hit is not None and attached and not bp.attached else "fail", + "detail": { + "run_until_hit": hit.addr if hit is not None else None, + "attached_then_removed": attached and not bp.attached, + "kind": "sw", + }, + } + finally: + session.close() + + events = _events_summary(session_dir) + matrix["sessions"] = { + "status": "pass" + if os.path.isfile(os.path.join(session_dir, "session.json")) + and os.path.isfile(os.path.join(session_dir, "events.jsonl")) + else "fail", + "detail": { + "dir": _relpath(session_dir), + "event_counts": events, + "hashes": session_hashes(session_dir), + }, + } + + trace_dir = os.path.join(out_dir, "sessions", "features-trace") + _rm_tree(trace_dir) + trace_session = ge.launch( + kernel=DEMO_ELF, + backend="native", + record=trace_dir, + name="s4-features-trace", + trace={"exec": True, "max_events": 4000}, + ) + try: + trace_session.run(duration=0.1) + finally: + trace_session.close() + trace_path = os.path.join(trace_dir, "blobs", "trace.jsonl") + trace_lines = 0 + if os.path.isfile(trace_path): + with open(trace_path, "r", encoding="utf-8", errors="replace") as fh: + trace_lines = sum(1 for _ in fh) + matrix["trace"] = { + "status": "pass" if trace_lines > 0 else "fail", + "detail": { + "dir": _relpath(trace_dir), + "blob": _relpath(trace_path), + "events": trace_lines, + "max_events": 4000, + "hashes": session_hashes(trace_dir), + }, + } + + if os.path.isfile(SDK_ELF): + from greeneye.inject import invoke_remote, load_service + + inject_dir = os.path.join(out_dir, "sessions", "features-inject") + _rm_tree(inject_dir) + inject_session = ge.launch( + kernel=DEMO_ELF, backend="native", record=inject_dir, name="s4-features-inject" + ) + try: + inject_session.run(duration=0.25) + service = load_service(inject_session, SDK_ELF, slot=6) + value = invoke_remote(inject_session, service, 1, 0x41) + finally: + inject_session.close() + matrix["injection"] = { + "status": "pass" if value == (0x41 ^ 0xBEEF) else "fail", + "detail": { + "dir": _relpath(inject_dir), + "blob": _relpath(SDK_ELF), + "slot": 6, + "ping_result": value, + "expected": 0x41 ^ 0xBEEF, + "hashes": session_hashes(inject_dir), + }, + } + else: + matrix["injection"] = { + "status": "not_run", + "reason": "sdk/build/injected_demo.elf not built (run make sdk)", + } + + matrix["mcp"] = { + "status": "not_run", + "reason": ( + "native MCP surface is S3's deliverable (plugins/mcp_server.py); " + "S4 must not race that work. Re-run after S3 lands (docs/37)." + ), + } + analyze_dir = os.path.join(out_dir, "sessions", "native-1") + compare_dir = os.path.join(out_dir, "sessions", "native-2") + if os.path.isfile(os.path.join(analyze_dir, "session.json")) and os.path.isfile( + os.path.join(compare_dir, "session.json") + ): + try: + from greeneye.analyze import render_summary + + summary = render_summary(analyze_dir) + delta_text = compare_sessions(analyze_dir, compare_dir) + matrix["analyze_compare"] = { + "status": "pass" if summary and delta_text else "fail", + "detail": { + "analyze_session": _relpath(analyze_dir), + "analyze_chars": len(summary), + "compare_pair": [_relpath(analyze_dir), _relpath(compare_dir)], + "compare_chars": len(delta_text), + "compare_headers": [ + line[3:].strip() + for line in delta_text.splitlines() + if line.startswith("## ") + ], + }, + } + except Exception as exc: # evidence step, never fatal to the matrix + matrix["analyze_compare"] = { + "status": "fail", + "reason": f"{type(exc).__name__}: {exc}", + } + else: + matrix["analyze_compare"] = { + "status": "not_run", + "reason": "native-1/native-2 sessions absent", + } + return {"ran": True, "reason": "ok", "features": matrix} + + +# -- report ---------------------------------------------------------------- + + +def threshold_checks(report: dict, thresholds: dict | None = None) -> dict: + """Evaluate the frozen thresholds against a report; returns check -> bool.""" + th = dict(FROZEN_THRESHOLDS) + th.update(thresholds or {}) + qemu = report.get("qemu") or {} + native = report.get("native") or {} + checks: dict = { + "qemu_first_fault_reproduced": bool( + qemu.get("ran") and qemu.get("first_touch_match") + ), + "native_above_qemu": above_qemu_line( + qemu.get("rung"), native.get("rung"), int(th["native_rungs_above_qemu"]) + ), + "identical_runs": bool( + native.get("ran") + and native.get("repeat", 0) >= int(th["identical_runs"]) + and native.get("identical") + ), + "native_pair_wall_s": bool( + native.get("ran") + and float(native.get("pair_wall_s", 1e9)) <= float(th["native_pair_wall_s"]) + ), + "device_model_documented": bool( + not th.get("device_model_required", True) or native.get("device_model_used") + ), + } + checks["all"] = all(checks.values()) + return checks + + +def build_report( + *, + image: str, + qemu: dict, + native: dict, + features: dict | None, + s1: dict, + thresholds: dict, +) -> dict: + checks = threshold_checks({"qemu": qemu, "native": native}, thresholds) + rung_progress = bool(checks["native_above_qemu"] and checks["identical_runs"]) + if not qemu.get("ran"): + verdict = "unverified_qemu_absent" + elif not checks["qemu_first_fault_reproduced"]: + verdict = "unverified_qemu_mismatch" + elif checks["all"]: + verdict = "above" + elif rung_progress and not checks["device_model_documented"]: + # Rung progress exists, but docs/35 1.1 also requires working device + # semantics; lenient MMIO cannot satisfy the line contract. Underclaim. + verdict = "above_lenient_only" + else: + verdict = "not_above" + return { + "schema": SCHEMA, + "harness": "harness/sim_qemu_line.py", + "line_definition": "docs/35-GREENEYE-SIMULATOR-DESIGN.md 1.1 + 3.3", + "firmware": { + "name": os.path.basename(image), + "sha256": firmware_sha256(image) if os.path.isfile(image) else None, + }, + "rung_order": list(RUNG_ORDER), + "thresholds": thresholds, + "threshold_checks": checks, + "verdict": verdict, + "verdict_explanation": { + "above": "all frozen checks passed: QEMU fault reproduced, native >= QEMU + 1 rung, identical runs, wall bound, documented device model", + "above_lenient_only": "native progressed past the QEMU fault, but lenient MMIO is not a documented device model (docs/35 1.1) - contract NOT satisfied", + "not_above": "one or more frozen checks failed and no rung-progress excuse applies", + "unverified_qemu_absent": "QEMU baseline unavailable; no A/B comparison is possible", + "unverified_qemu_mismatch": "QEMU ran but did not reproduce its documented first fault; the rungs are not comparable", + }.get(verdict, ""), + "rung_progress": rung_progress, + "above_qemu_line": verdict == "above", + "qemu": {k: v for k, v in qemu.items() if k != "device_map"}, + "native": native, + "features": features, + "s1_integration": { + "source": s1.get("source"), + "profile": _relpath(s1.get("profile")), + "declared_rung": s1.get("declared_rung"), + "notes": s1.get("notes", []), + }, + "x86_vms_running": _x86_vms_running(), + "not_verified": [ + "lenient MMIO (B0) is observation, not device semantics", + "B3/B4 are not reached: the S1 device model stops at the B2 DMEM-runspace unknown", + "the boot shim values (bootargs/WPR meta/DMEM mirror) are declared assumptions", + "MCP on native is S3-owned and not exercised here", + "L2 timing: cycles are modeled (docs/32), never silicon claims", + "no hardware/GPU/guest/module involvement by design", + ], + "rerun_when_s1_lands": [ + "python3 harness/sim_qemu_line.py", + "S1 landed (2026-10-03): sim/profiles/*.json is discovered and the native side runs sim.gsp_boot", + "sim/devices/*.json with greeneye-device-map/1 (or a device_map key) is still picked up as an observational map", + ], + } + + +def render_markdown(report: dict) -> str: + qemu = report.get("qemu") or {} + native = report.get("native") or {} + lines = [ + "# Simulator vs the QEMU line - measured report", + "", + f"* schema: `{report.get('schema')}`", + f"* harness: `{report.get('harness')}`", + f"* line: {report.get('line_definition')}", + f"* firmware: `{(report.get('firmware') or {}).get('name')}` " + f"sha256 `{((report.get('firmware') or {}).get('sha256') or '')[:16]}...`", + f"* verdict: **{report.get('verdict')}** " + f"(above_qemu_line={report.get('above_qemu_line')})", + f"* verdict meaning: {report.get('verdict_explanation') or '-'}", + "", + "## Rung table", + "", + "| side | ran | rung | instructions | cycles (modeled) | stop | wall s | evidence |", + "|---|---|---|---:|---:|---|---:|---|", + ] + runs = native.get("runs") or [] + lines.append( + f"| QEMU probe | {qemu.get('ran')} | {qemu.get('rung')} | - | - | " + f"{'fault' if qemu.get('first_fault') else '-'} | {qemu.get('wall_s')} | " + f"`{qemu.get('session') or qemu.get('reason')}` |" + ) + for index, run in enumerate(runs, start=1): + stop = run.get("stop") or {} + pc_fw = stop.get("pc_fw") + pc_fw_text = f"{pc_fw:#x}" if pc_fw is not None else "-" + lines.append( + f"| native run {index} | True | {run.get('rung')} | {run.get('instructions')} | " + f"{run.get('cycles')} | {stop.get('reason')}@{pc_fw_text} | " + f"{run.get('wall_s')} | `{run.get('session')}` |" + ) + if not runs: + lines.append(f"| native | {native.get('ran')} | - | - | - | - | - | {native.get('reason')} |") + + first = qemu.get("first_fault") or {} + lines += [ + "", + "### QEMU first fault", + "", + f"* {first.get('desc')} at fw vaddr `{first.get('epc_fw') if first.get('epc_fw') is not None else 0:#x}` " + f"accessing `{(first.get('access_addr') or 0):#x}` ({first.get('size')} bytes, {first.get('rw')})", + f"* first-touch matches the documented reference: {qemu.get('first_touch_match')}", + f"* fault records in the capped log window: {qemu.get('fault_count_window')}", + "", + "## Frozen thresholds", + "", + "| check | pass |", + "|---|---|", + ] + for name, value in (report.get("threshold_checks") or {}).items(): + lines.append(f"| {name} | {value} |") + lines += [ + "", + f"Thresholds (frozen {report['thresholds'].get('frozen_on')}): " + f"native >= QEMU + {report['thresholds'].get('native_rungs_above_qemu')} rung(s); " + f"{report['thresholds'].get('identical_runs')} identical runs; " + f"pair wall <= {report['thresholds'].get('native_pair_wall_s')} s.", + "", + "## Determinism", + "", + f"* identical across {native.get('repeat')} native runs: {native.get('identical')}", + f"* session compare ran: {(native.get('compare') or {}).get('ran')} " + f"({(native.get('compare') or {}).get('headers')})", + "", + "## Session hashes (sha256)", + "", + "| session | file | sha256 |", + "|---|---|---|", + ] + hash_rows = [("qemu-probe", qemu.get("session"), qemu.get("session_hashes") or {})] + for index, run in enumerate(runs, start=1): + hash_rows.append((f"native-{index}", run.get("session"), run.get("session_hashes") or {})) + features_detail = ((report.get("features") or {}).get("features") or {}) + for feature in ("sessions", "trace", "injection"): + detail = (features_detail.get(feature) or {}).get("detail") or {} + if isinstance(detail, dict): + hashes = detail.get("hashes") or {} + if hashes: + hash_rows.append((feature, detail.get("dir") or detail.get("blob"), hashes)) + for label, session, hashes in hash_rows: + if not hashes: + lines.append(f"| {label} | - | (no hashes recorded) |") + continue + for name in sorted(hashes): + lines.append(f"| {label} | {name} | `{hashes[name]}` |") + lines += [ + "", + "## Native feature matrix (demo firmware)", + "", + "| feature | status | detail |", + "|---|---|---|", + ] + feature_block = (report.get("features") or {}).get("features") or {} + for name in sorted(feature_block): + entry = feature_block[name] + detail = entry.get("detail") or entry.get("reason") or "" + lines.append(f"| {name} | {entry.get('status')} | {detail} |") + if not feature_block: + lines.append(f"| - | not_run | {(report.get('features') or {}).get('reason')} |") + lines += [ + "", + "## S1 integration", + "", + f"* device source: {report['s1_integration'].get('source') or 'lenient (no S1 device model found)'}", + f"* profile: {report['s1_integration'].get('profile') or '-'}", + f"* declared rung: {report['s1_integration'].get('declared_rung')}", + ] + for note in report["s1_integration"].get("notes") or []: + lines.append(f"* note: {note}") + lines += [ + "", + "## Not verified", + "", + ] + for item in report.get("not_verified") or []: + lines.append(f"* {item}") + lines += [ + "", + "## S1 integration notes", + "", + ] + for item in report.get("rerun_when_s1_lands") or []: + lines.append(f"* `{item}`" if item.startswith("python3") else f"* {item}") + lines += [ + "", + f"* other agents' x86 QEMU VMs observed (never touched): {report.get('x86_vms_running')}", + "", + ] + return "\n".join(lines) + + +def run_harness( + *, + image: str = DEFAULT_IMAGE, + out_dir: str = DEFAULT_OUT, + instructions: int = DEFAULT_INSTRUCTIONS, + qemu_duration: float = DEFAULT_QEMU_DURATION_S, + repeat: int = 2, + device_map: str | None = None, + features: bool = True, + thresholds: dict | None = None, + run_qemu: bool = True, +) -> dict: + """Run the full A/B harness and write ``sim-qemu-line.json`` + ``.md``.""" + os.makedirs(out_dir, exist_ok=True) + frozen = dict(FROZEN_THRESHOLDS) + frozen.update(thresholds or {}) + + if device_map is not None: + dm = DeviceMap.load(device_map) + s1 = { + "device_map": dm, + "profile": None, + "declared_rung": None, + "source": _relpath(device_map), + "notes": ["explicit --device-map override"], + } + else: + sha = firmware_sha256(image) if os.path.isfile(image) else None + s1 = discover_s1_device_model(image, sha) + + qemu = ( + run_qemu_baseline(image=image, out_dir=out_dir, duration=qemu_duration) + if run_qemu + else {"backend": "qemu", "ran": False, "reason": "qemu=False", "rung": None} + ) + + native = run_native_baseline( + image=image, + out_dir=out_dir, + instructions=instructions, + repeat=repeat, + device_map=s1.get("device_map"), + declared_rung=s1.get("declared_rung"), + profile=s1.get("profile"), + ) + if not native.get("ran"): + report = build_report( + image=image, qemu=qemu, native=native, features=None, s1=s1, thresholds=frozen + ) + else: + feature_result = run_feature_matrix(out_dir) if features else None + report = build_report( + image=image, + qemu=qemu, + native=native, + features=feature_result, + s1=s1, + thresholds=frozen, + ) + + json_path = _write_json(os.path.join(out_dir, "sim-qemu-line.json"), report) + md_path = _write_text(os.path.join(out_dir, "sim-qemu-line.md"), render_markdown(report)) + report["report_json"] = _relpath(json_path) + report["report_md"] = _relpath(md_path) + return report + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python3 harness/sim_qemu_line.py", + description=( + "S4 A/B: QEMU probe vs native GSP simulator on the real 570.144 image; " + "emits sim-qemu-line.json + .md. Bounded, no hardware." + ), + ) + parser.add_argument("--image", default=DEFAULT_IMAGE, help="flat GSP firmware image") + parser.add_argument("--out", default=DEFAULT_OUT, help="report directory") + parser.add_argument( + "--instructions", type=int, default=DEFAULT_INSTRUCTIONS, + help=f"native instruction budget per run (<= {MAX_INSTRUCTIONS})", + ) + parser.add_argument( + "--qemu-duration", type=float, default=DEFAULT_QEMU_DURATION_S, + help=f"QEMU wall-clock probe bound (<= {MAX_QEMU_DURATION_S}s)", + ) + parser.add_argument("--repeat", type=int, default=2, help="native runs for determinism") + parser.add_argument("--device-map", help="explicit greeneye-device-map/1 file (skips discovery)") + parser.add_argument("--no-qemu", action="store_true", help="native only (no baseline claim)") + parser.add_argument("--no-features", action="store_true", help="skip the feature matrix") + parser.add_argument("--json", action="store_true", help="print the report JSON") + args = parser.parse_args(argv) + + try: + report = run_harness( + image=args.image, + out_dir=args.out, + instructions=args.instructions, + qemu_duration=args.qemu_duration, + repeat=args.repeat, + device_map=args.device_map, + features=not args.no_features, + run_qemu=not args.no_qemu, + ) + except (HarnessError, GreenEyeError) as exc: + print(f"[sim-qemu-line] error: {exc}", file=sys.stderr) + return 1 + + if not os.path.isfile(args.image): + print(f"[sim-qemu-line] skipped: firmware absent: {args.image}", file=sys.stderr) + return 2 + if args.json: + print(json.dumps(report, indent=1, sort_keys=True, default=str)) + native = report.get("native") or {} + print(f"[sim-qemu-line] qemu rung: {(report.get('qemu') or {}).get('rung')}") + print(f"[sim-qemu-line] native rung: {native.get('rung')} (identical={native.get('identical')})") + print(f"[sim-qemu-line] verdict: {report.get('verdict')}") + print(f"[sim-qemu-line] thresholds: {report.get('threshold_checks')}") + print(f"[sim-qemu-line] report: {report.get('report_json')} + {report.get('report_md')}") + return 0 if report.get("above_qemu_line") else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/debug/risc-v-gsp-rm-vm/harness/triage.py b/debug/risc-v-gsp-rm-vm/harness/triage.py new file mode 100644 index 0000000..2175e48 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/triage.py @@ -0,0 +1,875 @@ +#!/usr/bin/env python3 +"""One-command new-version GSP firmware triage (docs/40, goal U46). + +For a NEW GSP firmware image (optionally against a ``--baseline``) this tool +answers, in one bounded run and with one report: + +* **what it is** - sha256, size, ELF shape and version label (explicit + ``--version``, the ``gsp_version.txt`` sidecar convention, parent directory, + file stem, in that order); +* **where it boots to** - the existing QEMU probe path (first exception / + first touch) and the S1 native boot ladder (``python3 -m sim.gsp_boot``, + rungs B0-B4); +* **how the native backend compares** - the existing ``harness/sim_qemu_line`` + A/B harness (QEMU vs native, deterministic repeat, frozen thresholds); +* **what changed vs the baseline** - the existing ``harness/version_diff`` + (image/symbol/corpus/probe/behavior sections), when ``--baseline`` is given; +* **what capability surface is present** - hooks/watch/memory/session API, + native simulator, TCG trace plugin, MCP, demo, boot ladder; +* **what work ``vgsp`` support needs** - a ``support_readiness`` verdict with + blockers and ordered next actions. + +Every step is wrapped: a failing step is recorded (``status: error``) and the +others still run. Every session a step uses is listed with sha256 hashes. +Nothing is downloaded; no hardware, GPU, mdev, kernel module or guest is +touched; QEMU is the only process launched besides the bounded native boot. + +This is the flagship workflow of the simulator program (handoff +``U46``) and remains **in development** (docs/40). The frozen value +thresholds below were recorded on 2026-10-03 *before* the first measurement +(docs/40 section 5); never move them silently. + +Run from the tool root:: + + python3 harness/triage.py --firmware ../../firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin --version 570.144 --out dumps/triage-570.144 + python3 harness/triage.py --firmware <new> --baseline <old> --out dumps/triage-570.211 --dry-run +""" +from __future__ import annotations + +import argparse +import importlib.util +import json +import os +import re +import shutil +import subprocess +import sys +import time +from dataclasses import dataclass + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOL_ROOT = os.path.dirname(HERE) +if TOOL_ROOT not in sys.path: + sys.path.insert(0, TOOL_ROOT) + +from harness.common import ( # noqa: E402 + DEMO_ELF, + ensure_dir, + qemu_available, + sha256_file, + write_json, + write_text, +) +from harness.sim_qemu_line import ( # noqa: E402 + MAX_INSTRUCTIONS, + run_harness, + run_qemu_baseline, + rung_ordinal, + session_hashes, +) +from harness.version_diff import ( # noqa: E402 + RM610_TOKEN, + Rm610GateError, + VersionDiffOptions, + check_rm610_gate, + run_version_diff, +) + +FORMAT = "greeneye-new-version-triage/1" +PLAN_FORMAT = "greeneye-new-version-triage-plan/1" + +#: Instruction budget bounds (task rule: <= 2M per attempt). +DEFAULT_BUDGET = 200_000 +#: QEMU probe wall-clock bound (matches the S4 harness default). +DEFAULT_QEMU_DURATION_S = 0.5 +#: Per-step wall-clock net (subprocess wedge protection; no unbounded runs). +DEFAULT_STEP_TIMEOUT_S = 300.0 +#: Native rung at which runtime observation (not just first touch) is possible. +OBSERVABLE_RUNG = "B2" + +#: Frozen 2026-10-03, before the first measured triage run (docs/40 section 5). +#: ``manual_baseline_s`` is the *conservative understatement* of the manual +#: steps this one command replaces (run+interpret probe, native boot ladder, +#: A/B, session hash collection and write-up for one new image). +FROZEN_THRESHOLDS = { + "frozen_on": "2026-10-03", + "manual_baseline_s": 1800.0, + "triage_wall_s_max": 600.0, + "required_steps": ("identity", "probe", "native_boot", "ab_eval", "features"), +} + +VERSION_RE = re.compile(r"v?(\d{3}\.\d+)([A-Za-z0-9._-]*)?") +_UNAVAILABLE_MARKERS = ( + "no module named", + "modulenotfounderror", + "importerror", + "cannot import", + "profile absent", + "image absent", + "unavailable", +) + + +# --------------------------------------------------------------------------- +# options and helpers +# --------------------------------------------------------------------------- + + +@dataclass +class TriageOptions: + firmware: str + baseline: str | None = None + version: str | None = None + baseline_label: str | None = None + out: str | None = None + budget: int = DEFAULT_BUDGET + qemu_duration: float = DEFAULT_QEMU_DURATION_S + step_timeout: float = DEFAULT_STEP_TIMEOUT_S + release: str | None = None + approval_token: str | None = None + dry_run: bool = False + + def labels(self) -> list[str]: + return [ + value + for value in ( + self.release, + self.version, + self.baseline_label, + os.path.basename(self.firmware) if self.firmware else None, + os.path.basename(self.baseline) if self.baseline else None, + ) + if value + ] + + +def _rm_tree(path: str) -> None: + if path and os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + + +def _rel(path: str, root: str) -> str: + try: + return os.path.relpath(path, root) + except ValueError: + return path + + +def _abs(path: str | None) -> str | None: + """Absolute, normalised path; session dirs from the harness may be tool-root relative.""" + if not path: + return None + return os.path.normpath(path if os.path.isabs(path) else os.path.join(TOOL_ROOT, path)) + + +def _first_line(text: str | None, limit: int = 300) -> str | None: + if not text: + return None + line = text.strip().splitlines()[-1].strip() if text.strip() else "" + return line[:limit] or None + + +def _looks_unavailable(text: str) -> bool: + lowered = (text or "").lower() + return any(marker in lowered for marker in _UNAVAILABLE_MARKERS) + + +def extract_version(firmware: str, explicit: str | None = None) -> tuple[str | None, str]: + """Version label: explicit, ``gsp_version.txt``, parent dir, file stem. + + The sidecar convention mirrors the existing probe/device-map helper + (``greeneye/device_map.py``); no firmware content is guessed. + """ + if explicit: + return explicit, "explicit --version" + sidecar = os.path.join(os.path.dirname(os.path.abspath(firmware)), "gsp_version.txt") + if os.path.isfile(sidecar): + try: + with open(sidecar, "r", encoding="utf-8", errors="replace") as fh: + text = "".join(ch for ch in fh.read() if ch.isprintable()).strip() + if text: + return text, "gsp_version.txt sidecar" + except OSError: + pass + parent = os.path.basename(os.path.dirname(os.path.abspath(firmware))) + if parent and VERSION_RE.fullmatch(parent): + return parent, "parent directory name" + stem = os.path.splitext(os.path.basename(firmware))[0] + return (stem or None), "file stem" + + +def _target_brief(path: str, label: str | None = None) -> dict: + info: dict = {"path": path, "name": os.path.basename(path)} + version, version_source = extract_version(path, label) + info["version"] = version + info["version_source"] = version_source + if os.path.isfile(path): + info["sha256"] = sha256_file(path) + info["size"] = os.path.getsize(path) + else: + info["error"] = "file not found" + return info + + +# --------------------------------------------------------------------------- +# steps +# --------------------------------------------------------------------------- + + +def step_identity(options: TriageOptions, out_dir: str) -> dict: + firmware = _target_brief(options.firmware, None) + version, version_source = extract_version(options.firmware, options.version) + firmware["version"] = version + firmware["version_source"] = version_source + if not os.path.isfile(options.firmware): + return { + "status": "error", + "reason": f"firmware not found: {options.firmware}", + "firmware": firmware, + } + result: dict = {"status": "ok", "firmware": firmware} + try: + from greeneye.elf import load_elf + + image = load_elf(options.firmware) + result["elf"] = { + "is_riscv": bool(image.is_riscv), + "entry": image.entry, + "min_vaddr": image.min_vaddr, + "max_vaddr": image.max_vaddr, + "segments": len(getattr(image, "segments", ()) or ()), + } + except Exception as exc: + result["elf"] = {"unreadable": f"{type(exc).__name__}: {exc}"} + if options.baseline: + result["baseline"] = _target_brief(options.baseline, options.baseline_label) + return result + + +def step_probe(options: TriageOptions, out_dir: str) -> dict: + result = run_qemu_baseline( + image=options.firmware, out_dir=out_dir, duration=options.qemu_duration + ) + detail: dict = { + "status": "ok" if result.get("ran") else "skipped", + "reason": result.get("reason"), + "rung": result.get("rung"), + "stopped_pc": result.get("stopped_pc"), + "first_fault": result.get("first_fault"), + "first_touch_match": result.get("first_touch_match"), + "fault_count_window": result.get("fault_count_window"), + "device_map_entries": result.get("device_map_entries"), + "wall_s": result.get("wall_s"), + "sessions": [], + } + session = _abs(result.get("session")) + if session: + detail["sessions"].append({"name": "qemu-probe", "dir": session}) + detail["session_hashes"] = result.get("session_hashes") or session_hashes(session) + return detail + + +def step_native_boot(options: TriageOptions, out_dir: str) -> dict: + session_dir = os.path.join(out_dir, "sessions", "native-boot") + _rm_tree(session_dir) + argv = [ + sys.executable, + "-m", + "sim.gsp_boot", + "--image", + options.firmware, + "--instructions", + str(options.budget), + "--record", + session_dir, + "--name", + "triage-native-boot", + "--json", + ] + try: + proc = subprocess.run( + argv, + cwd=TOOL_ROOT, + capture_output=True, + text=True, + timeout=options.step_timeout, + ) + except subprocess.TimeoutExpired: + return {"status": "error", "reason": f"native boot timed out after {options.step_timeout}s"} + except OSError as exc: + return {"status": "skipped", "reason": f"native boot ladder unavailable: {exc}"} + + stdout, stderr = proc.stdout or "", proc.stderr or "" + payload = None + if stdout.strip(): + try: + payload = json.loads(stdout) + except ValueError: + payload = None + if proc.returncode == 0 and isinstance(payload, dict): + session = _abs(payload.get("session")) or session_dir + return { + "status": "ok", + "rung": payload.get("rung"), + "instructions_executed": payload.get("instructions_executed"), + "cycles": payload.get("cycles"), + "stop": payload.get("stop"), + "first_unknown_mmio": payload.get("first_unknown_mmio"), + "mmio_unknown": payload.get("mmio_unknown"), + "device_accesses": payload.get("device_accesses"), + "milestones": payload.get("milestones"), + "sessions": [{"name": "native-boot", "dir": session}], + "session_hashes": session_hashes(session), + } + reason = _first_line(stderr) or _first_line(stdout) or f"exit {proc.returncode}" + status = "skipped" if _looks_unavailable(stderr + stdout) or proc.returncode == 2 else "error" + if os.path.isdir(session_dir) and os.listdir(session_dir): + return { + "status": status, + "reason": reason, + "sessions": [{"name": "native-boot", "dir": session_dir}], + "session_hashes": session_hashes(session_dir), + } + return {"status": status, "reason": reason} + + +def step_ab_eval(options: TriageOptions, out_dir: str) -> dict: + if not os.path.isfile(options.firmware): + return {"status": "skipped", "reason": f"firmware absent: {options.firmware}"} + ab_dir = os.path.join(out_dir, "ab-eval") + report = run_harness( + image=options.firmware, + out_dir=ab_dir, + instructions=options.budget, + qemu_duration=options.qemu_duration, + repeat=2, + features=False, + run_qemu=True, + ) + qemu = report.get("qemu") or {} + native = report.get("native") or {} + detail: dict = { + "status": "ok" if native.get("ran") else "skipped", + "reason": native.get("reason") if not native.get("ran") else None, + "qemu_rung": qemu.get("rung"), + "native_rung": native.get("rung"), + "native_identical": native.get("identical"), + "native_repeat": native.get("repeat"), + "above_qemu_line": report.get("above_qemu_line"), + "verdict": report.get("verdict"), + "threshold_checks": report.get("threshold_checks"), + "report_json": _abs(report.get("report_json")), + "report_md": _abs(report.get("report_md")), + "sessions": [], + } + qemu_session = _abs(qemu.get("session")) + if qemu_session: + detail["sessions"].append({"name": "ab-qemu", "dir": qemu_session}) + for index, run in enumerate(native.get("runs") or [], start=1): + session = _abs(run.get("session")) or os.path.join(ab_dir, "sessions", f"native-{index}") + detail["sessions"].append({"name": f"ab-native-{index}", "dir": session}) + return detail + + +def step_version_diff(options: TriageOptions, out_dir: str) -> dict: + if not options.baseline: + return {"status": "skipped", "reason": "no --baseline supplied"} + if not os.path.isfile(options.baseline): + return {"status": "skipped", "reason": f"baseline absent: {options.baseline}"} + vd_dir = os.path.join(out_dir, "version-diff") + old_version, _ = extract_version(options.baseline, options.baseline_label) + report = run_version_diff( + VersionDiffOptions( + old=options.baseline, + new=options.firmware, + out=vd_dir, + old_label=options.baseline_label or old_version, + new_label=options.version, + release=options.release, + probe_instructions=max(1, min(options.budget, 2000)), + behavior_duration=0.4, + approval_token=options.approval_token, + ) + ) + sections = {name: section.get("status") for name, section in report["sections"].items()} + symbols = report["sections"].get("symbols") or {} + detail: dict = { + "status": "error" if "error" in sections.values() else "ok", + "reason": None, + "old_label": report.get("old", {}).get("label"), + "new_label": report.get("new", {}).get("label"), + "sections": sections, + "sections_skipped": report.get("summary", {}).get("sections_skipped"), + "sections_error": report.get("summary", {}).get("sections_error"), + "report_json": os.path.join(vd_dir, "version-diff.json"), + "report_md": os.path.join(vd_dir, "version-diff.md"), + "image_changes": _image_changes(report), + "symbol_changes": _symbol_changes(symbols), + "sessions": [], + } + sessions_root = os.path.join(vd_dir, "sessions") + if os.path.isdir(sessions_root): + for name in sorted(os.listdir(sessions_root)): + path = os.path.join(sessions_root, name) + if os.path.isdir(path): + detail["sessions"].append({"name": f"version-diff/{name}", "dir": path}) + return detail + + +def _image_changes(report: dict) -> dict: + section = (report.get("sections") or {}).get("image") or {} + return { + "status": section.get("status"), + "same": section.get("same"), + "deltas": section.get("deltas"), + } + + +def _symbol_changes(symbols: dict) -> dict: + totals = symbols.get("totals") or {} + return { + "status": symbols.get("status"), + "reason": symbols.get("reason"), + "shift": symbols.get("shift"), + "matched_named": symbols.get("matched_named"), + "matched_shifted": symbols.get("matched_shifted"), + "added": totals.get("added"), + "removed": totals.get("removed"), + "moved": totals.get("moved"), + "resized_named": totals.get("resized_named"), + } + + +def _spec_available(module: str) -> bool: + try: + return importlib.util.find_spec(module) is not None + except (ImportError, ValueError): + return False + + +def step_features(options: TriageOptions, out_dir: str) -> dict: + checks: dict = {} + notes: list[str] = [] + try: + import greeneye as ge + + checks["greeneye_version"] = ge.__version__ + checks["hooks_watch_step"] = all( + hasattr(ge.Session, name) for name in ("hook", "watch", "run", "step", "close") + ) + checks["memory_struct"] = hasattr(ge, "Memory") + checks["native_backend"] = _spec_available("greeneye.sim_backend") + except Exception as exc: + notes.append(f"greeneye import failed: {type(exc).__name__}: {exc}") + checks.update({"greeneye_version": None, "hooks_watch_step": False, "memory_struct": False, "native_backend": False}) + checks["native_boot_ladder"] = os.path.isfile(os.path.join(TOOL_ROOT, "sim", "gsp_boot.py")) and os.path.isdir( + os.path.join(TOOL_ROOT, "sim", "profiles") + ) + checks["ab_harness"] = os.path.isfile(os.path.join(TOOL_ROOT, "harness", "sim_qemu_line.py")) + checks["version_diff"] = os.path.isfile(os.path.join(TOOL_ROOT, "harness", "version_diff.py")) + checks["tcg_trace_plugin"] = os.path.isfile(os.path.join(TOOL_ROOT, "qemu", "greeneye-trace.so")) + checks["mcp_server"] = os.path.isfile(os.path.join(TOOL_ROOT, "plugins", "mcp_server.py")) + checks["demo_firmware"] = os.path.isfile(DEMO_ELF) + checks["qemu"] = qemu_available() + unavailable = sorted(name for name, value in checks.items() if value is False) + if unavailable: + notes.append("unavailable: " + ", ".join(unavailable)) + return {"status": "ok", "checks": checks, "unavailable": unavailable, "notes": notes} + + +DEFAULT_RUNNERS = ( + ("identity", step_identity), + ("probe", step_probe), + ("native_boot", step_native_boot), + ("ab_eval", step_ab_eval), + ("version_diff", step_version_diff), + ("features", step_features), +) + + +# --------------------------------------------------------------------------- +# readiness and report assembly +# --------------------------------------------------------------------------- + + +def support_readiness(report: dict) -> dict: + """Blockers and next actions derived from the recorded step results.""" + steps = {step["name"]: step for step in report["steps"]} + blockers: list[str] = [] + next_actions: list[str] = [] + identity = steps.get("identity", {}) + if identity.get("status") != "ok": + blockers.append(f"firmware identity failed: {identity.get('reason') or 'unreadable'}") + probe = steps.get("probe", {}) + if probe.get("status") == "error": + blockers.append(f"QEMU probe failed: {probe.get('reason')}") + native = steps.get("native_boot", {}) + native_rung = native.get("rung") + native_ok = native.get("status") == "ok" + if not native_ok: + next_actions.append( + "native boot ladder unavailable/again incomplete " + f"({native.get('reason') or native.get('status')}): the S1 device model/profile is the enabler" + ) + elif rung_ordinal(native_rung) is not None and rung_ordinal(native_rung) >= rung_ordinal(OBSERVABLE_RUNG): + next_actions.append( + f"native ladder is at {native_rung} (runtime-observable): inspect the milestone/stop with " + "hooks and watchpoints, then route the finding through the accuracy loop" + ) + else: + next_actions.append( + f"native ladder stops at {native_rung}: extend the device model past the next unknown MMIO " + "before claiming runtime observation" + ) + ab = steps.get("ab_eval", {}) + if ab.get("status") == "ok" and not ab.get("above_qemu_line"): + next_actions.append( + f"native A/B is not yet above the QEMU line (verdict={ab.get('verdict')}): " + "device model and documented shims gate the claim" + ) + vd = steps.get("version_diff", {}) + baseline_supplied = bool(report.get("input", {}).get("baseline_supplied")) + if vd.get("status") == "error": + blockers.append(f"version diff errored: {vd.get('sections_error') or vd.get('reason')}") + elif vd.get("status") == "ok" and baseline_supplied: + skipped = vd.get("sections_skipped") or {} + if "symbols" in skipped: + next_actions.append( + "symbol inventory skipped: supply Ghidra exports / symbol DBs so the handler " + "inventory delta can gate the vgsp support scope" + ) + if "corpus" in skipped: + next_actions.append( + "corpus diff skipped: supply --corpus-diff indexes for decompiled-change hints" + ) + else: + next_actions.append( + "re-run with --baseline <previous.fwimage> for the symbol/behavior delta " + "(the release-to-support gate needs the A/B)" + ) + features = steps.get("features", {}) + for name in features.get("unavailable") or []: + if name in ("qemu", "native_boot_ladder", "hooks_watch_step", "memory_struct"): + next_actions.append(f"capability unavailable: {name}") + if not blockers and native_ok and rung_ordinal(native_rung) is not None and rung_ordinal(native_rung) >= rung_ordinal(OBSERVABLE_RUNG): + verdict = "sim-observable" + elif not blockers: + verdict = "first-touch-only" + else: + verdict = "blocked" + return { + "verdict": verdict, + "baseline_compared": bool(baseline_supplied and vd.get("status") == "ok"), + "observable_rung": OBSERVABLE_RUNG, + "blockers": blockers, + "next_actions": next_actions, + } + + +def _collect_sessions(steps: list[dict], out_dir: str) -> list[dict]: + rows: list[dict] = [] + seen: set[str] = set() + for step in steps: + for entry in step.get("sessions") or []: + path = _abs(entry.get("dir")) + if not path or path in seen: + continue + seen.add(path) + hashes = step.get("session_hashes") or {} + rows.append( + { + "step": step["name"], + "name": entry.get("name") or step["name"], + "dir": _rel(path, out_dir), + "present": os.path.isdir(path), + "hashes": hashes or session_hashes(path), + } + ) + return rows + + +def render_markdown(report: dict) -> str: + firmware = report["input"].get("firmware") or {} + steps = report["steps"] + readiness = report["support_readiness"] + lines = [ + f"# New-version triage — {firmware.get('version') or firmware.get('name')}", + "", + "> Status: in development (docs/40). Simulator-only: no hardware, GPU, mdev,", + "> kernel module or guest was used. Frozen thresholds were recorded before the run.", + "", + f"* format: `{report['format']}`", + f"* generated: {report['generated_at']}", + f"* wall: {report['wall_s']}s (budget {report['input'].get('budget')} instructions/attempt)", + f"* RM610 gate: {report['rm610_gate']['status']}", + "", + "## Firmware", + "", + "| field | value |", + "|---|---|", + f"| path | `{firmware.get('path')}` |", + f"| sha256 | `{firmware.get('sha256')}` |", + f"| size | {firmware.get('size')} |", + f"| version | {firmware.get('version')} ({firmware.get('version_source')}) |", + f"| ELF | {json.dumps(report['input'].get('elf') or {}, sort_keys=True)} |", + ] + baseline = report["input"].get("baseline") + if baseline: + lines += [ + f"| baseline | `{baseline.get('name')}` {baseline.get('sha256')} |", + f"| baseline version | {baseline.get('version')} ({baseline.get('version_source')}) |", + ] + lines += ["", "## Steps", "", "| step | status | wall_s | key result |", "|---|---|---|---|"] + for step in steps: + key = ( + step.get("reason") + or step.get("verdict") + or step.get("rung") + or step.get("status") + ) + lines.append(f"| {step['name']} | {step['status']} | {step.get('wall_s')} | {str(key)[:120]} |") + lines += [ + "", + "## Support readiness", + "", + f"* verdict: **{readiness['verdict']}**", + f"* baseline compared: {readiness['baseline_compared']}", + ] + for label, items in (("blockers", readiness["blockers"]), ("next actions", readiness["next_actions"])): + lines.append(f"* {label}:") + if not items: + lines.append(" * none") + for item in items: + lines.append(f" * {item}") + sessions = report.get("sessions") or [] + lines += ["", "## Sessions used", "", "| step | name | dir | session.json sha256 |", "|---|---|---|---|"] + if not sessions: + lines.append("| - | - | - | (none recorded) |") + for row in sessions: + sha = (row.get("hashes") or {}).get("session.json") or "-" + lines.append(f"| {row['step']} | {row['name']} | `{row['dir']}` | `{sha}` |") + checks = report["threshold_checks"] + lines += [ + "", + "## Value (frozen thresholds, docs/40 section 5)", + "", + "| metric | value |", + "|---|---|", + f"| triage wall_s | {checks['triage_wall_s']} |", + f"| wall_s max | {checks['triage_wall_s_max']} |", + f"| within max | {checks['wall_within_max']} |", + f"| manual baseline_s | {checks['manual_baseline_s']} |", + f"| speedup_x | {checks['speedup_x']} |", + f"| beats manual baseline | {checks['beats_manual_baseline']} |", + "", + "## Artifacts", + "", + ] + for key in ("report_json", "report_md"): + lines.append(f"* {key}: `{report['artifacts'][key]}`") + lines.append("") + return "\n".join(lines) + + +def run_triage(options: TriageOptions, runners=None) -> dict: + """Run every step (wrapped), write ``triage-report.json|.md``, return it.""" + check_rm610_gate(options.labels(), approval_token=options.approval_token) + if not options.out: + raise ValueError("--out is required unless --dry-run is used") + if options.budget <= 0 or options.budget > MAX_INSTRUCTIONS: + raise ValueError(f"--budget must be in 1..{MAX_INSTRUCTIONS}, got {options.budget}") + out = os.path.abspath(options.out) + ensure_dir(out) + started = time.monotonic() + steps: list[dict] = [] + for name, fn in runners or DEFAULT_RUNNERS: + step_started = time.monotonic() + try: + detail = fn(options, out) + if not isinstance(detail, dict): + detail = {"status": "ok", "result": detail} + except Exception as exc: + detail = {"status": "error", "reason": f"{type(exc).__name__}: {exc}"} + entry = {"name": name, "wall_s": round(time.monotonic() - step_started, 3)} + entry.update(detail) + steps.append(entry) + wall_s = round(time.monotonic() - started, 3) + report: dict = { + "format": FORMAT, + "tool": "harness/triage.py", + "dry_run": False, + "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "rm610_gate": {"status": "passed", "labels": sorted(set(options.labels()))}, + "input": { + "firmware": _target_brief(options.firmware, options.version), + "baseline": _target_brief(options.baseline, options.baseline_label) if options.baseline else None, + "baseline_supplied": options.baseline is not None, + "budget": options.budget, + "qemu_duration": options.qemu_duration, + "triage_command": ( + "python3 harness/triage.py --firmware <firmware>" + + (" --baseline <baseline>" if options.baseline else "") + + (f" --version {options.version}" if options.version else "") + + f" --budget {options.budget} --out <out>" + ), + }, + "thresholds": dict(FROZEN_THRESHOLDS), + "steps": steps, + "support_readiness": {}, + "wall_s": wall_s, + "artifacts": {}, + } + firmware = report["input"]["firmware"] + if firmware.get("error"): + report["input"]["firmware"]["status"] = "error" + report["input"]["elf"] = next((s.get("elf") for s in steps if s["name"] == "identity"), None) + report["errors"] = [step["name"] for step in steps if step["status"] == "error"] + report["sessions"] = _collect_sessions(steps, out) + report["support_readiness"] = support_readiness(report) + checks = { + "triage_wall_s": wall_s, + "triage_wall_s_max": FROZEN_THRESHOLDS["triage_wall_s_max"], + "wall_within_max": wall_s <= FROZEN_THRESHOLDS["triage_wall_s_max"], + "manual_baseline_s": FROZEN_THRESHOLDS["manual_baseline_s"], + "speedup_x": round(FROZEN_THRESHOLDS["manual_baseline_s"] / wall_s, 1) if wall_s > 0 else None, + "beats_manual_baseline": wall_s < FROZEN_THRESHOLDS["manual_baseline_s"], + "required_steps_missing": [ + name + for name in FROZEN_THRESHOLDS["required_steps"] + if steps_by_name(steps, name).get("status") not in ("ok", "skipped") + ], + } + report["threshold_checks"] = checks + artifacts = { + "report_json": os.path.join(out, "triage-report.json"), + "report_md": os.path.join(out, "triage-report.md"), + } + report["artifacts"] = artifacts + write_json(artifacts["report_json"], report) + write_text(artifacts["report_md"], render_markdown(report)) + return report + + +def steps_by_name(steps: list[dict], name: str) -> dict: + return next((step for step in steps if step["name"] == name), {}) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def build_plan(options: TriageOptions) -> dict: + out = os.path.abspath(options.out) if options.out else "<out>" + return { + "format": PLAN_FORMAT, + "tool": "harness/triage.py", + "dry_run": True, + "status": "planned", + "rm610_gate": {"status": "passed", "labels": sorted(set(options.labels()))}, + "firmware": {"path": options.firmware, "version": options.version or "auto"}, + "baseline": options.baseline, + "out": out, + "budget": options.budget, + "steps": [ + {"name": name, "budget": options.budget if name in ("native_boot", "ab_eval") else None} + for name, _fn in DEFAULT_RUNNERS + ], + "side_effects": { + "writes": [ + os.path.join(out, "triage-report.json"), + os.path.join(out, "triage-report.md"), + os.path.join(out, "sessions/"), + os.path.join(out, "ab-eval/"), + os.path.join(out, "version-diff/ (only with --baseline)"), + ], + "hardware": "none", + "network": "none", + "max_instructions_per_attempt": MAX_INSTRUCTIONS, + }, + } + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="risc-v-gsp-rm-debugger triage", description=__doc__.splitlines()[0]) + parser.add_argument("--firmware", required=True, help="new GSP firmware image (read-only)") + parser.add_argument("--baseline", help="previous firmware image for the version diff (optional)") + parser.add_argument("--version", help="version label (default: gsp_version.txt / dir / stem)") + parser.add_argument("--baseline-label", help="version label recorded for the baseline") + parser.add_argument("--out", help="report directory (required unless --dry-run)") + parser.add_argument("--budget", type=int, default=DEFAULT_BUDGET, + help=f"instruction budget per native attempt (1..{MAX_INSTRUCTIONS})") + parser.add_argument("--qemu-duration", type=float, default=DEFAULT_QEMU_DURATION_S, + help="QEMU probe wall-clock bound in seconds") + parser.add_argument("--step-timeout", type=float, default=DEFAULT_STEP_TIMEOUT_S, + help="per-step wall-clock net in seconds") + parser.add_argument("--release", help="release label; an rm610 release is gated by docs/23") + parser.add_argument("--approval-token", help=f"explicit RM610 token ({RM610_TOKEN})") + parser.add_argument("--dry-run", action="store_true", + help="print the plan and exit without opening targets or creating --out") + return parser + + +def options_from_args(args: argparse.Namespace) -> TriageOptions: + return TriageOptions( + firmware=args.firmware, + baseline=args.baseline, + version=args.version, + baseline_label=args.baseline_label, + out=args.out, + budget=args.budget, + qemu_duration=args.qemu_duration, + step_timeout=args.step_timeout, + release=args.release, + approval_token=args.approval_token, + dry_run=args.dry_run, + ) + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + options = options_from_args(args) + try: + check_rm610_gate(options.labels(), approval_token=options.approval_token) + except Rm610GateError as exc: + print(f"[triage] RM610 gate refused: {exc}", file=sys.stderr) + return 3 + if options.dry_run: + plan = build_plan(options) + print(json.dumps(plan, indent=2, sort_keys=True)) + print(f"[triage] dry-run: {len(plan['steps'])} steps planned; no targets opened, no --out created") + return 0 + if not options.out: + parser.error("--out is required unless --dry-run is used") + if options.budget <= 0 or options.budget > MAX_INSTRUCTIONS: + parser.error(f"--budget must be in 1..{MAX_INSTRUCTIONS}") + if not os.path.isfile(options.firmware): + print(f"[triage] skipped: firmware absent: {options.firmware}", file=sys.stderr) + return 2 + try: + report = run_triage(options) + except Rm610GateError as exc: + print(f"[triage] RM610 gate refused: {exc}", file=sys.stderr) + return 3 + except ValueError as exc: + parser.error(str(exc)) + except Exception as exc: + print(f"[triage] error: {type(exc).__name__}: {exc}", file=sys.stderr) + return 2 + for step in report["steps"]: + reason = f" reason={step['reason']}" if step.get("reason") else "" + print(f"[triage] {step['name']}: status={step['status']}{reason}") + readiness = report["support_readiness"] + print(f"[triage] readiness: {readiness['verdict']} (baseline_compared={readiness['baseline_compared']})") + for action in readiness["next_actions"][:6]: + print(f"[triage] next: {action}") + checks = report["threshold_checks"] + print(f"[triage] wall: {checks['triage_wall_s']}s vs manual baseline {checks['manual_baseline_s']}s " + f"(speedup {checks['speedup_x']}x)") + print(f"[triage] wrote {report['artifacts']['report_json']} + {report['artifacts']['report_md']}") + return 1 if report["errors"] else 0 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py b/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py index 0059da2..5acce65 100644 --- a/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py +++ b/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py @@ -29,6 +29,7 @@ import argparse import hashlib import json import os +import shutil import socket import subprocess import sys @@ -58,8 +59,12 @@ _FWIMAGE = os.path.join( ) MUTATION_SCRIPT = os.path.join(_TOOL_ROOT, "harness", "mutation_harness.py") MATRIX_SCRIPT = os.path.join(_TOOL_ROOT, "harness", "portability_matrix.py") +SERVICE_ELF = os.path.join(_TOOL_ROOT, "sdk", "build", "injected_demo.elf") LOOPBACK_HOSTS = ("127.0.0.1", "localhost", "::1") +#: Firmware digests are cached by (path, size, mtime) - the real image is 28 MiB. +_SHA_CACHE: dict[tuple, str | None] = {} + #: Per-response size caps so a single tool call can never flood a client. MAX_READ_BYTES = 1 << 20 MAX_HEXDUMP_BYTES = 1 << 20 @@ -138,6 +143,21 @@ def _tool(name: str, description: str, properties: dict | None = None, required: TOOLS: list[dict] = [ _tool("session_info", "Active session state: backend, PC, symbols, recording, breakpoints/watchpoints, enabled capabilities."), _tool( + "backends_list", + "List the execution backends this server can launch (QEMU and the native GSP simulator) and the active one.", + ), + _tool( + "session_launch", + "Launch a new session on a chosen backend (protocol-level backend selection) and serve it. Standalone mode only.", + { + "backend": {"type": "string", "enum": ["native", "qemu"], "default": "native", "description": "execution backend"}, + "kernel": _string("firmware ELF (default: bundled demo when built)"), + "record": {"type": ["boolean", "string"], "default": True, "description": "record to dumps/ (true) or an explicit directory"}, + "name": _string("session name", "mcp"), + "trace": _boolean("native only: enable the exec/access trace at launch", False), + }, + ), + _tool( "symbols_resolve", "Resolve a symbol name, 'symbol+0xNN' expression or hex address against the session symbol DB.", {"target": _address("symbol name, symbol+offset or address")}, @@ -222,6 +242,10 @@ TOOLS: list[dict] = [ ("service",), ), _tool( + "service_list", + "List services injected into this session (injection SDK registry) and the demo handler slot table.", + ), + _tool( "trace_start", "Start the native exec/access trace (native backend, recording session only).", { @@ -232,6 +256,24 @@ TOOLS: list[dict] = [ }, ), _tool("trace_stop", "Stop the native trace, flush it to blobs/trace.jsonl and return event counts."), + _tool( + "sim_cycles", + "Native simulator counters: instruction count, modeled cycle total, the cycle-model table and its fidelity label.", + ), + _tool( + "sim_device_state", + "Native simulator device state: unknown-MMIO policy/log, device-map size, and the sim/devices model when present.", + {"limit": _integer("most recent unmodelled MMIO entries to include", 16, 0, 1000)}, + ), + _tool( + "sim_profile", + "Native simulator boot/profile metadata: firmware, bias, stop info, trace config, model identity and fidelity.", + ), + _tool( + "sim_regions", + "Native simulator memory-region metadata: RAM/UART windows, loaded firmware segments, device-map entries and observed unmodelled MMIO pages.", + {"limit": _integer("entry cap for the device-map and observed-page lists", 16, 0, 1000)}, + ), _tool("analyze_current", "Render the symbol-annotated summary of the active recording.", {"max_rows": _integer("table row cap", 40, 1, 1000)}), _tool( "compare_sessions", @@ -493,9 +535,16 @@ class GreenEyeMCPServer: # -- tools: session ---------------------------------------------------- + def _active_backend(self) -> str: + if hasattr(self.session.client, "trace"): + return "native" + if self.session.target is not None: + return "qemu" + return "custom" + def tool_session_info(self, args: dict) -> dict: session = self.session - backend = "native" if hasattr(session.client, "trace") else ("qemu" if session.target is not None else "custom") + backend = self._active_backend() return { "name": session.name, "backend": backend, @@ -526,6 +575,76 @@ class GreenEyeMCPServer: sym = self.session.symbols.resolve(addr) return {"address": addr, "symbol": sym.name if sym else None, "annotation": self._annotate(addr)} + def tool_backends_list(self, args: dict) -> dict: + try: + import greeneye.sim_backend # noqa: F401 + + native_available, native_note = True, None + except Exception as exc: # pragma: no cover - only on a broken install + native_available, native_note = False, str(exc) + qemu_binary = shutil.which("qemu-system-riscv64") or os.environ.get("GREENEYE_QEMU") + return { + "active": self._active_backend(), + "launchable": self.owns_session, + "backends": [ + { + "name": "native", + "available": native_available, + "transport": "in-process deterministic RV64IMAC simulator (sim_backend.launch_native)", + "features": ["cycles", "trace", "device_state", "injection", "deterministic"], + "note": native_note, + }, + { + "name": "qemu", + "available": bool(qemu_binary), + "transport": "qemu-system-riscv64 gdbstub", + "binary": qemu_binary, + "features": ["trace_plugin", "injection"], + }, + ], + } + + def tool_session_launch(self, args: dict) -> dict: + if not self.owns_session: + raise _unavailable("session_launch is standalone-mode only; in plugin mode the REPL owns the session") + backend = str(args.get("backend", "native")) + if backend not in ("qemu", "native"): + raise ToolError("backend must be 'qemu' or 'native'", code="invalid_arguments") + kernel = args.get("kernel") + if kernel is not None: + kernel = str(kernel) + if not os.path.isfile(kernel): + raise ToolError(f"kernel not found: {kernel}", code="invalid_arguments") + elif os.path.isfile(_DEMO_ELF): + kernel = _DEMO_ELF + record = args.get("record", True) + if record is False: + record = None + trace = bool(args.get("trace", False)) + if trace and backend != "native": + raise ToolError("trace= is native-only (QEMU uses the TCG plugin)", code="invalid_arguments") + name = str(args.get("name", "mcp")) + try: + import greeneye as ge + + session = ge.launch( + kernel=kernel, + backend=backend, + record=record, + name=name, + trace=trace or None, + ) + except Exception as exc: + raise ToolError(f"launch failed: {exc}", code="launch_failed", backend=backend, kernel=kernel) + if self.session is not None and not self.session_closed: + try: + self.session.close() + except Exception as exc: # replacing a session must not fail the launch + self._log(f"closing previous session failed: {exc!r}") + self.session = session + self.session_closed = False + return {"launched": True, "backend": self._active_backend(), "session": self.tool_session_info({})} + # -- tools: instrumentation ------------------------------------------- def tool_bp_set(self, args: dict) -> dict: @@ -684,6 +803,39 @@ class GreenEyeMCPServer: result["firmware"] = invoke_via_firmware(self.session, service, cmd, a0, a1) return result + def tool_service_list(self, args: dict) -> dict: + try: + from greeneye.inject import CTRL_HANDLERS, INJECT_CMD_BASE, services + except Exception as exc: + raise _unavailable(f"injection support unavailable: {exc}") from exc + loaded = [] + for service in services(self.session): + view = getattr(service, "to_dict", None) + loaded.append(view() if callable(view) else {"name": getattr(service, "name", None)}) + slots: list[dict] = [] + ctrl = self.session.symbols.get("gsp_ctrl") + if ctrl is not None: + for index in range(8): + try: + value = self.session.memory.read_u64(ctrl.addr + CTRL_HANDLERS + index * 8) + except Exception: + slots = [] + break + slots.append( + { + "slot": index, + "cmd": INJECT_CMD_BASE + index, + "entry": value, + "annotation": self._annotate(value) if value else None, + } + ) + return { + "services": loaded, + "count": len(loaded), + "slots": slots, + "mailbox_cmd_base": INJECT_CMD_BASE, + } + # -- tools: native trace ---------------------------------------------- def tool_trace_start(self, args: dict) -> dict: @@ -715,6 +867,262 @@ class GreenEyeMCPServer: blob = tracer.flush(self.session.recorder) return {"tracing": False, "blob": blob, "counts": tracer.counts()} + # -- tools: native simulator ------------------------------------------ + + def _native_machine(self): + if not hasattr(self.session.client, "trace"): + raise _unavailable("this tool requires the native simulator backend (launch/serve with --backend native)") + machine = getattr(self.session.target, "machine", None) + if machine is None: + raise _unavailable("native backend exposes no Machine handle") + return machine + + def tool_sim_cycles(self, args: dict) -> dict: + machine = self._native_machine() + model = getattr(machine, "cycle_model", None) + cycles = int(getattr(machine, "cycles", 0)) + insns = int(getattr(machine, "insns", 0)) + describe = getattr(model, "describe", None) + model_info = describe() if callable(describe) else ({"name": model.name} if model is not None else None) + try: + from sim.cycles import CYCLE_FIDELITY + except Exception: # pragma: no cover - sim always ships cycles.py today + CYCLE_FIDELITY = "cycles are modeled defaults, not measured" + return { + "backend": "native", + "insns": insns, + "cycles": cycles, + "cycles_per_instruction": round(cycles / insns, 6) if insns else None, + "accounting_enabled": model is not None, + "cycle_model": model_info, + "fidelity": CYCLE_FIDELITY if model is not None else "cycle accounting disabled for this session", + } + + def _device_model_state(self, machine) -> dict: + """Defensive probe of an optional ``sim.devices`` device model. + + The S1 device model may land concurrently with this server; when its + module/entry point is absent the tool reports a structured + ``not_available`` sub-result instead of failing the import or call. + """ + import importlib + + try: + module = importlib.import_module("sim.devices") + except Exception as exc: + return { + "error": "not_available", + "message": f"sim.devices device model not present: {exc}", + } + for name in ("state_for", "describe", "snapshot", "state", "device_state"): + entry = getattr(module, name, None) + if not callable(entry): + continue + try: + return {"source": f"sim.devices.{name}", "state": entry(machine)} + except TypeError: + try: + return {"source": f"sim.devices.{name}", "state": entry()} + except Exception as exc: + return {"error": "probe_failed", "message": f"sim.devices.{name}() failed: {exc}"} + except Exception as exc: + return {"error": "probe_failed", "message": f"sim.devices.{name}(machine) failed: {exc}"} + devices = getattr(machine, "devices", None) + if devices is not None: + state = getattr(devices, "state", None) + if callable(state): + try: + return {"source": "machine.devices.state", "state": state()} + except Exception as exc: + return { + "error": "probe_failed", + "message": f"machine.devices.state() failed: {exc}", + } + return { + "error": "not_available", + "message": "machine.devices exposes no state() entry point", + } + return { + "error": "not_available", + "message": "sim.devices is present but exposes no state_for/describe/snapshot entry point", + } + + def tool_sim_device_state(self, args: dict) -> dict: + machine = self._native_machine() + limit = _bounded(_as_int(args.get("limit", 16), "limit"), 0, 1000, "limit") + target = self.session.target + log = getattr(target, "mmio_log", None) + if log is None: + log = getattr(machine, "mmio_log", []) or [] + device_map = getattr(machine, "device_map", None) + return { + "backend": "native", + "pc": self.session.pc, + "pc_symbol": self._annotate(self.session.pc), + "bias": getattr(machine, "bias", 0), + "unknown_mmio": getattr(machine, "unknown_mmio", None), + "mmio_unknown": int(getattr(machine, "mmio_unknown", 0)), + "mmio_log": list(log)[-limit:] if limit else [], + "device_map_entries": len(device_map) if device_map is not None else None, + "device_model": self._device_model_state(machine), + } + + def tool_sim_profile(self, args: dict) -> dict: + machine = self._native_machine() + meta = dict(getattr(self.session.recorder, "meta", {}) or {}) + model = getattr(machine, "cycle_model", None) + kernel = self._session_kernel() + device_map = getattr(machine, "device_map", None) + profile = { + "backend": "native", + "kernel": kernel, + "firmware_sha256": _file_sha256(kernel), + "bias": getattr(machine, "bias", 0), + "unknown_mmio": getattr(machine, "unknown_mmio", None), + "device_map_entries": len(device_map) if device_map is not None else None, + "insns": int(getattr(machine, "insns", 0)), + "cycles": int(getattr(machine, "cycles", 0)), + "cycle_model": model.name if model is not None else None, + "accounting_enabled": model is not None, + "mmio_unknown": int(getattr(machine, "mmio_unknown", 0)), + "recording": getattr(self.session.recorder, "dir", None), + "profile_kind": meta.get("kind"), + "trace": meta.get("trace"), + "session_meta": meta, + } + if meta.get("stop_reason") is not None: + profile["stop"] = { + "reason": meta.get("stop_reason"), + "pc": meta.get("stop_pc"), + "pc_fw": meta.get("stop_pc_fw"), + } + return profile + + def tool_sim_regions(self, args: dict) -> dict: + """Read-only region map of the native session: RAM, UART, loaded + firmware segments, the optional device map and observed unmodelled + MMIO pages. Metadata only - no execution, no timing claims.""" + machine = self._native_machine() + limit = _bounded(_as_int(args.get("limit", 16), "limit"), 0, 1000, "limit") + target = self.session.target + bias = int(getattr(machine, "bias", 0) or 0) + ram_base = getattr(machine, "ram_base", None) + if ram_base is None: + ram_base = getattr(target, "ram_base", None) + ram_size = getattr(machine, "ram_size", None) + if ram_size is None: + memory_mb = getattr(target, "memory_mb", None) + ram_size = int(memory_mb) * 1024 * 1024 if memory_mb else None + + regions: list[dict] = [] + if ram_base is not None and ram_size: + regions.append( + { + "name": "ram", + "kind": "ram", + "base": int(ram_base), + "size": int(ram_size), + "end": int(ram_base) + int(ram_size), + } + ) + try: + from sim.machine import UART_BASE, UART_SIZE + except Exception: # pragma: no cover - constants always ship with sim + UART_BASE, UART_SIZE = 0x10000000, 0x100 + regions.append( + { + "name": "uart", + "kind": "device", + "base": UART_BASE, + "size": UART_SIZE, + "end": UART_BASE + UART_SIZE, + "note": "NS16550A console", + } + ) + image = getattr(self.session, "image", None) + for index, segment in enumerate(getattr(image, "segments", []) or []): + if not getattr(segment, "is_load", False): + continue + vaddr = int(getattr(segment, "p_vaddr", 0)) + regions.append( + { + "name": f"image[{index}]", + "kind": "firmware", + "base": vaddr + bias, + "size": int(getattr(segment, "p_memsz", 0)), + "end": vaddr + bias + int(getattr(segment, "p_memsz", 0)), + "file_size": int(getattr(segment, "p_filesz", 0)), + "vaddr_fw": vaddr, + "flags": { + "read": True, + "write": bool(getattr(segment, "writable", False)), + "exec": bool(getattr(segment, "executable", False)), + }, + "source": getattr(image, "path", None), + } + ) + + device_map = getattr(machine, "device_map", None) + device_total = None + device_entries: list[dict] = [] + entries_fn = getattr(device_map, "entries", None) + if callable(entries_fn): + all_entries = list(entries_fn()) + device_total = len(all_entries) + for entry in all_entries[:limit] if limit else []: + device_entries.append( + { + "address": int(getattr(entry, "address", 0)), + "width": int(getattr(entry, "width", 0)), + "kind": getattr(entry, "kind", None), + "hits": int(getattr(entry, "hits", 0)), + "first_epc_fw": int(getattr(entry, "first_epc_fw", 0)), + "source": getattr(entry, "source", None), + } + ) + + pages: dict[int, dict] = {} + for record in getattr(machine, "mmio_log", []) or []: + addr = int(record.get("addr", 0)) + page = addr & ~0xFFF + info = pages.setdefault( + page, + {"page": page, "touches": 0, "reads": 0, "writes": 0, "addresses": set(), "pcs": set()}, + ) + info["touches"] += 1 + if record.get("rw") == "w": + info["writes"] += 1 + else: + info["reads"] += 1 + info["addresses"].add(addr) + if record.get("pc") is not None: + info["pcs"].add(int(record["pc"])) + observed = [ + { + "page": info["page"], + "touches": info["touches"], + "reads": info["reads"], + "writes": info["writes"], + "addresses": sorted(info["addresses"]), + "pcs": sorted(info["pcs"]), + } + for info in sorted(pages.values(), key=lambda item: (-item["touches"], item["page"]))[:limit] + if limit + ] + + return { + "backend": "native", + "pc": self.session.pc, + "pc_symbol": self._annotate(self.session.pc), + "bias": bias, + "unknown_mmio": getattr(machine, "unknown_mmio", None), + "regions": regions, + "device_map_entries": device_total, + "device_map": device_entries, + "observed_mmio_pages": observed, + "fidelity": "region metadata only; no timing or device-environment claims", + } + # -- tools: analysis --------------------------------------------------- def tool_analyze_current(self, args: dict) -> dict: @@ -1013,6 +1421,30 @@ def _version() -> str: return "unknown" +def _file_sha256(path: str | None) -> str | None: + """Digest of a firmware file, cached by (path, size, mtime).""" + if not path or not os.path.isfile(str(path)): + return None + path = str(path) + try: + stat = os.stat(path) + except OSError: + return None + key = (path, stat.st_size, stat.st_mtime_ns) + if key in _SHA_CACHE: + return _SHA_CACHE[key] + try: + digest = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 20), b""): + digest.update(chunk) + value = digest.hexdigest() + except OSError: + value = None + _SHA_CACHE[key] = value + return value + + # --------------------------------------------------------------------------- # plugin entry point # --------------------------------------------------------------------------- diff --git a/debug/risc-v-gsp-rm-vm/risc-v-gsp-rm-debugger.sh b/debug/risc-v-gsp-rm-vm/risc-v-gsp-rm-debugger.sh index 400281a..ca0f733 100755 --- a/debug/risc-v-gsp-rm-vm/risc-v-gsp-rm-debugger.sh +++ b/debug/risc-v-gsp-rm-vm/risc-v-gsp-rm-debugger.sh @@ -5,4 +5,12 @@ set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export PYTHONPATH="$HERE${PYTHONPATH:+:$PYTHONPATH}" export GREENEYE_DUMPS="${GREENEYE_DUMPS:-$HERE/dumps}" + +# `triage` is the harness entry point for the new-version workflow (docs/40); +# everything else is the greeneye package CLI. +if [[ "${1:-}" == "triage" ]]; then + shift + exec python3 "$HERE/harness/triage.py" "$@" +fi + exec python3 -m greeneye "$@" diff --git a/debug/risc-v-gsp-rm-vm/sim/__init__.py b/debug/risc-v-gsp-rm-vm/sim/__init__.py index ad20893..c083bb7 100644 --- a/debug/risc-v-gsp-rm-vm/sim/__init__.py +++ b/debug/risc-v-gsp-rm-vm/sim/__init__.py @@ -5,8 +5,17 @@ RV64IMAC from a 256 MiB RAM region with an NS16550A UART; the debugger-facing adapter lives in ``greeneye.sim_backend``. A machine can be given a ``greeneye.device_map.DeviceMap`` and ``unknown_mmio="lenient"`` to serve unmodelled MMIO accesses from harvested first-touch data instead of faulting. +:mod:`sim.cycles` adds a modeled, table-driven cycle counter alongside the +instruction count (not a timing claim; see ``docs/32-GSP-CYCLE-MODEL.md``). """ from .decode import decode16, decode32 # noqa: F401 +from .cycles import ( # noqa: F401 + CYCLE_FIDELITY, + CYCLE_MODEL_NAME, + DEFAULT_MODEL, + NOTE, + CycleModel, +) from .machine import ( # noqa: F401 R_BP, R_BUDGET, @@ -32,6 +41,11 @@ __all__ = [ "decode32", "Trace", "make_trace", + "CycleModel", + "DEFAULT_MODEL", + "CYCLE_MODEL_NAME", + "CYCLE_FIDELITY", + "NOTE", "R_BUDGET", "R_BP", "R_WATCH", diff --git a/debug/risc-v-gsp-rm-vm/sim/cycles.py b/debug/risc-v-gsp-rm-vm/sim/cycles.py new file mode 100644 index 0000000..b841c6c --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/cycles.py @@ -0,0 +1,247 @@ +"""Modeled RV64IMAC instruction-class cycle accounting for the native core. + +**Every number in this module is a modeled default - NOT a measurement.** +The tables encode deliberately simple, public assumptions about an in-order +single-issue RISC-V core: one cycle for plain ALU work, a multi-cycle cost for +the M extension, a fixed modeled latency added when a memory access completes, +and a larger one for MMIO. They are *not* derived from the GSP core, not from +silicon, and not from vendor data. + +The simulator's fidelity statement is unchanged (docs/06-ACCURACY.md): +instruction semantics (L0) and memory behaviour (L1) are modelled; L2 +(time/concurrency) is **not**. A cycle count from this module is a +deterministic accounting artifact under an explicit model - never a timing, +latency, throughput or silicon claim. + +The model is deterministic by construction: cost depends only on the decoded +instruction class (``sim.decode`` kind/sub) and whether a completed access was +RAM or MMIO. No host clock, no entropy, no caches, no pipeline hazards. +""" +from __future__ import annotations + +from .decode import ( + A_DIV, + A_DIVU, + A_MUL, + A_MULH, + A_MULHSU, + A_MULHU, + A_REM, + A_REMU, + K_AMO, + K_AUIPC, + K_BRANCH, + K_CSR, + K_EBREAK, + K_ECALL, + K_FENCE, + K_ILLEGAL, + K_JAL, + K_JALR, + K_LOAD, + K_LUI, + K_MRET, + K_OP, + K_OP32, + K_OPI, + K_OPI32, + K_STORE, +) + +#: Model identifier written into session metadata and reports. +CYCLE_MODEL_NAME = "rv64imac-modeled/1" + +#: Fidelity label that must travel with every cycle number from this module. +CYCLE_FIDELITY = "L2-not-modelled; cycles are modeled defaults, not measured" + +#: One-line provenance/label, repeated wherever the model is described. +NOTE = ( + "modeled defaults sourced from public in-order RISC-V pipeline assumptions; " + "not measured, not a silicon timing claim" +) + +# Instruction classes used for reporting and per-class reasoning. +CLASS_ALU = "alu" +CLASS_BRANCH = "branch" +CLASS_JUMP = "jump" +CLASS_MUL = "mul" +CLASS_DIV = "div" +CLASS_LOAD = "load" +CLASS_STORE = "store" +CLASS_AMO = "amo" +CLASS_CSR = "csr" +CLASS_TRAP = "trap" +CLASS_FENCE = "fence" +CLASS_ILLEGAL = "illegal" + +_CLASS_BY_KIND = { + K_LUI: CLASS_ALU, + K_AUIPC: CLASS_ALU, + K_JAL: CLASS_JUMP, + K_JALR: CLASS_JUMP, + K_BRANCH: CLASS_BRANCH, + K_LOAD: CLASS_LOAD, + K_STORE: CLASS_STORE, + K_OPI: CLASS_ALU, + K_OPI32: CLASS_ALU, + K_FENCE: CLASS_FENCE, + K_CSR: CLASS_CSR, + K_ECALL: CLASS_TRAP, + K_EBREAK: CLASS_TRAP, + K_MRET: CLASS_TRAP, + K_AMO: CLASS_AMO, + K_ILLEGAL: CLASS_ILLEGAL, +} + +#: Sentinel in ``base_costs``: the cost lives in ``op_costs`` (M extension). +_SENTINEL = -1 + +_M_EXT_KINDS = (K_OP, K_OP32) + + +class CycleModel: + """Table-driven, deterministic instruction-class cycle costs. + + All keyword values are **modeled defaults** (see the module docstring); + they are parameters so a future calibration can replace them without + touching the execution loop. ``memory_latency``/``mmio_latency`` are the + extra cycles added when a load/store/AMO access completes (RAM vs + modelled-MMIO respectively); they are *not* cache models. + """ + + name = CYCLE_MODEL_NAME + + def __init__( + self, + *, + alu: int = 1, # plain integer ALU / shift / lui / auipc / op-imm + branch: int = 1, # conditional branch (taken penalty deliberately not modelled) + jump: int = 1, # jal / jalr + mul: int = 3, # mul / mulh / mulhsu / mulhu (conservative) + div: int = 20, # div / divu / rem / remu (conservative, data-independent) + load: int = 1, # load base; + memory_latency/mmio_latency on completion + store: int = 1, # store base; + memory_latency/mmio_latency on completion + amo: int = 2, # amoswap/lr/sc base; + one memory latency on completion + csr: int = 2, # CSR read/modify/write + trap: int = 3, # ecall / ebreak / mret + fence: int = 1, # fence / fence.i (no ordering model; nominal) + illegal: int = 1, # decode failure (execution stops immediately) + memory_latency: int = 3, # extra cycles for a completed RAM access + mmio_latency: int = 12, # extra cycles for a completed unmodelled-MMIO access + ): + for label, value in ( + ("alu", alu), + ("branch", branch), + ("jump", jump), + ("mul", mul), + ("div", div), + ("load", load), + ("store", store), + ("amo", amo), + ("csr", csr), + ("trap", trap), + ("fence", fence), + ("illegal", illegal), + ("memory_latency", memory_latency), + ("mmio_latency", mmio_latency), + ): + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise ValueError(f"cycle model: {label} must be a non-negative int, got {value!r}") + if alu < 1 or branch < 1 or jump < 1: + raise ValueError("cycle model: alu/branch/jump must be >= 1 for a clean per-instruction model") + self.alu = alu + self.branch = branch + self.jump = jump + self.mul = mul + self.div = div + self.load = load + self.store = store + self.amo = amo + self.csr = csr + self.trap = trap + self.fence = fence + self.illegal = illegal + self.memory_latency = memory_latency + self.mmio_latency = mmio_latency + + base = [0] * 18 + base[K_LUI] = alu + base[K_AUIPC] = alu + base[K_JAL] = jump + base[K_JALR] = jump + base[K_BRANCH] = branch + base[K_LOAD] = load + base[K_STORE] = store + base[K_OPI] = alu + base[K_OP] = _SENTINEL + base[K_OPI32] = alu + base[K_OP32] = _SENTINEL + base[K_FENCE] = fence + base[K_CSR] = csr + base[K_ECALL] = trap + base[K_EBREAK] = trap + base[K_MRET] = trap + base[K_AMO] = amo + base[K_ILLEGAL] = illegal + + op = [alu] * 18 + for sub in (A_MUL, A_MULH, A_MULHSU, A_MULHU): + op[sub] = mul + for sub in (A_DIV, A_DIVU, A_REM, A_REMU): + op[sub] = div + # Immutable fast-path tables consumed by Machine.run's hot loop. + self.base_costs = tuple(base) + self.op_costs = tuple(op) + + # -- queries ----------------------------------------------------------- + + def instruction_cost(self, kind: int, sub: int = 0) -> int: + """Modeled cycles for one decoded instruction (memory latency excluded).""" + cost = self.base_costs[kind] + if cost == _SENTINEL: + return self.op_costs[sub] + return cost + + def memory_cost(self, is_mmio: bool = False) -> int: + """Modeled extra cycles when a load/store/AMO access completes.""" + return self.mmio_latency if is_mmio else self.memory_latency + + @staticmethod + def classify(kind: int, sub: int = 0) -> str: + """Human-readable class for a decoded instruction (reporting only).""" + if kind in _M_EXT_KINDS: + if sub >= A_DIV: + return CLASS_DIV + if sub >= A_MUL: + return CLASS_MUL + return CLASS_ALU + return _CLASS_BY_KIND.get(kind, "other") + + def describe(self) -> dict: + """The full table, for session metadata and reports.""" + return { + "name": self.name, + "note": NOTE, + "fidelity": CYCLE_FIDELITY, + "classes": { + CLASS_ALU: self.alu, + CLASS_BRANCH: self.branch, + CLASS_JUMP: self.jump, + CLASS_MUL: self.mul, + CLASS_DIV: self.div, + CLASS_LOAD: self.load, + CLASS_STORE: self.store, + CLASS_AMO: self.amo, + CLASS_CSR: self.csr, + CLASS_TRAP: self.trap, + CLASS_FENCE: self.fence, + CLASS_ILLEGAL: self.illegal, + }, + "memory_latency": self.memory_latency, + "mmio_latency": self.mmio_latency, + } + + +#: Shared default model. Machine() uses it unless told otherwise; it is +#: immutable in practice, so one instance is safe for every target. +DEFAULT_MODEL = CycleModel() diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py b/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py new file mode 100644 index 0000000..caf22b3 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py @@ -0,0 +1,163 @@ +"""GSP device model: registry + per-block modules. + +Public surface: + +* :class:`~sim.devices.base.DeviceRegistry` -- address-range lookup, access + log and serialisable state. +* :func:`registry_from_profile` -- build a registry from a versioned + ``sim/profiles/*.json`` document. +* per-block devices: :mod:`sim.devices.pmc`, :mod:`sim.devices.falcon`, + :mod:`sim.devices.priscv`, :mod:`sim.devices.timer`, :mod:`sim.devices.csb`, + :mod:`sim.devices.fwsec`, :mod:`sim.devices.pmu`, plus the + :mod:`sim.devices.dma` translation seam and :mod:`sim.devices.irq` router. + +Evidence and limits for every modelled register are documented in the module +docstrings and in ``docs/36-GSP-DEVICE-MODEL.md``; unmodelled offsets always +stay unhandled so the machine's unknown-MMIO log remains the source of truth +for what is still missing. +""" +from __future__ import annotations + +import json + +from .base import Device, DeviceRegistry, SNAPSHOT_SCHEMA, Window +from .csb import CsbWindow +from .dma import DmaTranslator +from .falcon import FalconBlock +from .fwsec import Wpr2Block +from .irq import IrqRouter +from .pmc import PmcBlock +from .pmu import PmuStub +from .priscv import PriscvBlock +from .timer import PtimerBlock + +PROFILE_SCHEMA = "greeneye-sim-profile/1" + +__all__ = [ + "Device", + "DeviceRegistry", + "Window", + "SNAPSHOT_SCHEMA", + "PROFILE_SCHEMA", + "PmcBlock", + "FalconBlock", + "PriscvBlock", + "PtimerBlock", + "CsbWindow", + "Wpr2Block", + "PmuStub", + "DmaTranslator", + "IrqRouter", + "load_profile", + "registry_from_profile", +] + + +def load_profile(path) -> dict: + """Load and minimally validate a ``greeneye-sim-profile/1`` document.""" + with open(path, "r", encoding="utf-8") as fh: + data = json.load(fh) + if data.get("schema") != PROFILE_SCHEMA: + raise ValueError(f"{path}: expected schema {PROFILE_SCHEMA!r}") + if "devices" not in data or not isinstance(data["devices"], dict): + raise ValueError(f"{path}: missing 'devices' object") + return data + + +def _int(value, default=None): + """Profile numbers may be JSON integers or ``0x``/decimal strings.""" + if value is None: + return default + if isinstance(value, str): + return int(value, 0) + return int(value) + + +def _windows(section: dict, default_size: int = 0x1000): + out = [] + for win in section.get("windows", ()): + out.append((_int(win["base"]), _int(win.get("size", default_size)))) + return out + + +def registry_from_profile(profile: dict, *, machine=None, + max_access_log: int = 100_000) -> DeviceRegistry: + """Build a :class:`DeviceRegistry` from a profile document. + + ``machine`` (optional) supplies RAM to the DMA translation seam; without + it DMA transfers are still modelled structurally but refuse to move data + (recorded, never faked). + """ + if profile.get("schema") != PROFILE_SCHEMA: + raise ValueError(f"expected schema {PROFILE_SCHEMA!r}") + sections = profile.get("devices", {}) + reg = DeviceRegistry(max_access_log=max_access_log) + + dma_section = sections.get("dma", {}) + dma_windows = [ + {"pa": _int(w["pa"]), "sim": _int(w["sim"]), "size": _int(w["size"])} + for w in dma_section.get("windows", ()) + ] + if machine is not None: + dma = DmaTranslator( + ram=machine.ram, + ram_base=machine.ram_base, + ram_size=machine.ram_size, + windows=dma_windows, + ) + else: + dma = DmaTranslator(ram=bytearray(), ram_base=0, ram_size=0, + windows=()) + + falcon = None + priscv = None + + for name, section in sections.items(): + if not isinstance(section, dict): + continue + if name == "dma": + continue + if name == "pmc": + dev = PmcBlock( + chip_id=_int(section.get("chip_id", 0x164)), + major=_int(section.get("major", 0)), + minor=_int(section.get("minor", 0)), + minor_ext=_int(section.get("minor_ext", 0)), + boot_42=(_int(section["boot_42"]) if "boot_42" in section else None), + ) + elif name == "falcon": + shims = section.get("shims", {}) + dev = FalconBlock( + dma=dma, + debuginfo_host_consume=bool(shims.get("debuginfo_host_consume", False)), + debuginfo_stage_mask=_int(section.get("debuginfo_stage_mask", 0x700000)), + debuginfo_stage_shift=_int(section.get("debuginfo_stage_shift", 20)), + ) + falcon = dev + elif name == "priscv": + dev = PriscvBlock() + priscv = dev + elif name == "timer": + if not section.get("enabled", True): + continue + dev = PtimerBlock( + ns_per_insn=float(section.get("ns_per_insn", 1.0)), + clock=(machine.insn_count if machine is not None else None), + ) + elif name == "csb": + dev = CsbWindow(registers=section.get("registers", {})) + elif name == "fwsec": + dev = Wpr2Block( + wpr2_start=(_int(section["wpr2_start"]) if "wpr2_start" in section else None), + wpr2_end=(_int(section["wpr2_end"]) if "wpr2_end" in section else None), + ) + elif name == "pmu": + dev = PmuStub() + else: + raise ValueError(f"unknown device section {name!r}") + for base, size in _windows(section): + reg.add(dev, base, size) + + reg.add_service(IrqRouter(falcon=falcon, priscv=priscv)) + reg.dma = dma + return reg diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/base.py b/debug/risc-v-gsp-rm-vm/sim/devices/base.py new file mode 100644 index 0000000..877109c --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/base.py @@ -0,0 +1,263 @@ +"""Device-model core: address-range registry and device base classes. + +The registry decodes a guest MMIO address to at most one modelled device +window (``base <= addr < base + size``) and asks that device for read/write +semantics. Modeled windows take precedence over the lenient device-map / +unknown fallback in :class:`sim.machine.Machine`; accesses inside a known +window that the device does not model are *not* silently zeroed here -- they +return ``unhandled`` so the machine still logs them as unknowns. + +Every modelled access is recorded in :attr:`DeviceRegistry.accesses` with the +PC, width, direction and value so a session can explain exactly what the +device model served. All state is exposed as JSON-serialisable data +(:meth:`Device.state` / :meth:`DeviceRegistry.snapshot`) so snapshot support +can be layered on later without changing this API. + +Evidence rule (SIMULATOR-WORK-BOUNDARY / docs/35): a register is only given +semantics when it is backed by at least one of + +* a hardware register/field header (offset, width, access, reset/field docs); +* a firmware/source use (the code that reads or writes it); +* an observed access pattern (address, width, direction, PC, value sequence). + +Each device class documents the evidence kind for every register it models. +Anything else stays ``unhandled`` and is reported by the machine's unknown +log; we never invent a read-back value. +""" +from __future__ import annotations + +import json + +#: Schema tag for :meth:`DeviceRegistry.snapshot`. +SNAPSHOT_SCHEMA = "greeneye-sim-devices/1" + + +class Device: + """One modelled register block at one or more address windows. + + Subclasses implement :meth:`read`/:meth:`write` (guest side, side effects + allowed) and :meth:`peek` (debugger side, no side effects). Returning + ``None`` from ``read``/``peek`` or ``False`` from ``write`` means "not + modelled"; the registry then reports the access as unhandled so it is + still logged by the caller. + """ + + name = "device" + + def __init__(self, base: int = 0, size: int = 0): + self.base = base + self.size = size + + # -- guest-visible semantics ------------------------------------------ + + def read(self, offset: int, size: int) -> int | None: + return None + + def write(self, offset: int, size: int, value: int) -> bool: + return False + + # -- debugger-visible semantics (no side effects) ---------------------- + + def peek(self, offset: int, size: int) -> int | None: + return self.read(offset, size) + + # -- serialisable state ------------------------------------------------ + + def state(self) -> dict: + return {} + + def load_state(self, data: dict) -> None: + pass + + def reset(self) -> None: + self.load_state({}) + + # -- helpers ----------------------------------------------------------- + + @staticmethod + def _mask(size: int) -> int: + return (1 << (8 * size)) - 1 + + def __repr__(self) -> str: # pragma: no cover - cosmetic + return f"{type(self).__name__}({self.name} @ {self.base:#x}+{self.size:#x})" + + +class Window: + """One (base, size) -> :class:`Device` mapping.""" + + __slots__ = ("base", "size", "device") + + def __init__(self, base: int, size: int, device: Device): + self.base = base + self.size = size + self.device = device + + def contains(self, addr: int, size: int) -> bool: + return self.base <= addr and addr + size <= self.base + self.size + + def to_dict(self) -> dict: + return { + "base": self.base, + "size": self.size, + "device": self.device.name, + } + + +class DeviceRegistry: + """Ordered set of device windows plus an access log. + + Lookup is last-registered-first so a narrow window may override a wider + one without hiding it: when the last matching window does not implement the + access (``read`` returns ``None`` / ``write`` returns ``False``), the + registry continues to the next matching window (safe fallback). If every + matching window refuses, the access is recorded once as unhandled. + Accesses are matched by exact containment (``addr + size`` inside the + window) so a straddling access is never half-served. + """ + + def __init__(self, *, max_access_log: int = 100_000): + self.windows: list[Window] = [] + self.devices: list[Device] = [] + self.services: list[object] = [] + self.dma = None # optional DmaTranslator aperture seam + self.accesses: list[dict] = [] + self.dropped_accesses = 0 + self.unhandled = 0 + self.max_access_log = max(0, int(max_access_log)) + + def add_service(self, service) -> object: + """Attach a non-window service (e.g. an IRQ router) for state export.""" + self.services.append(service) + return service + + # -- construction ------------------------------------------------------ + + def add(self, device: Device, base: int | None = None, size: int | None = None) -> Device: + """Register ``device`` for one window (defaults: device.base/size).""" + b = device.base if base is None else base + s = device.size if size is None else size + if s <= 0: + raise ValueError(f"device window size must be positive, got {s}") + if device not in self.devices: + self.devices.append(device) + self.windows.append(Window(b, s, device)) + return device + + def device_by_name(self, name: str) -> Device | None: + for dev in self.devices: + if dev.name == name: + return dev + return None + + # -- lookup ------------------------------------------------------------ + + def _candidates(self, addr: int, size: int): + """Matching windows, last-registered-first (fallback order).""" + for win in reversed(self.windows): + if win.contains(addr, size): + yield win + + def _record(self, addr: int, size: int, write: bool, value: int | None, + handled: bool, pc: int) -> None: + entry = { + "addr": addr, + "size": size, + "rw": "w" if write else "r", + "handled": bool(handled), + "pc": pc, + } + if write and value is not None: + entry["value"] = value & ((1 << (8 * size)) - 1) + if handled and not write: + entry["value"] = value + if len(self.accesses) < self.max_access_log: + self.accesses.append(entry) + else: + self.dropped_accesses += 1 + + def read(self, addr: int, size: int, pc: int = 0) -> tuple[bool, int]: + """Guest read: ``(handled, value)``. Unhandled accesses are logged. + + The last matching window is asked first; a window that does not model + the offset defers to the next matching window. + """ + matched = False + for win in self._candidates(addr, size): + matched = True + value = win.device.read(addr - win.base, size) + if value is not None: + self._record(addr, size, False, value, True, pc) + return True, value + self.unhandled += 1 + if matched: + self._record(addr, size, False, None, False, pc) + return False, 0 + + def write(self, addr: int, size: int, value: int, pc: int = 0) -> bool: + """Guest write: True when handled. Unhandled accesses are logged. + + The last matching window is asked first; a window that does not model + the offset defers to the next matching window. + """ + matched = False + for win in self._candidates(addr, size): + matched = True + if win.device.write(addr - win.base, size, value): + self._record(addr, size, True, value, True, pc) + return True + self.unhandled += 1 + if matched: + self._record(addr, size, True, value, False, pc) + return False + + def peek(self, addr: int, size: int) -> bytes | None: + """Debugger read of device space: bytes, or None when unmapped.""" + for win in self._candidates(addr, size): + value = win.device.peek(addr - win.base, size) + if value is not None: + return int(value).to_bytes(size, "little") + return None + + # -- state ------------------------------------------------------------- + + def state(self) -> dict: + return { + "schema": SNAPSHOT_SCHEMA, + "devices": [{"name": d.name, "base": d.base, "size": d.size, + "state": d.state()} for d in self.devices], + "services": [{"name": getattr(s, "name", type(s).__name__), + "state": s.state()} for s in self.services], + "counters": { + "accesses": len(self.accesses), + "dropped_accesses": self.dropped_accesses, + "unhandled": self.unhandled, + }, + } + + def load_state(self, data: dict) -> None: + if data.get("schema") != SNAPSHOT_SCHEMA: + raise ValueError(f"not a {SNAPSHOT_SCHEMA} snapshot") + for entry in data.get("devices", []): + dev = self.device_by_name(entry.get("name", "")) + if dev is not None: + dev.load_state(entry.get("state", {})) + counters = data.get("counters", {}) + self.dropped_accesses = int(counters.get("dropped_accesses", 0)) + self.unhandled = int(counters.get("unhandled", 0)) + + def snapshot_json(self) -> str: + return json.dumps(self.state(), sort_keys=True) + + def reset(self) -> None: + self.accesses.clear() + self.dropped_accesses = 0 + self.unhandled = 0 + for dev in self.devices: + dev.reset() + for service in self.services: + reset = getattr(service, "reset", None) + if callable(reset): + reset() + + def describe(self) -> list[dict]: + return [w.to_dict() for w in self.windows] diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/csb.py b/debug/risc-v-gsp-rm-vm/sim/devices/csb.py new file mode 100644 index 0000000..818a263 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/csb.py @@ -0,0 +1,89 @@ +"""CSB / TRANSCOM message-window seam (B3 landing point). + +Evidence kind: **hardware register/field header + observed access pattern** +(once the firmware reaches the message loop). At the time of writing the +570.144 boot has not been observed polling the GSP message-queue CSB window, +so this module deliberately models **no register semantics by default**: a +window is only served when a profile declares it *and* lists the individual +registers with their widths, reset values and semantics (``rw``, ``w1c``, +``set``, ``clear``). Undeclared offsets stay unhandled and are logged. + +The CSB/TRANSCOM window carries the host<->GSP message queues and doorbells +(head/tail pointers, interrupt set/clear); B3 requires the first such poll to +return the documented read-back, which will be added here together with the +observed access evidence and a test. +""" +from __future__ import annotations + +from .base import Device + +#: Per-register semantics accepted by a profile. +SEMANTICS = ("rw", "ro", "wo", "w1c", "set", "clear") + + +class CsbWindow(Device): + """Profile-declared CSB/mailbox register window (empty by default).""" + + name = "csb" + + def __init__(self, *, base: int = 0, size: int = 0x1000, registers=None): + super().__init__(base=base, size=size) + # registers: {offset: {"width": 4, "value": int, "semantics": "rw"}} + self.registers: dict[int, dict] = {} + for off, spec in (registers or {}).items(): + self.declare(int(off), spec) + self.reset() + + def declare(self, offset: int, spec: dict) -> None: + semantics = spec.get("semantics", "rw") + if semantics not in SEMANTICS: + raise ValueError(f"unknown CSB semantics {semantics!r}") + width = int(spec.get("width", 4)) + if width not in (1, 2, 4, 8): + raise ValueError(f"unsupported CSB register width {width}") + self.registers[offset] = { + "width": width, + "reset": int(spec.get("value", 0)), + "semantics": semantics, + } + + def reset(self) -> None: + self.values = {off: spec["reset"] for off, spec in self.registers.items()} + + def _spec(self, offset: int, size: int) -> dict | None: + spec = self.registers.get(offset) + if spec is None or spec["width"] != size: + return None + return spec + + def read(self, offset: int, size: int) -> int | None: + spec = self._spec(offset, size) + if spec is None or spec["semantics"] == "wo": + return None + return self.values.get(offset, 0) + + def write(self, offset: int, size: int, value: int) -> bool: + spec = self._spec(offset, size) + if spec is None or spec["semantics"] == "ro": + return False + mask = (1 << (8 * size)) - 1 + value &= mask + sem = spec["semantics"] + if sem == "w1c": + self.values[offset] = self.values.get(offset, 0) & ~value & mask + elif sem == "set": + self.values[offset] = (self.values.get(offset, 0) | value) & mask + elif sem == "clear": + self.values[offset] = (self.values.get(offset, 0) & ~value) & mask + else: + self.values[offset] = value + return True + + def state(self) -> dict: + return {"values": {str(k): v for k, v in sorted(self.values.items())}, + "declared": sorted(self.registers)} + + def load_state(self, data: dict) -> None: + values = {int(k): int(v) for k, v in data.get("values", {}).items()} + self.values = {off: values.get(off, spec["reset"]) + for off, spec in self.registers.items()} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/dma.py b/debug/risc-v-gsp-rm-vm/sim/devices/dma.py new file mode 100644 index 0000000..45d03e5 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/dma.py @@ -0,0 +1,117 @@ +"""DMA / address-aperture translation seam. + +Evidence kind: **hardware address-map header + firmware use + observed +access pattern (through the falcon DMA registers).** + +The GSP RISC-V address map (Turing) defines apertures the firmware and its +DMA engine address in GPU physical space: + +* ``IMEM`` ``0x0_0000_0000`` (4 GiB window) -- local instruction memory +* ``DMEM`` ``0x1_0000_0000`` (4 GiB window) -- local data memory +* ``EMEM`` ``0x2_0000_0000`` (4 GiB window) -- external memory window +* ``FBGPA`` ``0x6180_0000_0000_0000`` -- framebuffer aperture +* ``SYSGPA````0x8180_0000_0000_0000`` -- system-memory aperture +* ``PRIV`` ``0x2000_0000_0000_0000`` -- register aperture + +The simulator keeps local memory in one flat RAM region; framebuffer and +system memory only exist as *profile-declared windows* that map a GPU +physical aperture onto a simulator RAM address. A window not declared in the +profile is not modelled: the transfer is refused and recorded (never +silently zero-filled). This class is the single place where that mapping +lives, so a future IOMMU model can replace it without touching devices. +""" +from __future__ import annotations + + +class DmaTranslator: + """Translate GPU physical addresses to simulator RAM through windows.""" + + def __init__(self, *, ram, ram_base: int, ram_size: int, windows=None): + self.ram = ram + self.ram_base = ram_base + self.ram_size = ram_size + self.windows: list[tuple[int, int, int]] = [] # (pa_base, sim_base, size) + self.transfers = 0 + self.refused = 0 + self.events: list[dict] = [] + for win in windows or (): + self.add_window(int(win["pa"]), int(win["sim"]), int(win["size"])) + + def add_window(self, pa_base: int, sim_base: int, size: int) -> None: + if size <= 0: + raise ValueError("DMA window size must be positive") + self.windows.append((pa_base, sim_base, size)) + + def translate(self, pa: int) -> int | None: + """GPU physical address -> simulator address, or None if unmodelled.""" + for pa_base, sim_base, size in reversed(self.windows): + if pa_base <= pa < pa_base + size: + return sim_base + (pa - pa_base) + return None + + def read(self, pa: int, size: int) -> bytes | None: + sim = self.translate(pa) + if sim is None: + self._refuse(pa, size, "r") + return None + return self._read_sim(sim, size) + + def write(self, pa: int, data: bytes) -> bool: + sim = self.translate(pa) + if sim is None: + self._refuse(pa, len(data), "w") + return False + self._write_sim(sim, data) + return True + + def _in_ram(self, addr: int, size: int) -> bool: + off = addr - self.ram_base + return 0 <= off and off + size <= self.ram_size + + def _read_sim(self, sim: int, size: int) -> bytes | None: + if not self._in_ram(sim, size): + self._refuse(sim, size, "r-sim") + return None + off = sim - self.ram_base + return bytes(self.ram[off:off + size]) + + def _write_sim(self, sim: int, data: bytes) -> bool: + if not self._in_ram(sim, len(data)): + self._refuse(sim, len(data), "w-sim") + return False + off = sim - self.ram_base + self.ram[off:off + len(data)] = data + return True + + def copy(self, src_pa: int, dst_pa: int, size: int) -> bool: + """Copy ``size`` bytes between GPU physical addresses. + + A transfer is counted only when both the source read and the + destination write complete; a refused destination write leaves the + transfer in :attr:`refused` and not in :attr:`transfers`. + """ + data = self.read(src_pa, size) + if data is None: + return False + ok = self.write(dst_pa, data) + if ok: + self.transfers += 1 + return ok + + def _refuse(self, pa: int, size: int, kind: str) -> None: + self.refused += 1 + if len(self.events) < 4096: + self.events.append({"pa": pa, "size": size, "kind": kind}) + + def state(self) -> dict: + return { + "windows": [{"pa": p, "sim": s, "size": n} for p, s, n in self.windows], + "transfers": self.transfers, + "refused": self.refused, + "events": list(self.events), + } + + def reset(self) -> None: + self.transfers = 0 + self.refused = 0 + self.events.clear() diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/falcon.py b/debug/risc-v-gsp-rm-vm/sim/devices/falcon.py new file mode 100644 index 0000000..8841906 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/falcon.py @@ -0,0 +1,228 @@ +"""GSP falcon register block (boot polling + handshake + DMA). + +Evidence kind: **hardware register/field header + firmware use + observed +access pattern.** + +The block is reached through the PRIV aperture (``PRIV + 0x110000``) and, in +the libos kernel's private mapping, at virtual base ``0x3_0000_0000`` (the +firmware accesses both forms during boot). Modelled registers, all with +header offsets relative to the block: + +====================================== ====== ===== ========================= +register offset access semantics +====================================== ====== ===== ========================= +``FALCON_IRQSSET`` 0x000 W ``irq_status |= value`` +``FALCON_IRQSCLR`` 0x004 W ``irq_status &= ~value`` +``FALCON_IRQSTAT`` 0x008 R ``irq_status`` +``FALCON_MAILBOX0`` 0x040 RW storage +``FALCON_MAILBOX1`` 0x044 RW storage +``FALCON_OS`` 0x080 RW storage +``FALCON_DEBUGINFO`` 0x094 RW storage (+ host shim) +``FALCON_CPUCTL`` 0x100 RW storage +``FALCON_BOOTVEC`` 0x104 RW storage +``FALCON_DMACTL`` 0x10c RW storage +``FALCON_DMATRFBASE`` 0x110 RW storage +``FALCON_DMATRFMOFFS`` 0x114 RW storage +``FALCON_DMATRFCMD`` 0x118 RW transfer on write +``FALCON_DMATRFFBOFFS`` 0x11c RW storage +``FALCON_DMATRFBASE1`` 0x128 RW storage +``FBIF_TRANSCFG(0..7)`` 0x600 RW storage +``FBIF_CTL`` 0x624 RW storage +====================================== ====== ===== ========================= + +Observed firmware use (570.144): the boot code writes MAILBOX0 (progress +code), MAILBOX1, DEBUGINFO (boot-stage marker ``0x11DEAD``) and IRQSSET +``HALT`` (bit 4), then polls DEBUGINFO bits ``22:20`` until they clear; it +reads back DMACTL/DMATRFCMD/DMATRFBASE/DMATRFBASE1/DMATRFMOFFS/ +DMATRFFBOFFS and IRQSTAT. The DMA transfer semantics (size encoding, IMEM/ +WRITE direction, ``((base1<<32|base)<<8)+fboffs`` addressing) follow the +register header fields and are exercised only through the translation seam +(:mod:`sim.devices.dma`); an unmapped transfer is refused and recorded, never +zero-filled. + +The DEBUGINFO "host consume" behaviour is a **boot shim**, not a device +claim: the firmware's own write-then-poll loop has no self-clearing path, and +the documented debug channel is write-then-wait-for-zero (consumer is the +host side). It is enabled per profile and recorded as an assumption. +""" +from __future__ import annotations + +from .base import Device + +# Register offsets (header values, relative to the falcon block base). +IRQSSET = 0x000 +IRQSCLR = 0x004 +IRQSTAT = 0x008 +MAILBOX0 = 0x040 +MAILBOX1 = 0x044 +OS = 0x080 +DEBUGINFO = 0x094 +CPUCTL = 0x100 +BOOTVEC = 0x104 +DMACTL = 0x10C +DMATRFBASE = 0x110 +DMATRFMOFFS = 0x114 +DMATRFCMD = 0x118 +DMATRFFBOFFS = 0x11C +DMATRFBASE1 = 0x128 +FBIF_TRANSCFG0 = 0x600 +FBIF_TRANSCFG_STRIDE = 4 +FBIF_TRANSCFG_COUNT = 8 +FBIF_CTL = 0x624 + +# DMATRFCMD fields (header). +CMD_FULL = 1 << 0 +CMD_IDLE = 1 << 1 +CMD_IMEM = 1 << 4 +CMD_WRITE = 1 << 5 +CMD_SIZE_SHIFT = 8 +CMD_SIZE_MASK = 0x7 +CMD_CTXDMA_SHIFT = 12 +CMD_CTXDMA_MASK = 0x7 +CMD_ERROR = 1 << 20 + +#: IRQSSET/IRQSCLR bit 4 is HALT in the header's field list; the firmware's +#: boot handshake sets it after writing MAILBOX0/DEBUGINFO. +IRQ_HALT = 1 << 4 + +# IMEM/DMEM apertures of the GSP RISC-V address map (header constants). +AMAP_IMEM_START = 0x0000000000000000 +AMAP_DMEM_START = 0x0000000100000000 + +_RW_REGISTERS = { + MAILBOX0, MAILBOX1, OS, DEBUGINFO, CPUCTL, BOOTVEC, DMACTL, DMATRFBASE, + DMATRFMOFFS, DMATRFCMD, DMATRFFBOFFS, DMATRFBASE1, FBIF_CTL, +} + + +class FalconBlock(Device): + """Falcon/GSP register block with boot-handshake and DMA semantics.""" + + name = "falcon" + + def __init__(self, *, base: int = 0, size: int = 0x1000, dma=None, + debuginfo_host_consume: bool = False, + debuginfo_stage_mask: int = 0x700000, + debuginfo_stage_shift: int = 20): + super().__init__(base=base, size=size) + self.dma = dma + self.debuginfo_host_consume = bool(debuginfo_host_consume) + self.debuginfo_stage_mask = debuginfo_stage_mask + self.debuginfo_stage_shift = debuginfo_stage_shift + self.reset() + + def reset(self) -> None: + self.irq_status = 0 + self.regs: dict[int, int] = {off: 0 for off in _RW_REGISTERS} + self.regs[DMATRFCMD] = CMD_IDLE + self.transcfg = [0] * FBIF_TRANSCFG_COUNT + self.dma_transfers = 0 + self.dma_refused = 0 + self.debuginfo_consumed = 0 + + # -- access ------------------------------------------------------------ + + def read(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == IRQSTAT: + return self.irq_status + if offset == DEBUGINFO: + value = self.regs.get(DEBUGINFO, 0) + if self.debuginfo_host_consume: + stage = (value & self.debuginfo_stage_mask) >> self.debuginfo_stage_shift + if stage: + # Boot shim: the host consumes the stage marker (documented + # write-then-wait-for-zero debug channel); the rest of the + # register is preserved. + self.regs[DEBUGINFO] = value & ~self.debuginfo_stage_mask + self.debuginfo_consumed += 1 + return value + if offset in self.regs: + return self.regs[offset] + if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: + return self.transcfg[(offset - FBIF_TRANSCFG0) // 4] + return None + + def write(self, offset: int, size: int, value: int) -> bool: + if size != 4: + return False + value &= 0xFFFFFFFF + if offset == IRQSSET: + self.irq_status = (self.irq_status | value) & 0xFFFFFFFF + return True + if offset == IRQSCLR: + self.irq_status = (self.irq_status & ~value) & 0xFFFFFFFF + return True + if offset == IRQSTAT: + return False # read-only + if offset == DMATRFCMD: + self.regs[DMATRFCMD] = value + self._dma_transfer(value) + return True + if offset in self.regs: + self.regs[offset] = value + return True + if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: + self.transcfg[(offset - FBIF_TRANSCFG0) // 4] = value + return True + return False + + def peek(self, offset: int, size: int) -> int | None: + # No shim side effects for debugger reads. + if size != 4: + return None + if offset == IRQSTAT: + return self.irq_status + if offset in self.regs: + return self.regs[offset] + if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: + return self.transcfg[(offset - FBIF_TRANSCFG0) // 4] + return None + + # -- DMA --------------------------------------------------------------- + + def _dma_transfer(self, cmd: int) -> None: + """Execute one falcon DMA transfer (header field semantics).""" + if self.dma is None: + self.dma_refused += 1 + return + size = 1 << (((cmd >> CMD_SIZE_SHIFT) & CMD_SIZE_MASK) + 2) + tcm = AMAP_IMEM_START if (cmd & CMD_IMEM) else AMAP_DMEM_START + base = ((self.regs.get(DMATRFBASE1, 0) & 0x1FF) << 32) | self.regs.get(DMATRFBASE, 0) + fb = (base << 8) + self.regs.get(DMATRFFBOFFS, 0) + tcm_off = self.regs.get(DMATRFMOFFS, 0) & 0xFFFF + if cmd & CMD_WRITE: + ok = self.dma.copy(tcm + tcm_off, fb, size) + else: + ok = self.dma.copy(fb, tcm + tcm_off, size) + if ok: + self.dma_transfers += 1 + self.regs[DMATRFCMD] = (cmd | CMD_IDLE) & ~CMD_ERROR + else: + self.dma_refused += 1 + self.regs[DMATRFCMD] = (cmd & ~CMD_IDLE) | CMD_ERROR + + # -- state ------------------------------------------------------------- + + def state(self) -> dict: + return { + "irq_status": self.irq_status, + "regs": {str(k): v for k, v in sorted(self.regs.items())}, + "transcfg": list(self.transcfg), + "dma_transfers": self.dma_transfers, + "dma_refused": self.dma_refused, + "debuginfo_consumed": self.debuginfo_consumed, + "debuginfo_host_consume": self.debuginfo_host_consume, + } + + def load_state(self, data: dict) -> None: + self.irq_status = int(data.get("irq_status", 0)) + regs = {int(k): int(v) for k, v in data.get("regs", {}).items()} + self.regs = {off: regs.get(off, 0) for off in _RW_REGISTERS} + self.transcfg = [int(v) for v in data.get("transcfg", [0] * FBIF_TRANSCFG_COUNT)] + self.dma_transfers = int(data.get("dma_transfers", 0)) + self.dma_refused = int(data.get("dma_refused", 0)) + self.debuginfo_consumed = int(data.get("debuginfo_consumed", 0)) + self.debuginfo_host_consume = bool( + data.get("debuginfo_host_consume", self.debuginfo_host_consume)) diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py b/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py new file mode 100644 index 0000000..8dcf633 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py @@ -0,0 +1,80 @@ +"""WPR2 / FWSEC state registers. + +Evidence kind: **hardware register/field header + firmware use** (the boot +flow locates the firmware write-protected region from these registers on +pre-Hopper parts; the host/booter writes them, the firmware reads them). + +Modelled registers (offsets in the FB/PFB register aperture): + +* ``NV_PFB_PRI_MMU_WPR2_ADDR_LO`` ``0x1FA824``: WPR2 start, bits ``31:4`` + shifted by ``NV_PFB_PRI_MMU_WPR2_ADDR_LO_ALIGNMENT = 12`` (4 KiB). +* ``NV_PFB_PRI_MMU_WPR2_ADDR_HI`` ``0x1FA828``: points at the start of the + last 128 KiB block of the WPR, same alignment; the end is + ``(value << 12) + 0x20000``. + +A profile that wants the firmware to see a WPR declares ``wpr2_start`` / +``wpr2_end``; without a declaration the window is unhandled and logged. +FWSEC-specific state is not modelled (no proven register use yet). +""" +from __future__ import annotations + +from .base import Device + +WPR2_ADDR_LO = 0x1FA824 +WPR2_ADDR_HI = 0x1FA828 +WPR2_ALIGNMENT = 12 +WPR2_BLOCK = 0x20000 + + +class Wpr2Block(Device): + """WPR2 address pair as seen by the firmware (host/booter-written).""" + + name = "fwsec" + + def __init__(self, *, base: int = 0, size: int = 0x1000, + wpr2_start: int | None = None, wpr2_end: int | None = None): + super().__init__(base=base, size=size) + self.reset() + if wpr2_start is not None and wpr2_end is not None: + self.set_range(wpr2_start, wpr2_end) + + def set_range(self, start: int, end: int) -> None: + if end <= start: + raise ValueError("WPR2 end must be greater than start") + self.regs[WPR2_ADDR_LO] = (start >> WPR2_ALIGNMENT) & 0xFFFFFFFF + self.regs[WPR2_ADDR_HI] = ((end - WPR2_BLOCK) >> WPR2_ALIGNMENT) & 0xFFFFFFFF + + @property + def start(self) -> int | None: + """WPR2 start derived from ADDR_LO (``None`` when unset).""" + lo = self.regs.get(WPR2_ADDR_LO) + return None if lo is None else lo << WPR2_ALIGNMENT + + @property + def end(self) -> int | None: + """WPR2 end derived from ADDR_HI (``None`` when unset).""" + hi = self.regs.get(WPR2_ADDR_HI) + return None if hi is None else (hi << WPR2_ALIGNMENT) + WPR2_BLOCK + + def reset(self) -> None: + self.regs: dict[int, int] = {} + + def read(self, offset: int, size: int) -> int | None: + if size == 4 and offset in self.regs: + return self.regs[offset] + return None + + def write(self, offset: int, size: int, value: int) -> bool: + if size == 4 and offset in (WPR2_ADDR_LO, WPR2_ADDR_HI): + self.regs[offset] = value & 0xFFFFFFFF + return True + return False + + def state(self) -> dict: + return {"regs": {str(k): v for k, v in sorted(self.regs.items())}, + "start": self.start, "end": self.end} + + def load_state(self, data: dict) -> None: + # start/end are derived from regs; serialised copies are ignored so a + # restored block can never report a stale range. + self.regs = {int(k): int(v) for k, v in data.get("regs", {}).items()} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/irq.py b/debug/risc-v-gsp-rm-vm/sim/devices/irq.py new file mode 100644 index 0000000..7ce63fb --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/irq.py @@ -0,0 +1,58 @@ +"""IRQ routing seam. + +Evidence kind: **hardware register/field header + observed access pattern.** + +The GSP boot handshake sets the falcon ``IRQSSET`` HALT bit (bit 4) after +writing MAILBOX0/DEBUGINFO, and the boot code reads ``FALCON_IRQSTAT``; the +libos kernel owns the RISC-V-local ``RISCV_IRQMSET``/``IRQMCLR``/``IRQMASK`` +group. This module aggregates those two sources behind one object so a +future core-interrupt model can consume a single ``pending()`` surface. + +It does **not** deliver interrupts to the machine's hart: mstatus/mie/mip/ +mtvec trap entry is not modelled (documented unknown). It only makes the +routing state explicit and serialisable. +""" +from __future__ import annotations + +# Falcon IRQSSET/IRQSCLR bit positions used by the boot handshake (header +# field list; only the ones with proven firmware use are named). +FALCON_IRQ_GPTMR = 1 << 0 +FALCON_IRQ_WDTMR = 1 << 1 +FALCON_IRQ_MTHD = 1 << 2 +FALCON_IRQ_CTXSW = 1 << 3 +FALCON_IRQ_HALT = 1 << 4 +FALCON_IRQ_EXTERR = 1 << 5 +FALCON_IRQ_SWGEN0 = 1 << 6 +FALCON_IRQ_SWGEN1 = 1 << 7 +FALCON_IRQ_DMA = 1 << 16 +FALCON_IRQ_SHA = 1 << 17 + + +class IrqRouter: + """Aggregate falcon + RISC-V-local interrupt state.""" + + name = "irq" + + def __init__(self, *, falcon=None, priscv=None): + self.falcon = falcon + self.priscv = priscv + + def falcon_status(self) -> int: + return self.falcon.irq_status if self.falcon is not None else 0 + + def riscv_mask(self) -> int: + return self.priscv.irq_mask if self.priscv is not None else 0 + + def pending(self) -> dict: + """Raw pending state; delivery is deliberately not modelled.""" + return { + "falcon_irq_status": self.falcon_status(), + "falcon_halt": bool(self.falcon_status() & FALCON_IRQ_HALT), + "riscv_irq_mask": self.riscv_mask(), + } + + def state(self) -> dict: + return self.pending() + + def reset(self) -> None: + pass diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/pmc.py b/debug/risc-v-gsp-rm-vm/sim/devices/pmc.py new file mode 100644 index 0000000..1ccf1cd --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/pmc.py @@ -0,0 +1,90 @@ +"""PMC / master register block (first-touch device). + +Evidence kind: **hardware register/field header + firmware use + observed +access pattern.** + +* ``NV_PMC_BOOT_42`` lives at offset ``0xA00`` of the master (PMC) block and is + read by the GSP boot code through the PRIV window at + ``0x2000_0000_0000_0A00`` (observed 4-byte read, first MMIO of the real + 570.144 image; the QEMU baseline faults there). +* The firmware extracts bits ``28:20`` (``NV_PMC_BOOT_42_CHIP_ID``) and + compares against the TU10x IDs ``0x162..0x168``; only a match continues the + boot (observed branch chain at firmware vaddr ``0x4009020``). +* ``NV_PMC_BOOT_42`` field layout: ARCHITECTURE ``28:24`` (TU100 = ``0x16``), + IMPLEMENTATION ``23:20``, MAJOR_REVISION ``19:16``, MINOR_REVISION + ``15:12``, MINOR_EXTENDED_REVISION ``11:8``, CHIP_ID ``28:20``. + +Only ``0xA00`` is modelled; every other offset in the window stays unhandled +and is logged by the machine. +""" +from __future__ import annotations + +from .base import Device + +BOOT_42_OFFSET = 0xA00 +CHIP_ID_SHIFT = 20 +CHIP_ID_MASK = 0x1FF +ARCHITECTURE_MASK = 0x1F +IMPLEMENTATION_MASK = 0xF +MAJOR_MASK = 0xF +MINOR_MASK = 0xF +MINOR_EXT_MASK = 0xF + +#: Chip IDs seen in the firmware's comparison chain (header field values). +#: TU105 (0x165) is kept because it appears in the observed chain, but its +#: product mapping is unverified. The former ``TU107 = 0x167`` entry was a +#: transcription error (0x167 is TU117) and has been removed. +TU102 = 0x162 +TU104 = 0x164 +TU105 = 0x165 # unverified product mapping (observed in the chain only) +TU106 = 0x166 +TU116 = 0x168 +TU117 = 0x167 + + +def boot_42_value(chip_id: int, *, major: int = 0, minor: int = 0, + minor_ext: int = 0) -> int: + """Assemble an ``NV_PMC_BOOT_42`` value from its documented fields. + + ARCHITECTURE and IMPLEMENTATION are derived from ``chip_id`` (they are the + upper/lower nibbles of the same bits ``28:20``), matching the header's + ``CHIP_ID_TU104 = 0x164`` / ``ARCHITECTURE_TU100 = 0x16`` pair. + """ + chip_id &= CHIP_ID_MASK + arch = (chip_id >> 4) & ARCHITECTURE_MASK + impl = chip_id & IMPLEMENTATION_MASK + value = (chip_id << CHIP_ID_SHIFT) + value |= (arch << 24) | (impl << 20) + value |= (major & MAJOR_MASK) << 16 + value |= (minor & MINOR_MASK) << 12 + value |= (minor_ext & MINOR_EXT_MASK) << 8 + return value + + +class PmcBlock(Device): + """Master/PMC block: ``NV_PMC_BOOT_42`` chip identification.""" + + name = "pmc" + + def __init__(self, *, chip_id: int = TU104, major: int = 0, minor: int = 0, + minor_ext: int = 0, boot_42: int | None = None, + base: int = 0, size: int = 0x1000): + super().__init__(base=base, size=size) + self.chip_id = chip_id + self.boot_42 = boot_42 if boot_42 is not None else boot_42_value( + chip_id, major=major, minor=minor, minor_ext=minor_ext) + + def read(self, offset: int, size: int) -> int | None: + if offset == BOOT_42_OFFSET and size == 4: + return self.boot_42 + return None + + def peek(self, offset: int, size: int) -> int | None: + return self.read(offset, size) + + def write(self, offset: int, size: int, value: int) -> bool: + # NV_PMC_BOOT_42 is read-only (R--4R). + return False + + def state(self) -> dict: + return {"chip_id": self.chip_id, "boot_42": self.boot_42} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/pmu.py b/debug/risc-v-gsp-rm-vm/sim/devices/pmu.py new file mode 100644 index 0000000..b241c7e --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/pmu.py @@ -0,0 +1,46 @@ +"""PMU stub. + +Evidence kind: **none yet** (no PMU register has been observed in the +570.144 boot runs, and no PMU register semantics are needed to reach the +rungs achieved so far). The class exists so a profile can attach a PMU +window without inventing behaviour: every access stays unhandled and is +reported by the machine's unknown log, and the stub records the accesses it +saw for later identification. + +When PMU accesses are observed, each register is added here with its header +field evidence and a test, exactly like the falcon block. +""" +from __future__ import annotations + +from .base import Device + + +class PmuStub(Device): + """A PMU window with no proven semantics: observe, never fake.""" + + name = "pmu" + + def __init__(self, *, base: int = 0, size: int = 0x1000): + super().__init__(base=base, size=size) + self.reset() + + def reset(self) -> None: + self.seen: dict[str, int] = {} + + def read(self, offset: int, size: int) -> int | None: + self._note(offset, size, "r") + return None + + def write(self, offset: int, size: int, value: int) -> bool: + self._note(offset, size, "w") + return False + + def peek(self, offset: int, size: int) -> int | None: + return None + + def _note(self, offset: int, size: int, rw: str) -> None: + key = f"{rw}:{offset:#x}:{size}" + self.seen[key] = self.seen.get(key, 0) + 1 + + def state(self) -> dict: + return {"seen": dict(sorted(self.seen.items()))} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/priscv.py b/debug/risc-v-gsp-rm-vm/sim/devices/priscv.py new file mode 100644 index 0000000..62a8fea --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/priscv.py @@ -0,0 +1,113 @@ +"""RISC-V local (PRISCV) register block. + +Evidence kind: **hardware register/field header + observed access pattern.** + +Reached through the PRIV aperture at ``PRIV + 0x111000`` and through the libos +kernel's private mapping at ``0x3_0000_1000`` (the firmware reads the trace +and error-status registers there during boot). Modelled registers: + +================================ ====== ===== ============================== +register offset access semantics +================================ ====== ===== ============================== +``RISCV_IRQMSET`` 0x2ac W ``irq_mask |= value`` +``RISCV_IRQMCLR`` 0x2b0 W ``irq_mask &= ~value`` +``RISCV_IRQMASK`` 0x2b4 R ``irq_mask`` +``RISCV_IRQDEST`` 0x2b8 RW storage +``RISCV_IRQTYPE`` 0x2bc RW storage +``RISCV_TRACECTL`` 0x344 RW storage +``RISCV_TRACE_RDIDX`` 0x348 RW storage +``RISCV_TRACE_WTIDX`` 0x34c RW storage +``RISCV_PRIV_ERR_STAT`` 0x360 RW storage +``RISCV_HUB_ERR_STAT`` 0x36c RW storage +================================ ====== ===== ============================== + +The IRQ mask semantics (set/clear/read) follow the header access types and the +observed boot reads; core interrupt *delivery* (mtvec/mip) is not part of this +device and remains unmodelled (documented unknown). +""" +from __future__ import annotations + +from .base import Device + +IRQMSET = 0x2AC +IRQMCLR = 0x2B0 +IRQMASK = 0x2B4 +IRQDEST = 0x2B8 +IRQTYPE = 0x2BC +TRACECTL = 0x344 +TRACE_RDIDX = 0x348 +TRACE_WTIDX = 0x34C +PRIV_ERR_STAT = 0x360 +PRIV_ERR_INFO = 0x364 +PRIV_ERR_ADDR = 0x368 +HUB_ERR_STAT = 0x36C + +_RW = {IRQDEST, IRQTYPE, TRACECTL, TRACE_RDIDX, TRACE_WTIDX, + PRIV_ERR_STAT, HUB_ERR_STAT} +#: Read-only error capture registers (header R-I4R); the boot panic path +#: reads them after a trap (observed reads at PC 0x40106BC). +_RO = {PRIV_ERR_INFO, PRIV_ERR_ADDR} + + +class PriscvBlock(Device): + """RISC-V-local control/trace/error registers.""" + + name = "priscv" + + def __init__(self, *, base: int = 0, size: int = 0x1000): + super().__init__(base=base, size=size) + self.reset() + + def reset(self) -> None: + self.irq_mask = 0 + self.regs: dict[int, int] = {off: 0 for off in _RW} + self.error: dict[int, int] = {off: 0 for off in _RO} + + def read(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == IRQMASK: + return self.irq_mask + if offset in self.regs: + return self.regs[offset] + if offset in self.error: + return self.error[offset] + return None + + def write(self, offset: int, size: int, value: int) -> bool: + if size != 4: + return False + value &= 0xFFFFFFFF + if offset == IRQMSET: + self.irq_mask = (self.irq_mask | value) & 0xFFFFFFFF + return True + if offset == IRQMCLR: + self.irq_mask = (self.irq_mask & ~value) & 0xFFFFFFFF + return True + if offset == IRQMASK: + return False # read-only + if offset in self.regs: + self.regs[offset] = value + return True + return False + + def peek(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == IRQMASK: + return self.irq_mask + if offset in self.error: + return self.error[offset] + return self.regs.get(offset) + + def state(self) -> dict: + return {"irq_mask": self.irq_mask, + "regs": {str(k): v for k, v in sorted(self.regs.items())}, + "error": {str(k): v for k, v in sorted(self.error.items())}} + + def load_state(self, data: dict) -> None: + self.irq_mask = int(data.get("irq_mask", 0)) + regs = {int(k): int(v) for k, v in data.get("regs", {}).items()} + self.regs = {off: regs.get(off, 0) for off in _RW} + err = {int(k): int(v) for k, v in data.get("error", {}).items()} + self.error = {off: err.get(off, 0) for off in _RO} diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/timer.py b/debug/risc-v-gsp-rm-vm/sim/devices/timer.py new file mode 100644 index 0000000..c57ee06 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/timer.py @@ -0,0 +1,86 @@ +"""PTIMER block (modeled virtual time; not a silicon timing claim). + +Evidence kind: **hardware register/field header** (offsets and field layout) +plus a *modeled* time source. The firmware has not yet been observed reading +this block in the 570.144 boot runs (the observed time-like reads were on +falcon MAILBOX1); the device is therefore opt-in: it is registered only when +a profile declares a ``timer`` section, and a section with +``"enabled": false`` is skipped. Its values are explicitly labelled +*modeled*. + +Header facts used: + +* ``NV_PTIMER`` block base ``0x9000`` in the register aperture. +* ``NV_PTIMER_TIME_0`` at aperture offset ``0x9400``: nanoseconds bits + ``31:5``. +* ``NV_PTIMER_TIME_1`` at aperture offset ``0x9410``: nanoseconds bits + ``28:0``. +* Reads of TIME_0 latch TIME_1 (paired read), as on the hardware register + pair; the simulator derives nanoseconds from the machine's instruction + count times the profile's ``ns_per_insn`` (default 1.0, i.e. 1 ns/insn -- + a placeholder, not calibrated). + +Register offsets below are **relative to the block window base** the profile +declares (``PRIV + 0x9000``), i.e. aperture offset minus ``PTIMER_BASE``: +``0x9400 - 0x9000 = 0x400``. Using the aperture-absolute numbers here would +place the registers outside the declared window and serve nothing. +""" +from __future__ import annotations + +from .base import Device + +PTIMER_BASE = 0x9000 +#: Block-relative offsets of NV_PTIMER_TIME_0/1 (aperture 0x9400/0x9410). +TIME_0 = 0x400 +TIME_1 = 0x410 + + +class PtimerBlock(Device): + """Two-register nanosecond timer with a modeled tick source.""" + + name = "timer" + + def __init__(self, *, base: int = 0, size: int = 0x1000, + ns_per_insn: float = 1.0, clock=None): + super().__init__(base=base, size=size) + self.ns_per_insn = float(ns_per_insn) + self.clock = clock # callable returning virtual instruction count + self._latch = 0 + self.reads = 0 + + def _now_ns(self) -> int: + insns = self.clock() if self.clock is not None else 0 + return int(insns * self.ns_per_insn) & 0xFFFFFFFFFFFFFFFF + + def read(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == TIME_0: + self._latch = self._now_ns() + self.reads += 1 + return (self._latch >> 5) & 0xFFFFFFFF # NSEC field 31:5 + if offset == TIME_1: + self.reads += 1 + return self._latch & 0x1FFFFFFF # NSEC field 28:0 + return None + + def peek(self, offset: int, size: int) -> int | None: + if size != 4: + return None + if offset == TIME_0: + return (self._now_ns() >> 5) & 0xFFFFFFFF + if offset == TIME_1: + return self._now_ns() & 0x1FFFFFFF + return None + + def write(self, offset: int, size: int, value: int) -> bool: + return False + + def state(self) -> dict: + return {"ns_per_insn": self.ns_per_insn, "latch": self._latch, + "reads": self.reads} + + def load_state(self, data: dict) -> None: + self.ns_per_insn = float(data.get("ns_per_insn", self.ns_per_insn)) + self._latch = int(data.get("latch", 0)) + self.reads = int(data.get("reads", 0)) diff --git a/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py b/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py new file mode 100644 index 0000000..c586ab3 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py @@ -0,0 +1,548 @@ +"""Real-firmware GSP boot runner (device model + versioned profile + shim). + +Loads ``gsp-570.144.fwimage.bin`` at its **link addresses** (RAM base 0), builds +the :mod:`sim.devices` registry from a ``greeneye-sim-profile/1`` document, runs +bounded (<= 2M instructions per attempt), and records a normal +``greeneye-session/1`` directory carrying the device state, unknown-MMIO +summary and the boot-shim assumptions. + +Boot ladder rungs (docs/35-GREENEYE-SIMULATOR-DESIGN.md §3.3) reported by +:class:`GspBootResult`: + +* ``B0`` -- flat image loads, first MMIO observed at ``0x2000_0000_0000_0A00``. +* ``B1`` -- first fault past the QEMU point, explained by the device model + (the QEMU fault is the PMC ``BOOT_42`` chip-ID read; modelled by + :class:`sim.devices.pmc.PmcBlock`). +* ``B2`` -- boot-stage progress: vector/MPU/TCM setup and bootargs consumption + exercised with the profile. +* ``B3`` -- first CSB/TRANSCOM message-window poll served with read-back. +* ``B4`` -- message loop reached (waits on a host message). + +The boot shim supplies reset-time state that the real SEC2 booter/host would +provide (bootargs and the register arguments); every shim value is an +explicit, documented assumption in the profile and the session metadata. The +device model itself never fakes a register read. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import sys +from dataclasses import dataclass, field + +from greeneye.device_map import firmware_sha256 +from greeneye.elf import load_elf +from greeneye.errors import GreenEyeError +from sim.devices import load_profile, registry_from_profile +from sim.machine import ( + R_BP, + R_BUDGET, + R_EBREAK, + R_ECALL, + R_FAULT, + R_STEP, + R_WATCH, +) + +TOOL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DEFAULT_IMAGE = os.path.normpath( + os.path.join(TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", + "gsp-570.144.fwimage.bin") +) +DEFAULT_PROFILE = os.path.join(TOOL_ROOT, "sim", "profiles", "tu104-570.144.json") + +#: Hard cap for one attempt (task rule: <= 2M instructions per attempt). +MAX_ATTEMPT_INSTRUCTIONS = 2_000_000 +#: Default bounded budget for a boot attempt. +DEFAULT_INSTRUCTIONS = 2_000_000 + +STOP_REASONS = { + R_BUDGET: "budget", + R_BP: "breakpoint", + R_WATCH: "watch", + R_EBREAK: "ebreak", + R_FAULT: "fault", + R_STEP: "step", + R_ECALL: "ecall", +} + +# GSP RISC-V address-map aperture starts (header constants). +SYSGPA_START = 0x8180000000000000 +FBGPA_START = 0x6180000000000000 +PRIV_START = 0x2000000000000000 + +# GspFwWprMeta field constants (header). +GSP_FW_WPR_META_MAGIC = 0xdc3aae21371a60b3 +GSP_FW_WPR_META_REVISION = 1 +GSP_FW_WPR_META_VERIFIED = 0xa0a0a0a0a0a0a0a0 +WPR_META_SIZE = 256 + +# NV_RISCV_BOOTLDR_PARAMS / NV_RISCV_BOOT_PARAMS layout (header). +BOOTLDR_BOOT_TYPE_RM = 0x4E56524D # 'NVRM' +BOOTLDR_VERSION = 1 +BOOTARGS_DEFAULT_SIZE = 0x78 # NV_RISCV_BOOT_PARAMS (120 bytes) +LIBOS_MEMORY_REGION_CONTIGUOUS = 1 +LIBOS_MEMORY_REGION_LOC_FB = 2 +LIBOS_MEMORY_REGION_LOC_SYSMEM = 1 +WPRM_ID8 = 0x5750524D # 'WPRM' + + +def _int(value, default=None): + if value is None: + return default + if isinstance(value, str): + return int(value, 0) + return int(value) + + +def _write_u32(machine, addr: int, value: int) -> None: + machine.write_mem(addr, (value & 0xFFFFFFFF).to_bytes(4, "little")) + + +def _write_u64(machine, addr: int, value: int) -> None: + machine.write_mem(addr, (value & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "little")) + + +def build_bootargs(machine, *, bootargs_pa: int, core_dump_pa: int, + core_dump_size: int, wpr_meta_pa: int, + wpr_meta_size: int = WPR_META_SIZE, + bootargs_size: int = BOOTARGS_DEFAULT_SIZE) -> None: + """Write ``NV_RISCV_BOOT_PARAMS`` and its ``WPRM`` init arg. + + Assumption (profile-recorded): the real SEC2 booter/host fills these; we + reconstruct the documented structure and one ``WPRM`` libos init argument + pointing at the ``GspFwWprMeta`` written by :func:`build_wpr_meta`. + """ + _write_u32(machine, bootargs_pa + 0x00, BOOTLDR_BOOT_TYPE_RM) + machine.write_mem(bootargs_pa + 0x04, (bootargs_size & 0xFFFF).to_bytes(2, "little")) + machine.write_mem(bootargs_pa + 0x06, bytes([BOOTLDR_VERSION])) + machine.write_mem(bootargs_pa + 0x07, b"\x00") + _write_u64(machine, bootargs_pa + 0x08, core_dump_pa) + _write_u32(machine, bootargs_pa + 0x10, core_dump_size) + # libos.initArgs[0]: WPRM in FB, contiguous. + base = bootargs_pa + 0x18 + _write_u64(machine, base + 0x00, WPRM_ID8) + _write_u64(machine, base + 0x08, wpr_meta_pa) + _write_u64(machine, base + 0x10, wpr_meta_size) + machine.write_mem(base + 0x18, bytes([LIBOS_MEMORY_REGION_CONTIGUOUS, + LIBOS_MEMORY_REGION_LOC_FB])) + # initArgs[1..2] stay zero (id8 == 0 terminates the array). + + +def build_wpr_meta(machine, *, wpr_meta_pa: int, elf_pa: int, elf_size: int, + wpr_start: int, wpr_end: int, + boot_bin_offset: int, gsp_fw_offset: int, + fb_size: int) -> None: + """Write a ``GspFwWprMeta`` (header layout) into RAM. + + Assumption (profile-recorded): the booter/CPU-RM produces this structure; + we place it with a self-consistent layout that satisfies the documented + validation (magic/revision/verified, monotonic FB ranges). Values are + declared in the profile, never derived from silicon. + """ + machine.write_mem(wpr_meta_pa, bytes(WPR_META_SIZE)) + _write_u64(machine, wpr_meta_pa + 0x00, GSP_FW_WPR_META_MAGIC) + _write_u64(machine, wpr_meta_pa + 0x08, GSP_FW_WPR_META_REVISION) + _write_u64(machine, wpr_meta_pa + 0x10, elf_pa) + _write_u64(machine, wpr_meta_pa + 0x18, elf_size) + _write_u64(machine, wpr_meta_pa + 0x20, 0) # sysmemAddrOfBootloader + _write_u64(machine, wpr_meta_pa + 0x28, 0) # sizeOfBootloader + _write_u64(machine, wpr_meta_pa + 0x30, 0) # bootloaderCodeOffset + _write_u64(machine, wpr_meta_pa + 0x38, 0) # bootloaderDataOffset + _write_u64(machine, wpr_meta_pa + 0x40, 0) # bootloaderManifestOffset + _write_u64(machine, wpr_meta_pa + 0x48, 0) # sysmemAddrOfSignature + _write_u64(machine, wpr_meta_pa + 0x50, 0) # sizeOfSignature + _write_u64(machine, wpr_meta_pa + 0x58, wpr_start) # gspFwRsvdStart + _write_u64(machine, wpr_meta_pa + 0x60, wpr_start) # nonWprHeapOffset + _write_u64(machine, wpr_meta_pa + 0x68, 0x10000) # nonWprHeapSize + _write_u64(machine, wpr_meta_pa + 0x70, wpr_start) # gspFwWprStart + _write_u64(machine, wpr_meta_pa + 0x78, wpr_start) # gspFwHeapOffset + _write_u64(machine, wpr_meta_pa + 0x80, wpr_end - wpr_start) # heap size + _write_u64(machine, wpr_meta_pa + 0x88, gsp_fw_offset) # gspFwOffset + _write_u64(machine, wpr_meta_pa + 0x90, boot_bin_offset) # bootBinOffset + _write_u64(machine, wpr_meta_pa + 0x98, 0) # frtsOffset + _write_u64(machine, wpr_meta_pa + 0xA0, 0) # frtsSize + _write_u64(machine, wpr_meta_pa + 0xA8, wpr_end) # gspFwWprEnd + _write_u64(machine, wpr_meta_pa + 0xB0, fb_size) # fbSize + _write_u64(machine, wpr_meta_pa + 0xB8, wpr_end) # vgaWorkspaceOffset + _write_u64(machine, wpr_meta_pa + 0xC0, 0x100000) # vgaWorkspaceSize + _write_u64(machine, wpr_meta_pa + 0xC8, 0) # bootCount + _write_u64(machine, wpr_meta_pa + 0xD0, 0) # partitionRpcAddr + _write_u64(machine, wpr_meta_pa + 0xF8, GSP_FW_WPR_META_VERIFIED) + + +#: Observed boot milestones (firmware vaddrs) used to classify the rung. +#: Evidence: disassembly of the 570.144 image (bootargs consumer reads +#: NV_RISCV_BOOT_PARAMS; the relocation loop copies from the DMEM aperture +#: into the 0x400E000 runtime region). +MILESTONES = ( + ("bootargs_consumer", 0x4007A30), + ("relocation_copy", 0x4007B7C), +) + + +@dataclass +class GspBootResult: + ran: bool + reason: str + image: str + profile: str | None = None + profile_name: str | None = None + image_sha256: str | None = None + session_dir: str | None = None + instructions: int = 0 + cycles: int = 0 + cycle_model: str | None = None + stop_reason: str | None = None + stop_pc: int | None = None + stop_pc_fw: int | None = None + rung: str = "B0" + first_unknown_mmio: dict | None = None + mmio_unknown: int = 0 + device_unhandled: int = 0 + device_accesses: int = 0 + device_state_sha256: str | None = None + registers: dict = field(default_factory=dict) + shim: dict = field(default_factory=dict) + milestones: list = field(default_factory=list) + device_map: object | None = field(default=None, repr=False) + + def to_dict(self) -> dict: + return { + "schema": "greeneye-gsp-boot/1", + "ran": self.ran, + "reason": self.reason, + "image": self.image, + "image_sha256": self.image_sha256, + "profile": self.profile, + "profile_name": self.profile_name, + "session": self.session_dir, + "rung": self.rung, + "instructions_executed": self.instructions, + "cycles": self.cycles, + "cycle_model": self.cycle_model, + "stop": {"reason": self.stop_reason, "pc": self.stop_pc, + "pc_fw": self.stop_pc_fw}, + "first_unknown_mmio": self.first_unknown_mmio, + "mmio_unknown": self.mmio_unknown, + "device_unhandled": self.device_unhandled, + "device_accesses": self.device_accesses, + "device_state_sha256": self.device_state_sha256, + "registers": self.registers, + "boot_shim": self.shim, + "milestones": list(self.milestones), + } + + +def _fw_pc(pc: int) -> int: + return pc + + +def classify_rung(machine, *, shim: dict) -> str: + """Best rung supported by the observed run (never inferred, only observed).""" + if machine.mmio_unknown and machine.mmio_log: + first = machine.mmio_log[0] + if first["addr"] == 0x2000000000000A00: + return "B0" + if shim.get("applied"): + # The firmware consumed the boot shim and advanced past the first + # unknown; B2 is claimed only when the run went past the chip gate and + # exercised the MPU/bootargs path (the runner checks the PC). + return shim.get("rung", "B1") + return "B1" + + +def run_boot( + *, + image: str = DEFAULT_IMAGE, + profile_path: str = DEFAULT_PROFILE, + instructions: int = DEFAULT_INSTRUCTIONS, + record=None, + name: str = "gsp-boot", + use_shim: bool = True, + shim_overrides: dict | None = None, +) -> GspBootResult: + """Run one bounded boot attempt and record a session.""" + if instructions <= 0 or instructions > MAX_ATTEMPT_INSTRUCTIONS: + raise GreenEyeError( + f"instructions must be in 1..{MAX_ATTEMPT_INSTRUCTIONS}, got {instructions}") + if not os.path.isfile(image): + return GspBootResult(ran=False, reason=f"image absent: {image}", image=image) + if not os.path.isfile(profile_path): + return GspBootResult(ran=False, reason=f"profile absent: {profile_path}", + image=image) + + profile = load_profile(profile_path) + image_obj = load_elf(image) + if not image_obj.is_riscv: + raise GreenEyeError(f"{image}: not a RISC-V ELF image") + sha = firmware_sha256(image) + + load = profile.get("load", {}) + ram_base = _int(load.get("ram_base", 0)) + memory_mb = _int(load.get("memory_mb", 12288)) + + from greeneye.sim_backend import launch_native # deferred + + session = launch_native( + kernel=image, + memory_mb=memory_mb, + ram_base=ram_base, + record=True if record is None else record, + name=name, + device_map=None, + unknown_mmio="lenient", + bias=0, + ) + machine = session.target.machine + machine.devices = registry_from_profile(profile, machine=machine) + machine.translator = getattr(machine.devices, "dma", None) + + shim_meta = {"enabled": bool(use_shim), "assumptions": []} + if use_shim: + shim = dict(profile.get("boot_shim", {})) + if shim_overrides: + shim.update(shim_overrides) + shim["_image_path"] = image + _apply_boot_shim(machine, shim, image_obj, shim_meta) + + milestones_hit: list[str] = [] + try: + reason, pc, a, b = _run_with_milestones(machine, instructions, milestones_hit) + model = machine.cycle_model + stop_reason = STOP_REASONS.get(reason, str(reason)) + first = machine.mmio_log[0] if machine.mmio_log else None + reg = machine.devices + state = reg.state() if reg is not None else None + state_json = json.dumps(state, sort_keys=True, default=str) if state else "" + state_sha = hashlib.sha256(state_json.encode()).hexdigest() if state else None + registers = {f"x{i}": machine.x[i] for i in range(32) if i and machine.x[i]} + registers["pc"] = pc + rung = _rung_from_run(machine, shim_meta, milestones_hit) + shim_meta["rung"] = rung + meta = { + "kind": "gsp-boot", + "profile": os.path.basename(profile_path), + "profile_name": profile.get("name"), + "firmware_sha256": sha, + "ram_base": ram_base, + "instructions_executed": machine.insns, + "cycles": machine.cycles, + "cycle_model": model.name if model is not None else None, + "stop_reason": stop_reason, + "stop_pc": pc, + "rung": rung, + "milestones": list(milestones_hit), + "mmio_unknown": machine.mmio_unknown, + "first_unknown_mmio": first, + "device_unhandled": getattr(reg, "unhandled", 0), + "device_accesses": len(getattr(reg, "accesses", ())), + "device_state_sha256": state_sha, + "boot_shim": shim_meta, + } + if getattr(session.recorder, "dir", None) is not None: + if state is not None: + rel = session.recorder.blob( + "device-state.json", + json.dumps(state, indent=1, sort_keys=True, default=str).encode(), + ) + meta["device_state_blob"] = rel + session.recorder.meta.update(meta) + session.note( + "gsp_boot", + rung=rung, + instructions=machine.insns, + stop_reason=stop_reason, + stop_pc=pc, + ) + session_dir = getattr(session.recorder, "dir", None) + finally: + session.close() + + return GspBootResult( + ran=True, + reason="ok", + image=image, + profile=profile_path, + profile_name=profile.get("name"), + image_sha256=sha, + session_dir=session_dir, + instructions=machine.insns, + cycles=machine.cycles, + cycle_model=model.name if model is not None else None, + stop_reason=stop_reason, + stop_pc=pc, + stop_pc_fw=_fw_pc(pc), + rung=rung, + first_unknown_mmio=first, + mmio_unknown=machine.mmio_unknown, + device_unhandled=getattr(reg, "unhandled", 0), + device_accesses=len(getattr(reg, "accesses", ())), + device_state_sha256=state_sha, + registers=registers, + shim=shim_meta, + milestones=list(milestones_hit), + ) + + +def _run_with_milestones(machine, budget: int, hit: list[str]): + """Run the budget, stopping at each milestone once to record it. + + Milestone hits are evidence of boot-stage progress (they are firmware + vaddrs reached), not inferred from timing or heuristics. + """ + remaining = budget + while remaining > 0: + pending = {addr for name, addr in MILESTONES if name not in hit} + reason, pc, a, b = machine.run(remaining, breakpoints=pending) + remaining = budget - machine.insns + if reason == R_BP: + for name, addr in MILESTONES: + if addr == pc and name not in hit: + hit.append(name) + break + continue + return reason, pc, a, b + return R_BUDGET, machine.pc, 0, 0 + + +def _rung_from_run(machine, shim_meta: dict, milestones_hit) -> str: + """Observed rung from milestones and the unknown-MMIO surface. + + B0: the first unmodelled access is the PMC ``BOOT_42`` chip-ID read. + B1: the device model served the whole run (no unknown MMIO) and the boot + chip gate was passed; the run may stop at the bootargs panic/halt. + B2: the boot shim was consumed and the firmware reached its relocation + stage (milestone ``relocation_copy``) -- boot-stage progress beyond + the bootargs consumer. + """ + first_unknown = machine.mmio_log[0] if machine.mmio_log else None + if first_unknown is not None and first_unknown["addr"] == 0x2000000000000A00: + return "B0" + if "relocation_copy" in milestones_hit: + return "B2" + return "B1" + + +def _rung_note(rung: str) -> str: + return { + "B0": "first MMIO is the unmodelled PMC BOOT_42 chip-ID read", + "B1": "device model serves the boot path; bootargs consumer reached", + "B2": "boot-stage progress past the bootargs consumer (relocation)", + }.get(rung, rung) + + +def _apply_boot_shim(machine, shim: dict, image_obj, shim_meta: dict) -> None: + """Write profile-declared reset state (documented assumption).""" + registers = {k: (int(v, 0) if isinstance(v, str) else int(v)) + for k, v in shim.get("registers", {}).items()} + machine.apply_reset_regs(registers) + bootargs_pa = shim.get("bootargs_pa") + if bootargs_pa is not None: + bootargs_pa = int(bootargs_pa, 0) if isinstance(bootargs_pa, str) else int(bootargs_pa) + core_dump_pa = _int(shim.get("core_dump_pa", 0x40000)) + core_dump_size = _int(shim.get("core_dump_size", 0x10000)) + build_bootargs( + machine, + bootargs_pa=bootargs_pa, + core_dump_pa=core_dump_pa, + core_dump_size=core_dump_size, + wpr_meta_pa=_int(shim.get("wpr_meta_pa", 0x90000)), + wpr_meta_size=_int(shim.get("wpr_meta_size", WPR_META_SIZE)), + bootargs_size=_int(shim.get("bootargs_size", BOOTARGS_DEFAULT_SIZE)), + ) + shim_meta["assumptions"].append( + "NV_RISCV_BOOT_PARAMS reconstructed at 0x%x from the documented " + "layout (header); real value is host/booter-provided" % bootargs_pa) + if shim.get("wpr_meta_pa") is not None and shim.get("wpr"): + wpr = shim["wpr"] + build_wpr_meta( + machine, + wpr_meta_pa=_int(shim["wpr_meta_pa"]), + elf_pa=_int(wpr.get("elf_pa", image_obj.entry)), + elf_size=_int(wpr.get("elf_size", image_obj.max_vaddr - 0x4000000)), + wpr_start=_int(wpr.get("wpr_start", 0x2000000)), + wpr_end=_int(wpr.get("wpr_end", 0x6000000)), + boot_bin_offset=_int(wpr.get("boot_bin_offset", 0x2000000)), + gsp_fw_offset=_int(wpr.get("gsp_fw_offset", 0x4000000)), + fb_size=_int(wpr.get("fb_size", 0x200000000)), + ) + shim_meta["assumptions"].append( + "GspFwWprMeta reconstructed from the documented layout (header); " + "real value is booter-provided") + shim_meta["applied"] = True + shim_meta["registers"] = {k: registers[k] for k in sorted(registers)} + image_at = shim.get("image_at") + if image_at is not None: + addr = _int(image_at) + with open(shim["_image_path"], "rb") as fh: + blob = fh.read() + machine.write_mem(addr, blob) + shim_meta["assumptions"].append( + "raw firmware image mirrored at 0x%x (DMEM) so the app's own " + "self-relocation copy is self-consistent; real value is " + "booter-DMA-provided" % addr) + shim_meta["image_at"] = addr + shim_meta["image_bytes"] = len(blob) + + +def _print_result(result: GspBootResult, out=sys.stdout) -> None: + print(f"image: {result.image}", file=out) + print(f"profile: {result.profile_name} ({result.profile})", file=out) + print(f"session: {result.session_dir}", file=out) + print(f"rung: {result.rung}", file=out) + print(f"instructions: {result.instructions}", file=out) + print(f"cycles (modeled): {result.cycles}", file=out) + print(f"stop: {result.stop_reason} pc={result.stop_pc:#x}" + if result.stop_pc is not None else f"stop: {result.stop_reason}", file=out) + print(f"unknown MMIO accesses: {result.mmio_unknown}", file=out) + print(f"device accesses: {result.device_accesses} " + f"(unhandled in windows: {result.device_unhandled})", file=out) + if result.first_unknown_mmio: + first = result.first_unknown_mmio + print(f"first unknown MMIO: pc={first['pc']:#x} addr={first['addr']:#x} " + f"size={first['size']} rw={first['rw']}", file=out) + print("note: device semantics carry their evidence in docs/36; " + "boot-shim values are declared assumptions", file=out) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python3 -m sim.gsp_boot", + description=("Bounded real-firmware GSP boot attempt with the device model " + "and versioned profile (read-only image; no hardware).")) + parser.add_argument("--image", default=DEFAULT_IMAGE) + parser.add_argument("--profile", default=DEFAULT_PROFILE) + parser.add_argument("--instructions", type=int, default=DEFAULT_INSTRUCTIONS) + parser.add_argument("--record", help="session directory (default: auto)") + parser.add_argument("--name", default="gsp-boot") + parser.add_argument("--no-shim", action="store_true", + help="run without the boot shim (pure device model)") + parser.add_argument("--json", action="store_true") + args = parser.parse_args(argv) + try: + result = run_boot( + image=args.image, + profile_path=args.profile, + instructions=args.instructions, + record=args.record, + name=args.name, + use_shim=not args.no_shim, + ) + except GreenEyeError as exc: + print(f"[gsp-boot] error: {exc}", file=sys.stderr) + return 1 + if not result.ran: + print(f"[gsp-boot] skipped: {result.reason}", file=sys.stderr) + return 2 + if args.json: + print(json.dumps(result.to_dict(), indent=1, sort_keys=True)) + else: + _print_result(result) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/debug/risc-v-gsp-rm-vm/sim/gsp_profile.py b/debug/risc-v-gsp-rm-vm/sim/gsp_profile.py new file mode 100644 index 0000000..6d87389 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/gsp_profile.py @@ -0,0 +1,317 @@ +"""GSP cycle-0 profile: bounded, deterministic real-firmware run with cycles. + +Loads the TU104 570.144 GSP flat image (read-only), maps it the same way the +device-map/probe path does (``bias=0x80000000``, ``greeneye.elf`` PT_LOAD plan, +lenient MMIO from a :class:`greeneye.device_map.DeviceMap`), executes a bounded +instruction budget, and writes a normal ``greeneye-session/1`` directory whose +metadata carries the modeled instruction/cycle totals and the stop reason. + +**Fidelity:** this is a bounded lenient first-touch profile, not a boot. The +cycle figures come from :mod:`sim.cycles` - modeled defaults, not silicon +timings (the simulator's L2 timing is *not* modelled; docs/06-ACCURACY.md, +docs/32-GSP-CYCLE-MODEL.md). The run is deterministic: two identical +invocations execute the same instructions and report the same cycle total. + +CLI:: + + python3 -m sim.gsp_profile --instructions 200000 + python3 -m sim.gsp_profile --record dumps/gsp-cycle0 --json + +The image is loaded read-only; nothing is written back to it, and no hardware, +GPU, module, guest or network is touched. +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +from dataclasses import dataclass, field as _field + +from greeneye.device_map import DeviceMap, firmware_sha256 +from greeneye.elf import load_elf +from greeneye.errors import GreenEyeError +from sim.machine import ( + R_BP, + R_BUDGET, + R_EBREAK, + R_ECALL, + R_FAULT, + R_STEP, + R_WATCH, +) + +#: Tool root (the directory containing ``sim/`` and ``greeneye/``). +TOOL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +#: The TU104 570.144 GSP firmware, as laid out in the workspace tree. +DEFAULT_IMAGE = os.path.normpath( + os.path.join( + TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", "gsp-570.144.fwimage.bin" + ) +) + +#: GPU physical addresses are relocated into simulator RAM at this bias +#: (same convention as the probe/device-map path). +DEFAULT_BIAS = 0x80000000 + +#: Small default budget: enough to pass the documented first-touch fault. +DEFAULT_INSTRUCTIONS = 200_000 + +#: Machine stop reasons -> stable session labels. +STOP_REASONS = { + R_BUDGET: "budget", + R_BP: "breakpoint", + R_WATCH: "watch", + R_EBREAK: "ebreak", + R_FAULT: "fault", + R_STEP: "step", + R_ECALL: "ecall", +} + + +@dataclass +class GspProfileResult: + """Outcome of one :func:`run_profile` invocation. + + ``ran`` is False (with ``reason`` explaining why) when the image is absent + and the caller should skip cleanly; ``device_map`` is the in-memory map + used/learned during the run (never serialised by :meth:`to_dict`). + """ + + ran: bool + reason: str + image: str + image_sha256: str | None = None + session_dir: str | None = None + instructions: int = 0 + cycles: int = 0 + cycle_model: str | None = None + stop_reason: str | None = None + stop_pc: int | None = None + stop_pc_fw: int | None = None + first_unknown_mmio: dict | None = None + mmio_unknown: int = 0 + device_map: DeviceMap | None = _field(default=None, repr=False) + + def to_dict(self) -> dict: + data = { + "schema": "greeneye-gsp-cycle-profile/1", + "ran": self.ran, + "reason": self.reason, + "image": self.image, + "image_sha256": self.image_sha256, + "session": self.session_dir, + "instructions_executed": self.instructions, + "cycles": self.cycles, + "cycle_model": self.cycle_model, + "fidelity": "modeled cycles; L2 (timing) not modelled (docs/06)", + "stop": {"reason": self.stop_reason, "pc": self.stop_pc, "pc_fw": self.stop_pc_fw}, + "first_unknown_mmio": self.first_unknown_mmio, + "mmio_unknown": self.mmio_unknown, + } + if self.device_map is not None: + data["device_map_entries"] = len(self.device_map) + return data + + +def _fw_pc(pc: int, bias: int) -> int: + if not bias or pc == 0 or pc < bias: + return pc + return pc - bias + + +def _memory_mb(image, bias: int, ram_base: int = DEFAULT_BIAS) -> int: + """RAM size that covers the relocated image, as the probe path computes it.""" + end = image.max_vaddr + bias + need = end - ram_base + (1 << 20) + return max(256, (need + (1 << 20) - 1) // (1 << 20)) + + +def run_profile( + *, + image: str = DEFAULT_IMAGE, + instructions: int = DEFAULT_INSTRUCTIONS, + bias: int = DEFAULT_BIAS, + record=None, + device_map=None, + learn_mmio: bool = False, + name: str = "gsp-cycle-profile", + cycle_model=None, +) -> GspProfileResult: + """Run the bounded cycle profile; skip cleanly when the image is absent. + + ``record`` is a session directory (or True for an auto-named one under + ``dumps/``); the default ``None`` still records, so a session is always + written. ``device_map`` may be a :class:`~greeneye.device_map.DeviceMap`, + a path to one, or None for an empty lenient map. + """ + if instructions <= 0: + raise GreenEyeError(f"instructions must be positive, got {instructions}") + if not os.path.isfile(image): + return GspProfileResult(ran=False, reason=f"image absent: {image}", image=image) + + image_obj = load_elf(image) + if not image_obj.is_riscv: + raise GreenEyeError(f"{image}: not a RISC-V ELF image") + sha = firmware_sha256(image) + + if isinstance(device_map, (str, os.PathLike)): + dm = DeviceMap.load(os.fspath(device_map)) + elif device_map is None: + dm = DeviceMap( + meta={ + "source": "gsp-profile", + "firmware": os.path.basename(image), + "firmware_sha256": sha, + "bias": bias, + } + ) + else: + dm = device_map + + from greeneye.sim_backend import launch_native # deferred: keeps sim/ standalone + + session = launch_native( + kernel=image, + memory_mb=_memory_mb(image_obj, bias), + record=True if record is None else record, + name=name, + device_map=dm, + unknown_mmio="lenient", + learn_mmio=learn_mmio, + bias=bias, + cycle_model=cycle_model, + ) + machine = session.target.machine + try: + reason, pc, _a, _b = machine.run(instructions) + first = machine.mmio_log[0] if machine.mmio_log else None + model = machine.cycle_model + stop_reason = STOP_REASONS.get(reason, str(reason)) + stop_pc_fw = _fw_pc(pc, bias) + meta = { + "kind": "gsp-cycle-profile", + "firmware_sha256": sha, + "bias": bias, + "instructions_executed": machine.insns, + "cycles": machine.cycles, + "cycle_model": model.name if model is not None else None, + "cycle_fidelity": "modeled cycles, not measured (docs/32-GSP-CYCLE-MODEL.md)", + "stop_reason": stop_reason, + "stop_pc": pc, + "stop_pc_fw": stop_pc_fw, + "mmio_unknown": machine.mmio_unknown, + "first_unknown_mmio": first, + } + if model is not None: + meta["cycle_model_table"] = model.describe() + if getattr(session.recorder, "dir", None) is not None: + session.recorder.meta.update(meta) + session.note( + "gsp_cycle_profile", + instructions=machine.insns, + cycles=machine.cycles, + stop_reason=stop_reason, + stop_pc_fw=stop_pc_fw, + ) + session_dir = getattr(session.recorder, "dir", None) + finally: + session.close() + + return GspProfileResult( + ran=True, + reason="ok", + image=image, + image_sha256=sha, + session_dir=session_dir, + instructions=machine.insns, + cycles=machine.cycles, + cycle_model=model.name if model is not None else None, + stop_reason=stop_reason, + stop_pc=pc, + stop_pc_fw=stop_pc_fw, + first_unknown_mmio=first, + mmio_unknown=machine.mmio_unknown, + device_map=dm, + ) + + +def _print_result(result: GspProfileResult, out=sys.stdout) -> None: + print(f"image: {result.image}", file=out) + print(f"sha256: {result.image_sha256}", file=out) + print(f"session: {result.session_dir}", file=out) + print(f"instructions: {result.instructions}", file=out) + print(f"cycles (modeled): {result.cycles}", file=out) + print(f"cycle model: {result.cycle_model}", file=out) + if result.instructions: + print(f"cycles/instruction: {result.cycles / result.instructions:.3f}", file=out) + print( + f"stop: {result.stop_reason} pc={result.stop_pc:#x} (fw {result.stop_pc_fw:#x})", + file=out, + ) + print(f"unknown MMIO accesses: {result.mmio_unknown}", file=out) + if result.first_unknown_mmio: + first = result.first_unknown_mmio + print( + "first unknown MMIO: " + f"pc={first['pc']:#x} addr={first['addr']:#x} " + f"size={first['size']} rw={first['rw']}", + file=out, + ) + print( + "note: cycles are modeled (sim/cycles.py defaults); no silicon timing claim", + file=out, + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python3 -m sim.gsp_profile", + description=( + "Bounded deterministic GSP cycle-0 profile (modeled cycles; no silicon claim). " + "Loads the real firmware read-only, runs under lenient MMIO and records a session." + ), + ) + parser.add_argument("--image", default=DEFAULT_IMAGE, help="flat GSP firmware image") + parser.add_argument( + "--instructions", type=int, default=DEFAULT_INSTRUCTIONS, help="instruction budget" + ) + parser.add_argument("--bias", type=lambda s: int(s, 0), default=DEFAULT_BIAS) + parser.add_argument("--record", help="session directory (default: auto under dumps/)") + parser.add_argument("--device-map", help="existing greeneye-device-map/1 JSON to serve MMIO") + parser.add_argument("--learn", action="store_true", help="learn observed MMIO into the map") + parser.add_argument("--learn-out", help="save the learned/used map here") + parser.add_argument("--name", default="gsp-cycle-profile") + parser.add_argument("--json", action="store_true", help="emit the result as JSON") + args = parser.parse_args(argv) + + try: + result = run_profile( + image=args.image, + instructions=args.instructions, + bias=args.bias, + record=args.record, + device_map=args.device_map, + learn_mmio=args.learn, + name=args.name, + ) + except GreenEyeError as exc: + print(f"[gsp-profile] error: {exc}", file=sys.stderr) + return 1 + + if not result.ran: + print(f"[gsp-profile] skipped: {result.reason}", file=sys.stderr) + return 2 + if args.learn_out and result.device_map is not None: + result.device_map.save(args.learn_out) + print(f"[gsp-profile] device map ({len(result.device_map)} entries) -> {args.learn_out}") + if args.json: + print(json.dumps(result.to_dict(), indent=1, sort_keys=True)) + else: + _print_result(result) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/debug/risc-v-gsp-rm-vm/sim/machine.py b/debug/risc-v-gsp-rm-vm/sim/machine.py index 8d77dd8..9908753 100644 --- a/debug/risc-v-gsp-rm-vm/sim/machine.py +++ b/debug/risc-v-gsp-rm-vm/sim/machine.py @@ -5,12 +5,20 @@ The execution loop is a monolithic dispatch over the cached decode tuples from image and the instruction budget handed to ``run``: no host clock, no threads, no entropy. RAM is an anonymous ``mmap`` (lazily paged, so a 256 MiB region is cheap); all other addresses resolve to the UART or fault. + +Alongside the instruction count (virtual time, ``insns``) the machine keeps a +``cycles`` counter advanced by :mod:`sim.cycles`, a table-driven **modeled** +instruction-class cost. It changes no execution behaviour: with the default +model enabled, ``insns``, stops and register/memory state are exactly as +before; ``cycle_model=False`` leaves ``cycles`` at 0. See +``docs/32-GSP-CYCLE-MODEL.md``. """ from __future__ import annotations import mmap import struct +from .cycles import DEFAULT_MODEL from .decode import ( A_ADD, A_AND, @@ -22,7 +30,6 @@ from .decode import ( A_MULHU, A_OR, A_REM, - A_REMU, A_SLL, A_SLT, A_SLTU, @@ -38,8 +45,6 @@ from .decode import ( I_SLLIW, I_SLTI, I_SLTIU, - I_SRAI, - I_SRAIW, I_SRLI, I_SRLIW, I_XORI, @@ -50,7 +55,6 @@ from .decode import ( K_EBREAK, K_ECALL, K_FENCE, - K_ILLEGAL, K_JAL, K_JALR, K_LOAD, @@ -182,6 +186,12 @@ class Machine: ``learn_mmio=True``, writes the observation back into the map. Nothing is logged or counted when lenient mode is off, and debugger ``read_mem``/ ``write_mem`` stay strict. + + ``cycle_model`` selects the modeled cycle accounting (:mod:`sim.cycles`): + ``None`` (default) uses :data:`sim.cycles.DEFAULT_MODEL`, a + :class:`~sim.cycles.CycleModel` instance uses that table, and ``False`` + disables accounting (``cycles`` stays 0). Accounting never changes + execution. """ def __init__( @@ -192,14 +202,36 @@ class Machine: unknown_mmio: str = "fault", learn_mmio: bool = False, bias: int = 0, + cycle_model=None, + devices=None, + reset_regs=None, + translator=None, ): if unknown_mmio not in UNKNOWN_MMIO_MODES: raise ValueError(f"unknown_mmio must be one of {UNKNOWN_MMIO_MODES}, got {unknown_mmio!r}") + if cycle_model is False: + cycle_model = None + elif cycle_model is None: + cycle_model = DEFAULT_MODEL + self.cycle_model = cycle_model + self.cycles = 0 + if cycle_model is None: + self._cycle_base = None + self._cycle_op = None + self._cycle_mem = 0 + self._cycle_io = 0 + else: + self._cycle_base = cycle_model.base_costs + self._cycle_op = cycle_model.op_costs + self._cycle_mem = cycle_model.memory_latency + self._cycle_io = cycle_model.mmio_latency self.ram_base = ram_base self.ram_size = memory_mb * 1024 * 1024 self.ram = mmap.mmap(-1, self.ram_size) self.uart = Uart16550() self.device_map = device_map + self.devices = devices + self.translator = translator # aperture -> RAM seam (sim.devices.dma) self.unknown_mmio = unknown_mmio self.learn_mmio = bool(learn_mmio) self.bias = bias # PC relocation for learned-map firmware addresses @@ -219,6 +251,34 @@ class Machine: self._u16p = self._u16.pack_into self._u32p = self._u32.pack_into self._u64p = self._u64.pack_into + if reset_regs: + self.apply_reset_regs(reset_regs) + + # -- reset state (boot shim support) ----------------------------------- + + def apply_reset_regs(self, regs: dict) -> None: + """Set initial register values (boot shim: a0-a5 bootargs, etc.). + + Accepts integer indices (0-31) or register names (``"a0"``..``"a7"``, + ``"ra"``, ``"sp"``, ``"tp"``, ``"s0"``.., ``"t0"``..). x0 is ignored. + """ + names = {"zero": 0, "ra": 1, "sp": 2, "gp": 3, "tp": 4, + "t0": 5, "t1": 6, "t2": 7, + "s0": 8, "fp": 8, "s1": 9, + "a0": 10, "a1": 11, "a2": 12, "a3": 13, "a4": 14, "a5": 15, + "a6": 16, "a7": 17, + "s2": 18, "s3": 19, "s4": 20, "s5": 21, "s6": 22, "s7": 23, + "s8": 24, "s9": 25, "s10": 26, "s11": 27, + "t3": 28, "t4": 29, "t5": 30, "t6": 31} + for key, value in regs.items(): + index = key if isinstance(key, int) else names.get(str(key)) + if index is None: + raise ValueError(f"unknown register name {key!r}") + self.set_reg(int(index), int(value)) + + def insn_count(self) -> int: + """Virtual time (instruction count) for modeled clock devices.""" + return self.insns # -- image loading ----------------------------------------------------- @@ -262,6 +322,21 @@ class Machine: out.append(self.uart.read(addr - UART_BASE)) addr += 1 size -= 1 + elif self.translator is not None and self.translator.translate(addr) is not None: + sim = self.translator.translate(addr) + off = sim - self.ram_base + if off < 0 or off + size > self.ram_size: + raise Fault(5, addr) + out += self.ram[off : off + size] + addr += size + size = 0 + elif self.devices is not None: + peeked = self.devices.peek(addr, size) + if peeked is None: + raise Fault(5, addr) + out += peeked + addr += size + size = 0 else: raise Fault(5, addr) return bytes(out) @@ -280,6 +355,17 @@ class Machine: elif UART_BASE <= here < UART_BASE + UART_SIZE: self.uart.write(here - UART_BASE, data[offset]) offset += 1 + elif self.translator is not None and self.translator.translate(here) is not None: + sim = self.translator.translate(here) + off = sim - self.ram_base + if off < 0 or off + remaining > self.ram_size: + raise Fault(7, here) + self.ram[off : off + remaining] = data[offset:] + offset = len(data) + elif self.devices is not None and self.devices.write( + here, remaining, int.from_bytes(data[offset:], "little") + ): + offset = len(data) else: raise Fault(7, here) @@ -308,9 +394,57 @@ class Machine: self.device_map = DeviceMap(meta={"source": "learn"}) self.device_map.record(addr, size, rw, value, pc=pc, bias=self.bias, source="trace") + def _aperture_load(self, addr: int, size: int) -> int | None: + """Serve a load from a translated memory aperture (FBGPA/SYSGPA).""" + if self.translator is None: + return None + sim = self.translator.translate(addr) + if sim is None: + return None + off = sim - self.ram_base + if off < 0 or off + size > self.ram_size: + return None + if size == 8: + return self._u64.unpack_from(self.ram, off)[0] + if size == 4: + return self._u32.unpack_from(self.ram, off)[0] + if size == 2: + return self._u16.unpack_from(self.ram, off)[0] + if size == 1: + return self.ram[off] + return None + + def _aperture_store(self, addr: int, size: int, value: int) -> bool: + """Serve a store into a translated memory aperture (FBGPA/SYSGPA).""" + if self.translator is None: + return False + sim = self.translator.translate(addr) + if sim is None: + return False + off = sim - self.ram_base + if off < 0 or off + size > self.ram_size: + return False + if size == 8: + self._u64p(self.ram, off, value & 0xFFFFFFFFFFFFFFFF) + elif size == 4: + self._u32p(self.ram, off, value & 0xFFFFFFFF) + elif size == 2: + self._u16p(self.ram, off, value & 0xFFFF) + else: + self.ram[off] = value & 0xFF + self.invalidate() + return True + def _mmio_load(self, addr: int, size: int, pc: int = 0) -> int: if UART_BASE <= addr < UART_BASE + UART_SIZE: return self.uart.read(addr - UART_BASE) + value = self._aperture_load(addr, size) + if value is not None: + return value + if self.devices is not None: + handled, value = self.devices.read(addr, size, pc=pc) + if handled: + return value handled, value = self._mapped_mmio(addr, size, False) if handled: return value @@ -323,6 +457,10 @@ class Machine: if UART_BASE <= addr < UART_BASE + UART_SIZE: self.uart.write(addr - UART_BASE, value) return + if self._aperture_store(addr, size, value): + return + if self.devices is not None and self.devices.write(addr, size, value, pc=pc): + return handled, _ = self._mapped_mmio(addr, size, True) if handled: if self.learn_mmio and self.device_map is not None: @@ -368,6 +506,13 @@ class Machine: a = b = 0 traced = trace is not None active = False + # Modeled cycle accounting (sim.cycles): pure table lookups, no timing. + cycle_base = self._cycle_base + cycle_op = self._cycle_op + cycle_mem = self._cycle_mem + cycle_io = self._cycle_io + counting = cycle_base is not None + cycle_sum = 0 while n < budget: if has_bps and pc in breakpoints: @@ -394,6 +539,9 @@ class Machine: cache[pc] = d kind, sub, rd, r1, r2, imm, nxt = d n += 1 + if counting: + kc = cycle_base[kind] + cycle_sum += cycle_op[sub] if kc < 0 else kc if traced: insn_pc = pc active = trace.begin(insn_pc, self.insns + n) @@ -434,6 +582,8 @@ class Machine: else: if rd: x[rd] = i16(ram, o)[0] + if counting: + cycle_sum += cycle_mem pc = nxt else: try: @@ -447,6 +597,8 @@ class Machine: x[rd] = v if active: trace.mem(insn_pc, ea, size, False, self.insns + n) + if counting: + cycle_sum += cycle_io pc = nxt continue @@ -473,6 +625,15 @@ class Machine: u16p(ram, o, value & 0xFFFF) else: ram[o] = value & 0xFF + # Self-modifying code: drop any cached decode overlapping + # the written range (firmware patches code at runtime). + p = ea & ~1 + end = ea + size + while p < end: + cache.pop(p, None) + p += 2 + if counting: + cycle_sum += cycle_mem pc = nxt else: try: @@ -484,6 +645,8 @@ class Machine: break if active: trace.mem(insn_pc, ea, size, True, self.insns + n) + if counting: + cycle_sum += cycle_io pc = nxt continue @@ -755,6 +918,8 @@ class Machine: trace.mem(insn_pc, ea, width, True, self.insns + n) if rd: x[rd] = (_sext32(old) & MASK64) if width == 4 else old + if counting: + cycle_sum += cycle_mem if 0 <= ea - base <= limit - width else cycle_io pc = nxt except Fault as f: reason = R_FAULT @@ -796,10 +961,17 @@ class Machine: self.pc = pc self.insns += n + self.cycles += cycle_sum if single_step and reason == R_BUDGET: reason = R_STEP return (reason, pc, a, b) + def device_state(self) -> dict | None: + """Serialisable state of the attached device registry (or None).""" + if self.devices is None or not hasattr(self.devices, "state"): + return None + return self.devices.state() + # -- guest memory helpers (AMO) ---------------------------------------- def _guest_load(self, addr: int, width: int, pc: int = 0) -> int: @@ -817,6 +989,11 @@ class Machine: self._u64p(self.ram, off, value) else: self._u32p(self.ram, off, value & 0xFFFFFFFF) + p = addr & ~1 + end = addr + width + while p < end: + self._cache.pop(p, None) + p += 2 return self._mmio_store(addr, width, value, pc) diff --git a/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json b/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json new file mode 100644 index 0000000..919dffe --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json @@ -0,0 +1,106 @@ +{ + "schema": "greeneye-sim-profile/1", + "name": "tu104-570.144", + "description": "Turing TU104 GSP 570.144: device model windows, load layout and boot-shim assumptions. New firmware version = new profile, not a code fork.", + "firmware": { + "file": "firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin", + "sha256": "2d1bb5c60b208cfd115f015b17dd04a6fece05fd2c0dcba3625d9d48cce3ceea", + "entry": "0x4009000", + "link_base": "0x4000000", + "max_vaddr": "0x5bb9000", + "size": 28528288 + }, + "load": { + "ram_base": "0x0", + "memory_mb": 12288, + "note": "firmware executes at its link addresses; PRIV/PMC is at 0x2000000000000000, local memory covers IMEM/DMEM/EMEM (0x0..0x300000000)." + }, + "devices": { + "pmc": { + "chip_id": "0x164", + "major": 0, + "minor": 0, + "minor_ext": 0, + "evidence": "NV_PMC_BOOT_42 at PMC+0xA00; CHIP_ID bits 28:20 compared against TU10x IDs by the boot code; TU104 = 0x164 (header field).", + "windows": [ + {"base": "0x2000000000000000", "size": "0x1000"} + ] + }, + "falcon": { + "shims": { + "debuginfo_host_consume": true + }, + "debuginfo_stage_mask": "0x700000", + "debuginfo_stage_shift": 20, + "evidence": "Falcon register header offsets; boot writes MAILBOX0/MAILBOX1/DEBUGINFO/IRQSSET(HALT) and polls DEBUGINFO bits 22:20; DMA field layout and transfer semantics from the register header. DEBUGINFO host-consume is a boot shim, not a device claim.", + "windows": [ + {"base": "0x2000000000110000", "size": "0x1000"}, + {"base": "0x300000000", "size": "0x1000"} + ] + }, + "priscv": { + "evidence": "RISC-V-local register header; boot reads TRACECTL/TRACE_RDIDX/TRACE_WTIDX/PRIV_ERR_STAT/IRQMASK through the libos private mapping at 0x300001000.", + "windows": [ + {"base": "0x2000000000111000", "size": "0x1000"}, + {"base": "0x300001000", "size": "0x1000"} + ] + }, + "timer": { + "ns_per_insn": 1.0, + "evidence": "PTIMER register header (NV_PTIMER_TIME_0 0x9400, TIME_1 0x9410). Values are MODELED from the instruction count; the firmware has not been observed reading this block in the 570.144 boot runs.", + "windows": [ + {"base": "0x2000000000009000", "size": "0x1000"} + ] + }, + "fwsec": { + "wpr2_start": "0x2000000", + "wpr2_end": "0x6000000", + "evidence": "PFB WPR2 ADDR_LO/HI register header (0x1FA824/0x1FA828, 4KiB alignment, HI points at the last 128KiB block); values are boot-shim supplied (host/booter-written on real hardware).", + "windows": [ + {"base": "0x20000000001FA000", "size": "0x1000"} + ] + }, + "csb": { + "evidence": "CSB/TRANSCOM message-window seam. No register semantics are modelled until a message-window poll is observed (B3); a profile may declare individual registers with width/value/semantics.", + "registers": {}, + "windows": [] + }, + "pmu": { + "evidence": "PMU stub: no PMU register has been observed; every access stays unhandled and logged.", + "windows": [] + }, + "dma": { + "evidence": "GSP RISC-V address map: SYSGPA/FBGPA apertures. In this flat model both apertures alias simulator RAM (documented assumption) so booter-style DMA and firmware aperture reads resolve; unmapped transfers are refused and recorded.", + "windows": [ + {"pa": "0x8180000000000000", "sim": "0x0", "size": "0x8000000"}, + {"pa": "0x6180000000000000", "sim": "0x0", "size": "0x8000000"} + ] + } + }, + "boot_shim": { + "note": "Reset-time state the real SEC2 booter/host provides. Every value here is an assumption recorded in the session; the device model never fakes a register read.", + "registers": { + "a0": "0x80000", + "a1": "0x80000", + "a2": "0x4000000", + "a3": "0x1b34ea0", + "a4": "0x4000000", + "a5": "0x0" + }, + "bootargs_pa": "0x80000", + "bootargs_size": 120, + "core_dump_pa": "0x40000", + "core_dump_size": "0x10000", + "wpr_meta_pa": "0x90000", + "image_at": "0x100000000", + "wpr": { + "elf_pa": "0x4000000", + "elf_size": "0x1b34ea0", + "wpr_start": "0x2000000", + "wpr_end": "0x6000000", + "boot_bin_offset": "0x3000000", + "gsp_fw_offset": "0x4000000", + "fb_size": "0x200000000" + } + } +} diff --git a/debug/risc-v-gsp-rm-vm/tests/test_gpgpu_sim_setup.py b/debug/risc-v-gsp-rm-vm/tests/test_gpgpu_sim_setup.py new file mode 100644 index 0000000..c52f75f --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/tests/test_gpgpu_sim_setup.py @@ -0,0 +1,43 @@ +"""Regression guard for ``gpgpu-sim/setup.sh`` clone-source policy. + +The script must default to the upstream URL and use a local mirror only via +the explicit ``GPGPUSIM_CLONE_SOURCE`` environment variable; no personal host +path may be hardcoded. These checks are static (plus ``bash -n``/``--help``) +so they need neither a network nor a GPGPU-Sim checkout. +""" +import os +import subprocess +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +TOOL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SETUP = os.path.join(TOOL_ROOT, "gpgpu-sim", "setup.sh") +PIN = "90ec3399763d7c8512cfe7dc193473086c38ca38" +UPSTREAM_URL = "https://github.com/gpgpu-sim/gpgpu-sim_distribution.git" + + +class SetupShTests(unittest.TestCase): + def test_clone_source_policy(self): + with open(SETUP, "r", encoding="utf-8") as fh: + text = fh.read() + self.assertNotIn("LOCAL_SRC=", text) + self.assertNotIn("/home/rmcore", text) + self.assertIn("GPGPUSIM_CLONE_SOURCE", text) + self.assertIn(UPSTREAM_URL, text) + self.assertIn(PIN, text) # pin enforcement stays in the script + + def test_script_syntax_and_help(self): + result = subprocess.run(["bash", "-n", SETUP], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + result = subprocess.run(["bash", SETUP, "--help"], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("GPGPUSIM_CLONE_SOURCE", result.stdout) + self.assertIn("upstream URL", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/debug/risc-v-gsp-rm-vm/tests/test_injection.py b/debug/risc-v-gsp-rm-vm/tests/test_injection.py index 5dca9cb..33cc092 100644 --- a/debug/risc-v-gsp-rm-vm/tests/test_injection.py +++ b/debug/risc-v-gsp-rm-vm/tests/test_injection.py @@ -1,6 +1,9 @@ """Integration tests for Frida-like remote calls, run-until and service injection. Skipped cleanly when QEMU, the demo firmware, clang/lld or the SDK are absent. +The native simulator is a first-class injection target too: ``NativeInjectionTests`` +proves the demo service blob loads and that direct and firmware-mailbox +invocations agree without QEMU. """ from __future__ import annotations @@ -24,12 +27,28 @@ def qemu_available() -> bool: return bool(os.environ.get("GREENEYE_QEMU") or shutil.which("qemu-system-riscv64")) +def native_available() -> bool: + try: + import greeneye.sim_backend # noqa: F401 + + return True + except Exception: + return False + + def toolchain_available() -> bool: return bool(shutil.which("clang")) and ( shutil.which("ld.lld") or os.path.exists("/usr/lib/llvm-18/bin/ld.lld") ) +def build_sdk() -> bool: + if os.path.exists(SERVICE_ELF): + return True + result = subprocess.run(["make", "-C", os.path.join(TOOL_ROOT, "sdk")], capture_output=True, text=True) + return result.returncode == 0 and os.path.exists(SERVICE_ELF) + + @unittest.skipUnless(qemu_available(), "qemu-system-riscv64 not installed") @unittest.skipUnless(os.path.exists(DEMO_ELF), "demo firmware not built (make demo)") class CallTests(unittest.TestCase): @@ -117,5 +136,47 @@ class InjectionTests(unittest.TestCase): session.close() +@unittest.skipUnless(native_available(), "native backend unavailable") +@unittest.skipUnless(os.path.exists(DEMO_ELF), "demo firmware not built (make demo)") +@unittest.skipUnless(toolchain_available(), "clang/lld not available") +class NativeInjectionTests(unittest.TestCase): + """Injection SDK against the native simulator (no QEMU required).""" + + @classmethod + def setUpClass(cls): + if not build_sdk(): + raise unittest.SkipTest("SDK build failed") + + def test_native_direct_and_firmware_invocation(self): + from greeneye.inject import invoke_remote, invoke_via_firmware, load_service, services + + session = ge.launch(kernel=DEMO_ELF, backend="native", record=True, name="unittest-native-inject") + try: + service = load_service(session, SERVICE_ELF, slot=6) + self.assertEqual(services(session), [service]) + self.assertEqual(invoke_remote(session, service, 1, 0x41), 0x41 ^ 0xBEEF) + self.assertEqual(invoke_remote(session, service, 2, 0x10, 0x20), 0x30) + session.run(duration=0.3) + self.assertEqual(invoke_via_firmware(session, service, 1, 0x41), 0x41 ^ 0xBEEF) + self.assertEqual(invoke_via_firmware(session, service, 2, 0x10, 0x20), 0x30) + finally: + session.close() + + def test_native_injection_replay_is_deterministic(self): + from greeneye.inject import invoke_remote, load_service + + results = [] + for _ in range(2): + session = ge.launch(kernel=DEMO_ELF, backend="native", record=False, name="unittest-native-inject-replay") + try: + service = load_service(session, SERVICE_ELF, slot=6) + results.append(invoke_remote(session, service, 2, 0x10, 0x20)) + results.append(session.pc) + finally: + session.close() + self.assertEqual(results[0], 0x30) + self.assertEqual(results, results[:2] + results[:2]) + + if __name__ == "__main__": unittest.main() diff --git a/debug/risc-v-gsp-rm-vm/tests/test_mcp.py b/debug/risc-v-gsp-rm-vm/tests/test_mcp.py index 2df8a4a..87a85a8 100644 --- a/debug/risc-v-gsp-rm-vm/tests/test_mcp.py +++ b/debug/risc-v-gsp-rm-vm/tests/test_mcp.py @@ -19,11 +19,14 @@ TOOL_ROOT = os.path.abspath(os.path.join(HERE, "..")) PLUGINS_DIR = os.path.join(TOOL_ROOT, "plugins") MCP_SERVER = os.path.join(PLUGINS_DIR, "mcp_server.py") DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf") +SERVICE_ELF = os.path.join(TOOL_ROOT, "sdk", "build", "injected_demo.elf") for _path in (TOOL_ROOT, PLUGINS_DIR): if _path not in sys.path: sys.path.insert(0, _path) import mcp_server as mcp # noqa: E402 +import greeneye as ge # noqa: E402 +from greeneye.inject import SERVICE_REGISTRY_ATTR, InjectedService # noqa: E402 from greeneye.rsp import StopReply # noqa: E402 from greeneye.session import Session # noqa: E402 from greeneye.symbols import Symbol, SymbolDB # noqa: E402 @@ -167,8 +170,19 @@ class ProtocolTests(McpTestCase): def test_tools_list_catalogue(self): tools = self.request(self.make_server(), "tools/list")["result"]["tools"] names = [tool["name"] for tool in tools] - self.assertEqual(len(names), 24) + self.assertGreaterEqual(len(names), 24) + self.assertEqual(len(names), len(mcp.TOOLS)) self.assertEqual(set(names), mcp._TOOL_NAMES) + for expected in ( + "backends_list", + "session_launch", + "service_list", + "sim_cycles", + "sim_device_state", + "sim_profile", + "sim_regions", + ): + self.assertIn(expected, names) for tool in tools: self.assertEqual(tool["inputSchema"]["type"], "object") self.assertTrue(tool["description"]) @@ -283,6 +297,328 @@ class ReadOnlyToolTests(McpTestCase): session.close() +class StubCycleModel: + """Minimal CycleModel shape for MCP sim-tool tests (no sim/ import needed).""" + + name = "rv64imac-modeled/1" + + def describe(self) -> dict: + return {"name": self.name, "alu": 1, "memory_latency": 3, "fidelity": "modeled"} + + +class StubMachine: + """Machine-shaped object exposing the public surface the sim tools read.""" + + def __init__(self): + self.cycles = 120 + self.insns = 100 + self.cycle_model = StubCycleModel() + self.unknown_mmio = "lenient" + self.bias = 0x80000000 + self.ram_base = BASE + self.ram_size = 0x20000 + self.mmio_log = [{"pc": BASE + 4, "addr": 0x2000000000000A00, "size": 8, "rw": "r"}] + self.mmio_unknown = 1 + self.device_map = None + + +class StubTarget: + """SimTarget-shaped handle so the server classifies the session as native.""" + + keep_alive = True + + def __init__(self, machine): + self.machine = machine + self.mmio_log = machine.mmio_log + self.mmio_unknown = machine.mmio_unknown + self.ram_base = BASE + self.memory_mb = 256 + + def kill(self): + pass + + +class NativeSimToolTests(McpTestCase): + def setUp(self): + super().setUp() + self.client.trace = None # backend detection: native + self.machine = StubMachine() + self.session.target = StubTarget(self.machine) + + def test_backends_list(self): + payload, is_error = self.call_tool(self.make_server(owns_session=True), "backends_list") + self.assertFalse(is_error) + self.assertEqual(payload["active"], "native") + self.assertTrue(payload["launchable"]) + names = {entry["name"] for entry in payload["backends"]} + self.assertEqual(names, {"native", "qemu"}) + + def test_sim_cycles(self): + payload, is_error = self.call_tool(self.make_server(), "sim_cycles") + self.assertFalse(is_error) + self.assertEqual(payload["backend"], "native") + self.assertEqual(payload["insns"], 100) + self.assertEqual(payload["cycles"], 120) + self.assertEqual(payload["cycles_per_instruction"], 1.2) + self.assertEqual(payload["cycle_model"]["name"], "rv64imac-modeled/1") + self.assertIn("modeled", payload["fidelity"]) + + def test_sim_device_state_reports_absent_device_model(self): + payload, is_error = self.call_tool(self.make_server(), "sim_device_state", limit=4) + self.assertFalse(is_error) + self.assertEqual(payload["mmio_unknown"], 1) + self.assertEqual(len(payload["mmio_log"]), 1) + self.assertEqual(payload["unknown_mmio"], "lenient") + self.assertIn("device_model", payload) + if "error" in payload["device_model"]: + self.assertEqual(payload["device_model"]["error"], "not_available") + + def test_sim_device_state_serialises_attached_registry(self): + class Registry: + def state(self): + return {"schema": "greeneye-sim-devices/1", "devices": []} + + self.machine.devices = Registry() + payload, is_error = self.call_tool(self.make_server(), "sim_device_state") + self.assertFalse(is_error) + self.assertEqual(payload["device_model"]["source"], "machine.devices.state") + self.assertEqual(payload["device_model"]["state"]["schema"], "greeneye-sim-devices/1") + json.dumps(payload) # the tool result must stay JSON-serialisable + + def test_sim_profile(self): + payload, is_error = self.call_tool(self.make_server(), "sim_profile") + self.assertFalse(is_error) + self.assertEqual(payload["backend"], "native") + self.assertEqual(payload["bias"], 0x80000000) + self.assertEqual(payload["cycles"], 120) + self.assertIn("session_meta", payload) + + def test_sim_regions(self): + payload, is_error = self.call_tool(self.make_server(), "sim_regions", limit=4) + self.assertFalse(is_error) + self.assertEqual(payload["backend"], "native") + self.assertEqual(payload["bias"], 0x80000000) + self.assertEqual(payload["unknown_mmio"], "lenient") + regions = {region["name"]: region for region in payload["regions"]} + self.assertIn("ram", regions) + self.assertIn("uart", regions) + self.assertEqual(regions["ram"]["base"], BASE) + self.assertEqual(regions["ram"]["size"], 0x20000) + self.assertEqual(regions["ram"]["end"], BASE + 0x20000) + self.assertEqual(regions["uart"]["base"], 0x10000000) + self.assertEqual(regions["uart"]["size"], 0x100) + self.assertIsNone(payload["device_map_entries"]) + self.assertEqual(payload["device_map"], []) + self.assertEqual(len(payload["observed_mmio_pages"]), 1) + page = payload["observed_mmio_pages"][0] + self.assertEqual(page["page"], 0x2000000000000A00 & ~0xFFF) + self.assertEqual(page["reads"], 1) + self.assertEqual(page["writes"], 0) + self.assertEqual(page["addresses"], [0x2000000000000A00]) + self.assertEqual(page["pcs"], [BASE + 4]) + + def test_sim_regions_device_map_entries(self): + class Entry: + address = 0x2000000000000A00 + width = 8 + kind = "r" + hits = 3 + first_epc_fw = 0x4009014 + source = "probe" + + class Map: + def entries(self): + return [Entry()] + + self.machine.device_map = Map() + payload, is_error = self.call_tool(self.make_server(), "sim_regions", limit=8) + self.assertFalse(is_error) + self.assertEqual(payload["device_map_entries"], 1) + self.assertEqual(payload["device_map"][0]["address"], 0x2000000000000A00) + self.assertEqual(payload["device_map"][0]["first_epc_fw"], 0x4009014) + self.assertEqual(payload["device_map"][0]["source"], "probe") + + def test_sim_tools_refuse_non_native(self): + # a fresh stub client without .trace is not a native session + client = StubClient() + session = Session(client=client, symbols=self.session.symbols, record=False, name="stub-qemu") + server = mcp.GreenEyeMCPServer( + session, input_stream=io.StringIO(), output_stream=io.StringIO(), log_stream=io.StringIO() + ) + try: + for name in ("sim_cycles", "sim_device_state", "sim_profile", "sim_regions"): + payload, is_error = self.call_tool(server, name) + self.assertTrue(is_error, name) + self.assertEqual(payload["error"], "not_available", name) + finally: + session.close() + + +class ServiceListTests(McpTestCase): + def test_service_list_empty_with_slots(self): + self.session.memory.write_u64(BASE + 0x200 + 0x150 + 6 * 8, 0x81000000) + payload, is_error = self.call_tool(self.make_server(), "service_list") + self.assertFalse(is_error) + self.assertEqual(payload["count"], 0) + self.assertEqual(payload["mailbox_cmd_base"], 16) + self.assertEqual(payload["slots"][6]["entry"], 0x81000000) + self.assertEqual(payload["slots"][6]["cmd"], 22) + + def test_service_list_reports_registry(self): + service = InjectedService( + path="/tmp/svc.elf", + name="svc.elf", + load_addr=0x81000000, + entry_addr=0x81000100, + dispatch_offset=0x100, + slot=6, + size=64, + ) + setattr(self.session, SERVICE_REGISTRY_ATTR, [service]) + payload, is_error = self.call_tool(self.make_server(), "service_list") + self.assertFalse(is_error) + self.assertEqual(payload["count"], 1) + self.assertEqual(payload["services"][0]["entry_addr"], 0x81000100) + self.assertEqual(payload["services"][0]["slot"], 6) + + +class SessionLaunchTests(McpTestCase): + def test_refused_in_plugin_mode(self): + payload, is_error = self.call_tool(self.make_server(owns_session=False), "session_launch", backend="native") + self.assertTrue(is_error) + self.assertEqual(payload["error"], "not_available") + + def test_invalid_backend(self): + payload, is_error = self.call_tool(self.make_server(owns_session=True), "session_launch", backend="mips") + self.assertTrue(is_error) + self.assertEqual(payload["error"], "invalid_arguments") + + def test_missing_kernel(self): + payload, is_error = self.call_tool( + self.make_server(owns_session=True), "session_launch", backend="native", kernel="/nonexistent.elf" + ) + self.assertTrue(is_error) + self.assertEqual(payload["error"], "invalid_arguments") + + def test_qemu_trace_argument_refused(self): + payload, is_error = self.call_tool( + self.make_server(owns_session=True), "session_launch", backend="qemu", trace=True + ) + self.assertTrue(is_error) + self.assertEqual(payload["error"], "invalid_arguments") + + @unittest.skipUnless(os.path.isfile(DEMO_ELF), "demo firmware not built (make demo)") + def test_launch_native_replaces_owned_session(self): + try: + import greeneye.sim_backend # noqa: F401 + except Exception as exc: + self.skipTest(f"native backend unavailable: {exc}") + server = self.make_server(owns_session=True) + # Contract (docs/20 §5/§7): session_launch is session-class and allowed + # without --allow-write; it only replaces the local served session. + self.assertFalse(server.allow_write) + with tempfile.TemporaryDirectory(prefix="mcp-launch-") as directory: + payload, is_error = self.call_tool( + server, "session_launch", backend="native", kernel=DEMO_ELF, record=directory + ) + self.assertFalse(is_error, payload) + self.assertEqual(payload["backend"], "native") + self.assertEqual(payload["session"]["kernel"], DEMO_ELF) + self.assertTrue(self.client.closed) + cycles, is_error = self.call_tool(server, "sim_cycles") + self.assertFalse(is_error, cycles) + self.assertEqual(cycles["backend"], "native") + self.assertIsNotNone(cycles["cycle_model"]) + regions, is_error = self.call_tool(server, "sim_regions") + self.assertFalse(is_error, regions) + self.assertIn("ram", {region["name"] for region in regions["regions"]}) + server.session.close() + + +@unittest.skipUnless(os.path.isfile(DEMO_ELF), "demo firmware not built (make demo)") +class NativeSessionToolTests(unittest.TestCase): + """End-to-end tool checks against a real native session (demo firmware).""" + + def setUp(self): + try: + import greeneye.sim_backend # noqa: F401 + except Exception as exc: + self.skipTest(f"native backend unavailable: {exc}") + self.session = ge.launch(kernel=DEMO_ELF, backend="native", record=None, name="native-mcp") + self.machine = self.session.target.machine + self.entry = self.machine.pc + self.addCleanup(self.session.close) + self.inp = io.StringIO() + self.out = io.StringIO() + self.log = io.StringIO() + + def make_server(self, **kw): + return mcp.GreenEyeMCPServer( + self.session, input_stream=self.inp, output_stream=self.out, log_stream=self.log, **kw + ) + + def call_tool(self, server, name, **arguments): + response = server.handle_message( + {"jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": {"name": name, "arguments": arguments}} + ) + self.assertIn("result", response, response) + result = response["result"] + return json.loads(result["content"][0]["text"]), result.get("isError", False) + + def test_native_step_budget(self): + server = self.make_server() + self.assertEqual(self.machine.insns, 0) + payload, is_error = self.call_tool(server, "step", count=3) + self.assertFalse(is_error, payload) + self.assertEqual(payload["steps"], 3) + self.assertEqual(self.machine.insns, 3) + self.assertNotEqual(self.machine.pc, self.entry) + self.assertEqual(payload["pc"], self.machine.pc) + + def test_native_step_budget_cap(self): + payload, is_error = self.call_tool(self.make_server(), "step", count=10001) + self.assertTrue(is_error) + self.assertEqual(payload["error"], "invalid_arguments") + + def test_native_run_budget_and_cycles(self): + server = self.make_server() + budget = 0.02 + before = self.machine.insns + payload, is_error = self.call_tool(server, "run", duration=budget) + self.assertFalse(is_error, payload) + self.assertGreaterEqual(payload["stops"], 1) + insns = self.machine.insns + self.assertGreater(insns, before) + self.assertLessEqual(insns, int(budget * 1_000_000) + 2000) + cycles, is_error = self.call_tool(server, "sim_cycles") + self.assertFalse(is_error, cycles) + self.assertEqual(cycles["backend"], "native") + self.assertEqual(cycles["insns"], insns) + self.assertGreater(cycles["cycles"], 0) + self.assertEqual(cycles["accounting_enabled"], True) + + def test_native_sim_regions(self): + payload, is_error = self.call_tool(self.make_server(), "sim_regions", limit=8) + self.assertFalse(is_error, payload) + self.assertEqual(payload["backend"], "native") + regions = {region["name"]: region for region in payload["regions"]} + self.assertEqual(regions["ram"]["base"], 0x80000000) + self.assertEqual(regions["ram"]["size"], 256 * 1024 * 1024) + self.assertEqual(regions["uart"]["base"], 0x10000000) + image_regions = [region for region in payload["regions"] if region["kind"] == "firmware"] + self.assertTrue(image_regions) + self.assertGreaterEqual(image_regions[0]["base"], 0x80000000) + self.assertTrue(image_regions[0]["flags"]["exec"]) + self.assertIsNone(payload["device_map_entries"]) + self.assertEqual(payload["observed_mmio_pages"], []) + + def test_native_session_info_reports_native(self): + payload, is_error = self.call_tool(self.make_server(), "session_info") + self.assertFalse(is_error) + self.assertEqual(payload["backend"], "native") + self.assertTrue(payload["capabilities"]["trace"]) + + class GatingTests(McpTestCase): def test_write_mem_refused_without_allow_write(self): server = self.make_server() @@ -426,6 +762,90 @@ class StandaloneEntryTests(unittest.TestCase): self.assertFalse(by_id[3]["result"].get("isError", False)) self.assertTrue(any(name.endswith("-mcp") for name in os.listdir(dumps))) + @unittest.skipUnless(os.path.isfile(MCP_SERVER), "mcp_server.py missing") + @unittest.skipUnless(os.path.isfile(DEMO_ELF), "demo firmware not built (make demo)") + def test_stdio_native_sim_tools_injection_and_trace(self): + try: + import greeneye.sim_backend # noqa: F401 + except Exception as exc: + self.skipTest(f"native backend unavailable: {exc}") + if not os.path.isfile(SERVICE_ELF): + build = subprocess.run( + ["make", "-C", os.path.join(TOOL_ROOT, "sdk")], capture_output=True, text=True + ) + if build.returncode != 0 or not os.path.isfile(SERVICE_ELF): + self.skipTest(f"SDK build failed: {build.stderr[-200:]}") + with tempfile.TemporaryDirectory(prefix="mcp-native-sim-") as dumps: + env = dict(os.environ, GREENEYE_DUMPS=dumps) + requests = [ + {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05"}}, + {"jsonrpc": "2.0", "method": "notifications/initialized"}, + {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "backends_list", "arguments": {}}}, + {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "sim_cycles", "arguments": {}}}, + {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "sim_profile", "arguments": {}}}, + {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "sim_device_state", "arguments": {"limit": 2}}}, + {"jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": {"name": "trace_start", "arguments": {"exec": False, "max_events": 10000}}}, + {"jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {"name": "trace_stop", "arguments": {}}}, + { + "jsonrpc": "2.0", + "id": 8, + "method": "tools/call", + "params": { + "name": "inject_service", + "arguments": {"service": SERVICE_ELF, "slot": 6, "cmd": 1, "arg0": 65, "path": "both"}, + }, + }, + {"jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": {"name": "service_list", "arguments": {}}}, + {"jsonrpc": "2.0", "id": 10, "method": "tools/call", "params": {"name": "close", "arguments": {}}}, + ] + payload = "".join(json.dumps(request) + "\n" for request in requests) + result = subprocess.run( + [sys.executable, MCP_SERVER, "--stdio", "--backend", "native", "--record", "--allow-inject"], + input=payload, + capture_output=True, + text=True, + cwd=TOOL_ROOT, + env=env, + timeout=300, + ) + self.assertEqual(result.returncode, 0, result.stderr[-2000:]) + responses = [json.loads(line) for line in result.stdout.splitlines() if line.strip()] + by_id = {response.get("id"): response for response in responses} + self.assertEqual(sorted(by_id), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + + def call_result(request_id): + response = by_id[request_id] + self.assertIn("result", response, response) + body = response["result"] + self.assertFalse(body.get("isError", False), body) + return json.loads(body["content"][0]["text"]) + + backends = call_result(2) + self.assertEqual(backends["active"], "native") + cycles = call_result(3) + self.assertEqual(cycles["backend"], "native") + self.assertIsNotNone(cycles["cycle_model"]) + profile = call_result(4) + self.assertEqual(profile["backend"], "native") + self.assertEqual(profile["kernel"], DEMO_ELF) + device_state = call_result(5) + self.assertIn("device_model", device_state) + trace_start = call_result(6) + self.assertTrue(trace_start["tracing"]) + trace_stop = call_result(7) + self.assertFalse(trace_stop["tracing"]) + self.assertTrue(str(trace_stop["blob"]).endswith("trace.jsonl")) + injected = call_result(8) + self.assertEqual(injected["remote"], 0x41 ^ 0xBEEF) + self.assertEqual(injected["firmware"], 0x41 ^ 0xBEEF) + services = call_result(9) + self.assertEqual(services["count"], 1) + self.assertEqual(services["services"][0]["entry_addr"], injected["service"]["entry_addr"]) + slots = services["slots"] + if slots: + self.assertEqual(slots[6]["entry"], injected["service"]["entry_addr"]) + self.assertFalse(by_id[10]["result"].get("isError", False)) + if __name__ == "__main__": unittest.main() diff --git a/debug/risc-v-gsp-rm-vm/tests/test_sim_cycles.py b/debug/risc-v-gsp-rm-vm/tests/test_sim_cycles.py new file mode 100644 index 0000000..9bb69ad --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/tests/test_sim_cycles.py @@ -0,0 +1,299 @@ +"""Cycle model (sim/cycles.py) and GSP cycle profile (sim/gsp_profile.py) tests. + +All assertions here are about a *modeled* cycle count and its determinism, not +about silicon timings. Everything is stdlib-only and offline: no hardware, +no GPU, no QEMU, no network. The real-image tests are bounded (200k +instructions), read-only, and skip cleanly when the 570.144 fwimage is absent. +""" +from __future__ import annotations + +import json +import os +import tempfile +import unittest + +from greeneye.device_map import QEMU_REFERENCE +from sim.cycles import ( + CLASS_ALU, + CLASS_DIV, + CLASS_LOAD, + CLASS_MUL, + CYCLE_FIDELITY, + DEFAULT_MODEL, + NOTE, + CycleModel, +) +from sim.decode import ( + A_ADD, + A_DIV, + A_MUL, + A_SUB, + K_AMO, + K_BRANCH, + K_CSR, + K_EBREAK, + K_ECALL, + K_ILLEGAL, + K_JAL, + K_JALR, + K_LOAD, + K_OP, + K_OP32, + decode32, +) +from sim.gsp_profile import DEFAULT_IMAGE, run_profile +from sim.machine import R_EBREAK, Machine + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOL_ROOT = os.path.normpath(os.path.join(HERE, "..")) +DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf") +REAL_IMAGE = os.path.normpath( + os.path.join( + HERE, "..", "..", "..", "firmware", "57X-GSP", "570.144", "gsp-570.144.fwimage.bin" + ) +) + +BASE = 0x80000000 +TOUCH_A = QEMU_REFERENCE["access_addr"] # 0x2000000000000A00 (570.144 first touch) + +# addi a0,a0,1 x3; ebreak +PROGRAM_ALU = bytes.fromhex("13051500" * 3 + "73001000") +# lui t0,0x80000; lw a0,0(t0); ebreak (one completed RAM load) +PROGRAM_RAM_LOAD = bytes.fromhex("b702008003a5020073001000") +# lui t0,0x20000; slli t0,t0,32; addi t0,t0,0x600; addi t0,t0,0x400; +# lw a0,0(t0); sw a0,0(t0); ebreak (one unknown-MMIO read + one write) +PROGRAM_MMIO = bytes.fromhex("b702002093920202938202609382024003a5020023a0a20073001000") + + +def decoded_cost(program: bytes, model: CycleModel, pc0: int = BASE) -> int: + """Instruction cost of a straight-line 4-byte-encoded program (no memory).""" + total = 0 + pc = pc0 + while pc < pc0 + len(program): + word = int.from_bytes(program[pc - pc0 : pc - pc0 + 4], "little") + kind, sub, _rd, _r1, _r2, _imm, _nxt = decode32(pc, word) + total += model.instruction_cost(kind, sub) + pc += 4 + return total + + +class CycleTableTests(unittest.TestCase): + def test_every_decoded_kind_has_a_positive_cost(self): + subs = {K_OP: range(18), K_OP32: range(18)} + for kind in range(18): + for sub in subs.get(kind, (0,)): + cost = DEFAULT_MODEL.instruction_cost(kind, sub) + self.assertIsInstance(cost, int) + self.assertGreaterEqual(cost, 1, f"kind={kind} sub={sub}") + + def test_alu_branch_and_jump_are_unit_cost(self): + model = DEFAULT_MODEL + self.assertEqual(model.instruction_cost(K_OP, A_ADD), model.alu) + self.assertEqual(model.instruction_cost(K_OP, A_SUB), model.alu) + self.assertEqual(model.instruction_cost(K_BRANCH), model.branch) + self.assertEqual(model.instruction_cost(K_JAL), model.jump) + self.assertEqual(model.instruction_cost(K_JALR), model.jump) + + def test_m_extension_is_conservative(self): + model = DEFAULT_MODEL + mul = model.instruction_cost(K_OP, A_MUL) + div = model.instruction_cost(K_OP, A_DIV) + self.assertGreater(mul, model.alu) + self.assertGreater(div, mul) + # Same table is used for the 32-bit M forms. + self.assertEqual(model.instruction_cost(K_OP32, A_MUL), mul) + self.assertEqual(model.instruction_cost(K_OP32, A_DIV), div) + + def test_memory_latency_is_a_parameter(self): + model = DEFAULT_MODEL + self.assertGreaterEqual(model.memory_cost(False), 1) + self.assertGreaterEqual(model.memory_cost(True), model.memory_cost(False)) + custom = CycleModel(memory_latency=9, mmio_latency=40) + self.assertEqual(custom.memory_cost(False), 9) + self.assertEqual(custom.memory_cost(True), 40) + self.assertEqual(custom.instruction_cost(K_LOAD), model.instruction_cost(K_LOAD)) + + def test_csr_fence_amo_and_trap_are_costs(self): + model = DEFAULT_MODEL + for kind in (K_CSR, K_AMO, K_EBREAK, K_ECALL, K_ILLEGAL): + self.assertGreaterEqual(model.instruction_cost(kind), 1) + + def test_classify_reports_classes(self): + self.assertEqual(CycleModel.classify(K_OP, A_ADD), CLASS_ALU) + self.assertEqual(CycleModel.classify(K_OP, A_MUL), CLASS_MUL) + self.assertEqual(CycleModel.classify(K_OP, A_DIV), CLASS_DIV) + self.assertEqual(CycleModel.classify(K_LOAD), CLASS_LOAD) + + def test_model_is_labelled_modeled_not_measured(self): + self.assertIn("modeled", NOTE.lower()) + self.assertIn("not measured", NOTE.lower()) + self.assertIn("L2", CYCLE_FIDELITY) + self.assertIn("modeled", DEFAULT_MODEL.name) + described = DEFAULT_MODEL.describe() + self.assertEqual(described["name"], DEFAULT_MODEL.name) + self.assertEqual(described["note"], NOTE) + self.assertIn("memory_latency", described) + + def test_invalid_parameters_rejected(self): + with self.assertRaises(ValueError): + CycleModel(alu=0) + with self.assertRaises(ValueError): + CycleModel(memory_latency=-1) + with self.assertRaises(ValueError): + CycleModel(div=True) + + +class MachineCycleTests(unittest.TestCase): + def _machine(self, program: bytes, model=None, **kwargs) -> Machine: + machine = Machine(cycle_model=model, **kwargs) + machine.load_segments([(BASE, program)], BASE) + return machine + + def test_counting_enabled_by_default(self): + machine = self._machine(PROGRAM_ALU) + machine.run(3) # three addi, budget stop + self.assertEqual(machine.insns, 3) + self.assertEqual(machine.cycles, 3) + + def test_ram_load_adds_the_modelled_memory_latency(self): + machine = self._machine(PROGRAM_RAM_LOAD) + reason, _pc, _a, _b = machine.run(100) + self.assertEqual(reason, R_EBREAK) + expected = decoded_cost(PROGRAM_RAM_LOAD, DEFAULT_MODEL) + DEFAULT_MODEL.memory_latency + self.assertEqual(machine.cycles, expected) + + def test_mmio_accesses_add_the_modelled_mmio_latency(self): + machine = self._machine(PROGRAM_MMIO, unknown_mmio="lenient") + reason, _pc, _a, _b = machine.run(100) + self.assertEqual(reason, R_EBREAK) + self.assertEqual(machine.mmio_unknown, 2) + self.assertEqual(machine.mmio_log[0]["addr"], TOUCH_A) + expected = decoded_cost(PROGRAM_MMIO, DEFAULT_MODEL) + 2 * DEFAULT_MODEL.mmio_latency + self.assertEqual(machine.cycles, expected) + + def test_variable_memory_latency_changes_only_the_counter(self): + slow = CycleModel(memory_latency=9) + quick = self._machine(PROGRAM_RAM_LOAD, model=DEFAULT_MODEL) + slower = self._machine(PROGRAM_RAM_LOAD, model=slow) + quick.run(100) + slower.run(100) + self.assertEqual(slower.insns, quick.insns) + self.assertEqual(slower.x, quick.x) + self.assertEqual(slower.cycles - quick.cycles, 9 - DEFAULT_MODEL.memory_latency) + + def test_disabled_model_leaves_the_counter_at_zero(self): + machine = self._machine(PROGRAM_ALU, model=False) + machine.run(3) + self.assertIsNone(machine.cycle_model) + self.assertEqual(machine.insns, 3) + self.assertEqual(machine.cycles, 0) + + def test_model_does_not_change_execution(self): + without = self._machine(PROGRAM_MMIO, model=False, unknown_mmio="lenient") + with_model = self._machine(PROGRAM_MMIO, unknown_mmio="lenient") + result_without = without.run(100) + result_with_model = with_model.run(100) + self.assertEqual(result_without, result_with_model) + self.assertEqual(without.insns, with_model.insns) + self.assertEqual(without.x, with_model.x) + self.assertEqual(without.mmio_log, with_model.mmio_log) + self.assertGreater(with_model.cycles, 0) + + def test_double_run_identical(self): + def profile(): + machine = self._machine(PROGRAM_MMIO, unknown_mmio="lenient") + result = machine.run(100) + return result, machine.insns, machine.cycles, machine.pc, tuple(machine.x) + + self.assertEqual(profile(), profile()) + + @unittest.skipUnless(os.path.isfile(DEMO_ELF), "demo firmware absent") + def test_demo_execution_identical_with_and_without_model(self): + from greeneye.elf import load_elf + + image = load_elf(DEMO_ELF) + plan = image.flat_load_plan(bias=0) + entry = image.relocated_entry(bias=0) + + def run(model): + machine = Machine(cycle_model=model) + machine.load_segments(plan, entry) + result = machine.run(20_000) + return result, machine.insns, tuple(machine.x), machine.uart.text() + + self.assertEqual(run(False), run(None)) + + +class GspProfileTests(unittest.TestCase): + def test_default_image_path_matches_workspace_firmware(self): + self.assertEqual( + os.path.normpath(DEFAULT_IMAGE), + os.path.normpath(REAL_IMAGE), + ) + + def test_absent_image_skips_cleanly(self): + with tempfile.TemporaryDirectory() as root: + missing = os.path.join(root, "no-such-image.bin") + result = run_profile(image=missing, instructions=1000) + self.assertFalse(result.ran) + self.assertIn("absent", result.reason) + self.assertIsNone(result.session_dir) + + def test_bad_budget_rejected(self): + from greeneye.errors import GreenEyeError + + with self.assertRaises(GreenEyeError): + run_profile(image=REAL_IMAGE, instructions=0) + + +@unittest.skipUnless(os.path.isfile(REAL_IMAGE), "TU104 570.144 fwimage absent") +class RealImageCycleProfileTests(unittest.TestCase): + def test_bounded_profile_first_touch_and_metadata(self): + with tempfile.TemporaryDirectory() as root: + result = run_profile( + image=REAL_IMAGE, instructions=200_000, record=root, name="unittest-cycle0" + ) + self.assertTrue(result.ran, result.reason) + self.assertEqual(result.instructions, 200_000) + self.assertEqual(result.stop_reason, "budget") + self.assertGreater(result.cycles, result.instructions) + self.assertEqual(result.cycle_model, DEFAULT_MODEL.name) + self.assertEqual(result.first_unknown_mmio["addr"], TOUCH_A) + self.assertEqual(result.first_unknown_mmio["rw"], "r") + self.assertEqual(result.first_unknown_mmio["pc"], 0x84009014) + self.assertEqual(result.stop_pc, 0x84009070) + self.assertEqual(result.stop_pc_fw, 0x4009070) + + with open(os.path.join(result.session_dir, "session.json"), "r", encoding="utf-8") as fh: + meta = json.load(fh) + self.assertEqual(meta["cycles"], result.cycles) + self.assertEqual(meta["instructions_executed"], 200_000) + self.assertEqual(meta["cycle_model"], DEFAULT_MODEL.name) + self.assertEqual(meta["firmware_sha256"], result.image_sha256) + self.assertIn("modeled", meta["cycle_fidelity"]) + self.assertEqual(meta["first_unknown_mmio"]["addr"], TOUCH_A) + self.assertEqual(meta["cycle_model_table"]["name"], DEFAULT_MODEL.name) + + def test_two_runs_identical_cycles(self): + with tempfile.TemporaryDirectory() as root: + first = run_profile( + image=REAL_IMAGE, + instructions=200_000, + record=os.path.join(root, "a"), + name="unittest-cycle-a", + ) + second = run_profile( + image=REAL_IMAGE, + instructions=200_000, + record=os.path.join(root, "b"), + name="unittest-cycle-b", + ) + self.assertTrue(first.ran and second.ran) + self.assertEqual(first.instructions, second.instructions) + self.assertEqual(first.cycles, second.cycles) + self.assertEqual(first.stop_pc, second.stop_pc) + self.assertEqual(first.image_sha256, second.image_sha256) + + +if __name__ == "__main__": + unittest.main() diff --git a/debug/risc-v-gsp-rm-vm/tests/test_sim_devices.py b/debug/risc-v-gsp-rm-vm/tests/test_sim_devices.py new file mode 100644 index 0000000..8063ee9 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/tests/test_sim_devices.py @@ -0,0 +1,458 @@ +"""Device-model tests: registry, per-block semantics, profile and boot ladder. + +Every modelled register assertion here is tied to the evidence documented in +``sim/devices/*.py`` and ``docs/36-GSP-DEVICE-MODEL.md``. The real-firmware +tests are bounded (<= 20k instructions) and assert *observed* rungs only. +""" +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from sim.devices import ( # noqa: E402 + CsbWindow, + DmaTranslator, + FalconBlock, + IrqRouter, + PmcBlock, + PmuStub, + PriscvBlock, + PtimerBlock, + Wpr2Block, + load_profile, + registry_from_profile, +) +from sim.devices.base import DeviceRegistry # noqa: E402 +from sim.devices import falcon as falcon_mod # noqa: E402 +from sim.machine import Machine # noqa: E402 + +TOOL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DEFAULT_IMAGE = os.path.normpath( + os.path.join(TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", + "gsp-570.144.fwimage.bin")) +DEFAULT_PROFILE = os.path.join(TOOL_ROOT, "sim", "profiles", "tu104-570.144.json") + + +class RegistryTests(unittest.TestCase): + def test_containment_and_unhandled_log(self): + reg = DeviceRegistry() + dev = PmcBlock(base=0x1000, size=0x1000) + reg.add(dev) + ok, value = reg.read(0x1A00, 4) + self.assertTrue(ok) + self.assertEqual(value, dev.boot_42) + # unmodelled offset inside the window: unhandled but window-matched + ok, _ = reg.read(0x1A04, 4) + self.assertFalse(ok) + self.assertEqual(reg.unhandled, 1) + # outside every window + ok, _ = reg.read(0x2000, 4) + self.assertFalse(ok) + self.assertEqual(reg.unhandled, 2) + # Only window-matched accesses are recorded; outside-window accesses + # are counted but left to the machine's unknown log. + self.assertEqual(len(reg.accesses), 2) + self.assertFalse(reg.accesses[1]["handled"]) + + def test_overlapping_windows_fall_back_on_refusal(self): + # A later window shadows an earlier one, but an offset the overlay does + # not model (here the PMU stub refuses everything) falls back to the + # wider device instead of being logged as unknown. + reg = DeviceRegistry() + fal = FalconBlock(base=0x1000, size=0x1000) + reg.add(fal) + reg.add(PmuStub(base=0x1000, size=0x1000)) + self.assertTrue(reg.write(0x1040, 4, 0xCA)) # MAILBOX0 via fallback + self.assertEqual(fal.regs[falcon_mod.MAILBOX0], 0xCA) + ok, value = reg.read(0x1040, 4) + self.assertTrue(ok) + self.assertEqual(value, 0xCA) + self.assertEqual(reg.unhandled, 0) + self.assertTrue(all(a["handled"] for a in reg.accesses)) + + def test_overlapping_windows_all_refuse_records_once(self): + reg = DeviceRegistry() + reg.add(PmuStub(base=0x1000, size=0x1000)) + reg.add(PmuStub(base=0x1000, size=0x1000)) + ok, _ = reg.read(0x1000, 4) + self.assertFalse(ok) + self.assertEqual(reg.unhandled, 1) + self.assertEqual(len(reg.accesses), 1) + self.assertFalse(reg.accesses[0]["handled"]) + self.assertFalse(reg.write(0x1000, 4, 1)) + self.assertEqual(reg.unhandled, 2) + self.assertEqual(len(reg.accesses), 2) + + def test_straddling_access_is_not_served(self): + reg = DeviceRegistry() + reg.add(PmcBlock(base=0x1000, size=0x10)) + ok, _ = reg.read(0x1008, 8) # ends at 0x1010, outside the window + self.assertFalse(ok) + + def test_state_round_trip(self): + reg = DeviceRegistry() + fal = FalconBlock(base=0x1000, size=0x1000) + reg.add(fal) + reg.write(0x1040, 4, 0xCA) + snap = reg.state() + reg2 = DeviceRegistry() + fal2 = FalconBlock(base=0x1000, size=0x1000) + reg2.add(fal2) + reg2.load_state(snap) + self.assertEqual(fal2.regs[falcon_mod.MAILBOX0], 0xCA) + self.assertEqual(fal2.state(), fal.state()) + + +class PmcTests(unittest.TestCase): + def test_tu104_value_fields(self): + dev = PmcBlock(chip_id=0x164) + value = dev.read(0xA00, 4) + self.assertEqual((value >> 20) & 0x1FF, 0x164) + self.assertEqual((value >> 24) & 0x1F, 0x16) # ARCHITECTURE TU100 + self.assertEqual((value >> 20) & 0xF, 0x4) # IMPLEMENTATION + self.assertEqual(dev.read(0xA04, 4), None) + self.assertFalse(dev.write(0xA00, 4, 0)) + + def test_profile_profile_value_override(self): + dev = PmcBlock(boot_42=0x00123456) + self.assertEqual(dev.read(0xA00, 4), 0x00123456) + + def test_chip_id_constants_are_distinct(self): + from sim.devices import pmc as pmc_mod + ids = [pmc_mod.TU102, pmc_mod.TU104, pmc_mod.TU105, pmc_mod.TU106, + pmc_mod.TU116, pmc_mod.TU117] + self.assertEqual(len(ids), len(set(ids))) + self.assertEqual(pmc_mod.TU117, 0x167) + self.assertFalse(hasattr(pmc_mod, "TU107")) + + +class FalconTests(unittest.TestCase): + def test_irq_set_clear_status(self): + dev = FalconBlock() + dev.write(falcon_mod.IRQSSET, 4, falcon_mod.IRQ_HALT) + self.assertEqual(dev.read(falcon_mod.IRQSTAT, 4), falcon_mod.IRQ_HALT) + dev.write(falcon_mod.IRQSCLR, 4, falcon_mod.IRQ_HALT) + self.assertEqual(dev.read(falcon_mod.IRQSTAT, 4), 0) + # write-only set/clear registers have no read-back + self.assertIsNone(dev.read(falcon_mod.IRQSSET, 4)) + + def test_mailbox_rw(self): + dev = FalconBlock() + dev.write(falcon_mod.MAILBOX0, 4, 0xCA) + dev.write(falcon_mod.MAILBOX1, 4, 0x11DEAD) + self.assertEqual(dev.read(falcon_mod.MAILBOX0, 4), 0xCA) + self.assertEqual(dev.read(falcon_mod.MAILBOX1, 4), 0x11DEAD) + + def test_debuginfo_storage_without_shim(self): + dev = FalconBlock(debuginfo_host_consume=False) + dev.write(falcon_mod.DEBUGINFO, 4, 0x11DEAD) + self.assertEqual(dev.read(falcon_mod.DEBUGINFO, 4), 0x11DEAD) + + def test_debuginfo_host_consume_shim(self): + dev = FalconBlock(debuginfo_host_consume=True) + dev.write(falcon_mod.DEBUGINFO, 4, 0x11DEAD) + value = dev.read(falcon_mod.DEBUGINFO, 4) + self.assertEqual(value, 0x11DEAD) # value preserved for the reader + self.assertEqual(dev.read(falcon_mod.DEBUGINFO, 4), 0x1DEAD) # stage cleared + self.assertEqual(dev.debuginfo_consumed, 1) + # debugger peek never consumes + dev.write(falcon_mod.DEBUGINFO, 4, 0x21DEAD) + self.assertEqual(dev.peek(falcon_mod.DEBUGINFO, 4), 0x21DEAD) + self.assertEqual(dev.debuginfo_consumed, 1) + + def test_dma_transfer_through_translator(self): + ram = bytearray(0x10000) + ram[0x100:0x108] = b"DEADBEEF" + dma = DmaTranslator(ram=ram, ram_base=0, ram_size=len(ram), + windows=[{"pa": 0x0, "sim": 0, "size": 0x10000}]) + dev = FalconBlock(dma=dma) + # Program: destination = 0x200, source = IMEM + 0x100. The register + # encodes ((base1 << 32) | base) << 8; the profile's DMA windows decide + # which aperture that address resolves to (aperture selection itself is + # not proven yet and is documented as an unknown). + dev.write(falcon_mod.DMATRFBASE1, 4, 0) + dev.write(falcon_mod.DMATRFBASE, 4, 0) + dev.write(falcon_mod.DMATRFFBOFFS, 4, 0x200) + dev.write(falcon_mod.DMATRFMOFFS, 4, 0x100) + cmd = (falcon_mod.CMD_IMEM | falcon_mod.CMD_WRITE + | (2 << falcon_mod.CMD_SIZE_SHIFT)) # IMEM -> FB, 16 bytes + dev.write(falcon_mod.DMATRFCMD, 4, cmd) + self.assertEqual(ram[0x200:0x210], b"DEADBEEF" + bytes(8)) + self.assertTrue(dev.regs[falcon_mod.DMATRFCMD] & falcon_mod.CMD_IDLE) + self.assertEqual(dev.dma_transfers, 1) + + def test_dma_refused_when_unmapped(self): + ram = bytearray(0x1000) + dma = DmaTranslator(ram=ram, ram_base=0, ram_size=len(ram), windows=[]) + dev = FalconBlock(dma=dma) + dev.write(falcon_mod.DMATRFBASE, 4, 0) + dev.write(falcon_mod.DMATRFCMD, 4, falcon_mod.CMD_IMEM) + self.assertFalse(dev.regs[falcon_mod.DMATRFCMD] & falcon_mod.CMD_IDLE) + self.assertTrue(dev.regs[falcon_mod.DMATRFCMD] & falcon_mod.CMD_ERROR) + self.assertEqual(dev.dma_refused, 1) + + +class PriscvTests(unittest.TestCase): + def test_irq_mask_semantics(self): + dev = PriscvBlock() + dev.write(0x2AC, 4, 0x5) + dev.write(0x2B0, 4, 0x1) + self.assertEqual(dev.read(0x2B4, 4), 0x4) + self.assertFalse(dev.write(0x2B4, 4, 0xFFFF)) # read-only + + def test_trace_and_error_registers(self): + dev = PriscvBlock() + dev.write(0x344, 4, 0x1234) + self.assertEqual(dev.read(0x344, 4), 0x1234) + # observed boot panic reads (R-I4R): read-back is zero, writes refused + self.assertEqual(dev.read(0x364, 4), 0) + self.assertEqual(dev.read(0x368, 4), 0) + self.assertFalse(dev.write(0x368, 4, 0x1)) + + +class TimerTests(unittest.TestCase): + def test_modeled_time_reads(self): + clock = {"insns": 0} + dev = PtimerBlock(ns_per_insn=2.0, clock=lambda: clock["insns"]) + self.assertEqual(dev.read(0x400, 4), 0) + self.assertEqual(dev.read(0x410, 4), 0) + clock["insns"] = 16 + # 32 ns -> TIME_0 NSEC field (bits 31:5) = 1 + self.assertEqual(dev.read(0x400, 4), 1) + self.assertEqual(dev.read(0x410, 4), 32) + + def test_profile_window_reaches_time_registers(self): + # Regression: the profile window is PRIV+0x9000, so TIME_0/TIME_1 must + # be block-relative (0x400/0x410); aperture-absolute 0x9400/0x9410 fall + # outside the declared window and serve nothing. + profile = { + "schema": "greeneye-sim-profile/1", + "devices": { + "timer": { + "ns_per_insn": 1.0, + "windows": [{"base": "0x9000", "size": "0x1000"}], + } + }, + } + machine = Machine(memory_mb=1, ram_base=0) + reg = registry_from_profile(profile, machine=machine) + machine.insns = 32 + ok, value = reg.read(0x9000 + 0x400, 4, pc=0) + self.assertTrue(ok) + self.assertEqual(value, 1) # (32 ns >> 5) + ok, value = reg.read(0x9000 + 0x410, 4, pc=0) + self.assertTrue(ok) + self.assertEqual(value, 32) + + def test_timer_registration_is_opt_in(self): + machine = Machine(memory_mb=1, ram_base=0) + no_section = { + "schema": "greeneye-sim-profile/1", + "devices": {"pmc": {"windows": [{"base": "0x1000", "size": "0x1000"}]}}, + } + reg = registry_from_profile(no_section, machine=machine) + self.assertIsNone(reg.device_by_name("timer")) + disabled = { + "schema": "greeneye-sim-profile/1", + "devices": {"timer": {"enabled": False, + "windows": [{"base": "0x9000", "size": "0x1000"}]}}, + } + reg = registry_from_profile(disabled, machine=machine) + self.assertIsNone(reg.device_by_name("timer")) + enabled = { + "schema": "greeneye-sim-profile/1", + "devices": {"timer": {"ns_per_insn": 1.0, + "windows": [{"base": "0x9000", "size": "0x1000"}]}}, + } + reg = registry_from_profile(enabled, machine=machine) + self.assertIsNotNone(reg.device_by_name("timer")) + + +class CsbTests(unittest.TestCase): + def test_undeclared_window_serves_nothing(self): + dev = CsbWindow(base=0x1000, size=0x1000) + self.assertIsNone(dev.read(0x0, 4)) + self.assertFalse(dev.write(0x0, 4, 1)) + + def test_declared_semantics(self): + dev = CsbWindow(base=0x1000, size=0x1000, registers={ + 0x00: {"width": 4, "value": 0xF, "semantics": "w1c"}, + 0x04: {"width": 4, "value": 0, "semantics": "rw"}, + 0x08: {"width": 4, "value": 0, "semantics": "ro"}, + }) + dev.write(0x00, 4, 0x3) + self.assertEqual(dev.read(0x00, 4), 0xC) + dev.write(0x04, 4, 0x77) + self.assertEqual(dev.read(0x04, 4), 0x77) + self.assertFalse(dev.write(0x08, 4, 1)) + + +class Wpr2Tests(unittest.TestCase): + def test_range_encoding(self): + dev = Wpr2Block() + dev.set_range(0x2000000, 0x6000000) + lo = dev.read(0x1FA824, 4) + hi = dev.read(0x1FA828, 4) + self.assertEqual((lo << 12), 0x2000000) + self.assertEqual((hi << 12) + 0x20000, 0x6000000) + self.assertIsNone(dev.read(0x1FA800, 4)) + + def test_state_derives_range_after_writes(self): + # Regression: writes update regs only; state() must derive start/end + # from the registers rather than reporting a stale set_range range. + dev = Wpr2Block() + dev.write(0x1FA824, 4, 0x2000000 >> 12) + dev.write(0x1FA828, 4, (0x6000000 - 0x20000) >> 12) + state = dev.state() + self.assertEqual(state["start"], 0x2000000) + self.assertEqual(state["end"], 0x6000000) + restored = Wpr2Block() + restored.load_state(state) + self.assertEqual(restored.start, 0x2000000) + self.assertEqual(restored.end, 0x6000000) + # A stale serialised start/end never wins over the registers. + stale = Wpr2Block() + stale.load_state({"regs": {str(0x1FA824): 0x2000}, + "start": 0xDEAD, "end": 0xBEEF}) + self.assertEqual(stale.start, 0x2000000) + self.assertIsNone(stale.end) + + +class PmuTests(unittest.TestCase): + def test_stub_never_fakes(self): + dev = PmuStub(base=0x1000, size=0x1000) + self.assertIsNone(dev.read(0x0, 4)) + self.assertFalse(dev.write(0x0, 4, 1)) + self.assertEqual(dev.seen["r:0x0:4"], 1) + + +class IrqRouterTests(unittest.TestCase): + def test_aggregation(self): + fal = FalconBlock() + pri = PriscvBlock() + router = IrqRouter(falcon=fal, priscv=pri) + fal.write(falcon_mod.IRQSSET, 4, falcon_mod.IRQ_HALT) + pri.write(0x2AC, 4, 0x2) + state = router.pending() + self.assertTrue(state["falcon_halt"]) + self.assertEqual(state["riscv_irq_mask"], 0x2) + + +class DmaTranslatorTests(unittest.TestCase): + def test_translate_and_refuse(self): + ram = bytearray(0x100) + dma = DmaTranslator(ram=ram, ram_base=0, ram_size=len(ram), + windows=[{"pa": 0x1000, "sim": 0, "size": 0x100}]) + self.assertEqual(dma.translate(0x1000), 0) + self.assertEqual(dma.translate(0x10FF), 0xFF) + self.assertIsNone(dma.translate(0x2000)) + self.assertTrue(dma.write(0x1000, b"AB")) + self.assertEqual(dma.read(0x1000, 2), b"AB") + self.assertIsNone(dma.read(0x2000, 2)) + self.assertEqual(dma.refused, 1) + + def test_copy_counts_only_completed_transfers_and_exports_events(self): + ram = bytearray(0x100) + ram[0:4] = b"ABCD" + dma = DmaTranslator(ram=ram, ram_base=0, ram_size=len(ram), + windows=[{"pa": 0x1000, "sim": 0, "size": 0x100}]) + # Destination outside every window: refused, not a transfer. + self.assertFalse(dma.copy(0x1000, 0x2000, 4)) + self.assertEqual(dma.transfers, 0) + self.assertEqual(dma.refused, 1) + # Both mapped: one completed transfer. + self.assertTrue(dma.copy(0x1000, 0x1004, 4)) + self.assertEqual(dma.transfers, 1) + self.assertEqual(ram[0x4:0x8], b"ABCD") + state = dma.state() + self.assertIn("events", state) + self.assertEqual(state["events"][0]["kind"], "w") + self.assertEqual(len(state["events"]), 1) + + +class MachineDeviceIntegrationTests(unittest.TestCase): + def _machine(self): + reg = DeviceRegistry() + reg.add(PmcBlock(base=0x2000000000000000, size=0x1000)) + m = Machine(memory_mb=1, ram_base=0, unknown_mmio="lenient", devices=reg) + return m, reg + + def test_modeled_device_takes_precedence_over_lenient(self): + m, reg = self._machine() + self.assertEqual(m._mmio_load(0x2000000000000A00, 4), PmcBlock().boot_42) + self.assertEqual(m.mmio_unknown, 0) + self.assertEqual(len(reg.accesses), 1) + + def test_unknown_inside_window_is_logged(self): + m, reg = self._machine() + self.assertEqual(m._mmio_load(0x2000000000000A04, 4), 0) + self.assertEqual(m.mmio_unknown, 1) + self.assertFalse(reg.accesses[0]["handled"]) + + def test_debugger_peek_and_device_state(self): + m, reg = self._machine() + self.assertEqual(m.read_mem(0x2000000000000A00, 4), + PmcBlock().boot_42.to_bytes(4, "little")) + self.assertIsInstance(m.device_state(), dict) + self.assertEqual(m.device_state()["schema"], "greeneye-sim-devices/1") + + def test_reset_regs_names(self): + m = Machine(memory_mb=1, ram_base=0, reset_regs={"a0": 0x1234, "sp": 0x8000}) + self.assertEqual(m.x[10], 0x1234) + self.assertEqual(m.x[2], 0x8000) + + +@unittest.skipUnless(os.path.isfile(DEFAULT_IMAGE), "real GSP firmware absent") +class BootLadderTests(unittest.TestCase): + """Bounded real-firmware runs: observed rungs only.""" + + def test_profile_loads_and_builds_windows(self): + profile = load_profile(DEFAULT_PROFILE) + self.assertEqual(profile["schema"], "greeneye-sim-profile/1") + m = Machine(memory_mb=16, ram_base=0, unknown_mmio="lenient") + reg = registry_from_profile(profile, machine=m) + names = {d.name for d in reg.devices} + self.assertIn("pmc", names) + self.assertIn("falcon", names) + self.assertIn("priscv", names) + self.assertIsNotNone(reg.dma) + + def test_no_shim_b1_device_model_serves_boot(self): + from sim.gsp_boot import run_boot + r = run_boot(image=DEFAULT_IMAGE, profile_path=DEFAULT_PROFILE, + instructions=20_000, record=False, use_shim=False) + self.assertTrue(r.ran) + self.assertEqual(r.rung, "B1") + self.assertIn("bootargs_consumer", r.milestones) + self.assertEqual(r.mmio_unknown, 0) + self.assertEqual(r.device_unhandled, 0) + + def test_shim_reaches_relocation_b2(self): + from sim.gsp_boot import run_boot + r = run_boot(image=DEFAULT_IMAGE, profile_path=DEFAULT_PROFILE, + instructions=20_000, record=False, use_shim=True) + self.assertTrue(r.ran) + self.assertEqual(r.rung, "B2") + self.assertIn("relocation_copy", r.milestones) + self.assertEqual(r.mmio_unknown, 0) + # Known unknown: the DMEM runspace content the app relocates from is + # not reconstructed; the run stops there rather than faking it. + self.assertEqual(r.stop_reason, "fault") + self.assertEqual(r.stop_pc, 0x400EA0C) + + def test_two_runs_identical(self): + from sim.gsp_boot import run_boot + kwargs = dict(image=DEFAULT_IMAGE, profile_path=DEFAULT_PROFILE, + instructions=20_000, record=False, use_shim=True) + a = run_boot(**kwargs) + b = run_boot(**kwargs) + self.assertEqual(a.instructions, b.instructions) + self.assertEqual(a.stop_pc, b.stop_pc) + self.assertEqual(a.stop_reason, b.stop_reason) + self.assertEqual(a.device_state_sha256, b.device_state_sha256) + self.assertEqual(a.registers, b.registers) + + +if __name__ == "__main__": + unittest.main() diff --git a/debug/risc-v-gsp-rm-vm/tests/test_sim_qemu_line.py b/debug/risc-v-gsp-rm-vm/tests/test_sim_qemu_line.py new file mode 100644 index 0000000..5a798f2 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/tests/test_sim_qemu_line.py @@ -0,0 +1,323 @@ +"""Tests for the S4 QEMU-line evaluation harness (harness/sim_qemu_line.py). + +The pure-function tests need no firmware. The integration tests use the real +TU104 570.144 fwimage read-only and **skip cleanly** when it (or +qemu-system-riscv64, or the bundled demo firmware) is absent. All runs are +bounded (<= 20k native instructions in tests, <= 1 s QEMU) and touch no +hardware, GPU, module, guest or network. + +Run:: + + python3 -m unittest tests.test_sim_qemu_line -v +""" +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +import unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOL_ROOT = os.path.abspath(os.path.join(HERE, "..")) +REAL_IMAGE = os.path.normpath( + os.path.join(TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", "gsp-570.144.fwimage.bin") +) +DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf") +if TOOL_ROOT not in sys.path: + sys.path.insert(0, TOOL_ROOT) + +from greeneye.device_map import QEMU_REFERENCE, firmware_sha256 # noqa: E402 +from harness.common import qemu_available # noqa: E402 +from harness.sim_qemu_line import FROZEN_THRESHOLDS # noqa: E402 + + +class PureUnitTests(unittest.TestCase): + """No firmware required: rung maths, thresholds and report rendering.""" + + def test_rung_ordinal_and_above(self): + from harness.sim_qemu_line import above_qemu_line, rung_ordinal + + self.assertEqual(rung_ordinal("pre-B0"), 0) + self.assertEqual(rung_ordinal("B0"), 1) + self.assertIsNone(rung_ordinal("B9")) + self.assertTrue(above_qemu_line("pre-B0", "B0")) + self.assertFalse(above_qemu_line("pre-B0", "pre-B0")) + self.assertFalse(above_qemu_line("B1", "B0")) + self.assertFalse(above_qemu_line(None, "B0")) + + def test_threshold_checks_require_device_model(self): + from harness.sim_qemu_line import threshold_checks + + qemu = {"ran": True, "first_touch_match": True, "rung": "pre-B0"} + native = { + "ran": True, + "rung": "B0", + "repeat": 2, + "identical": True, + "pair_wall_s": 1.0, + "device_model_used": False, + } + checks = threshold_checks({"qemu": qemu, "native": native}) + self.assertTrue(checks["qemu_first_fault_reproduced"]) + self.assertTrue(checks["native_above_qemu"]) + self.assertTrue(checks["identical_runs"]) + self.assertFalse(checks["device_model_documented"]) + self.assertFalse(checks["all"]) + native["device_model_used"] = True + self.assertTrue(threshold_checks({"qemu": qemu, "native": native})["all"]) + + def test_build_report_verdicts(self): + from harness.sim_qemu_line import build_report + + qemu = {"ran": True, "first_touch_match": True, "rung": "pre-B0"} + native_ok = { + "ran": True, + "rung": "B1", + "repeat": 2, + "identical": True, + "pair_wall_s": 1.0, + "device_model_used": True, + "runs": [], + } + report = build_report( + image=os.path.join(TOOL_ROOT, "does-not-exist.bin"), + qemu=qemu, + native=native_ok, + features=None, + s1={"source": None, "declared_rung": None, "notes": []}, + thresholds=dict(FROZEN_THRESHOLDS), + ) + self.assertEqual(report["verdict"], "above") + self.assertTrue(report["above_qemu_line"]) + self.assertTrue(report["threshold_checks"]["all"]) + + lenient = dict(native_ok, device_model_used=False) + report = build_report( + image="missing.bin", + qemu=qemu, + native=lenient, + features=None, + s1={"source": None, "declared_rung": None, "notes": []}, + thresholds=dict(FROZEN_THRESHOLDS), + ) + self.assertEqual(report["verdict"], "above_lenient_only") + self.assertFalse(report["above_qemu_line"]) + + report = build_report( + image="missing.bin", + qemu={"ran": False, "rung": None}, + native=native_ok, + features=None, + s1={"source": None, "declared_rung": None, "notes": []}, + thresholds=dict(FROZEN_THRESHOLDS), + ) + self.assertEqual(report["verdict"], "unverified_qemu_absent") + + report = build_report( + image="missing.bin", + qemu={"ran": True, "first_touch_match": False, "rung": "pre-B0"}, + native=native_ok, + features=None, + s1={"source": None, "declared_rung": None, "notes": []}, + thresholds=dict(FROZEN_THRESHOLDS), + ) + self.assertEqual(report["verdict"], "unverified_qemu_mismatch") + + def test_render_markdown_shape(self): + from harness.sim_qemu_line import build_report, render_markdown + + report = build_report( + image="missing.bin", + qemu={ + "ran": True, + "first_touch_match": True, + "rung": "pre-B0", + "wall_s": 1.0, + "first_fault": {"desc": "fault_load", "epc_fw": 0x4009014, + "access_addr": QEMU_REFERENCE["access_addr"], + "size": 4, "rw": "read"}, + "session_hashes": {"session.json": "ab" * 32}, + "session": "harness/qemu_line/sessions/qemu-probe", + }, + native={ + "ran": True, + "rung": "B0", + "rung_basis": "test", + "repeat": 2, + "identical": True, + "pair_wall_s": 1.0, + "device_model_used": False, + "runs": [ + { + "rung": "B0", + "instructions": 10, + "cycles": 12, + "wall_s": 0.1, + "stop": {"reason": "budget", "pc_fw": 0x4009070}, + "session": "harness/qemu_line/sessions/native-1", + "session_hashes": {"session.json": "cd" * 32}, + } + ], + "compare": {"ran": True, "headers": []}, + }, + features={"features": {"mcp": {"status": "not_run", "reason": "S3"}}}, + s1={"source": None, "declared_rung": None, "notes": []}, + thresholds=dict(FROZEN_THRESHOLDS), + ) + text = render_markdown(report) + self.assertIn("Rung table", text) + self.assertIn("Session hashes", text) + self.assertIn("Frozen thresholds", text) + self.assertIn("above_lenient_only", text) + self.assertIn("native run 1", text) + + +@unittest.skipUnless(os.path.isfile(REAL_IMAGE), "TU104 570.144 fwimage absent") +class NativeIntegrationTests(unittest.TestCase): + """Native-only run on the real image: shape + two-run determinism.""" + + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="sim-qemu-line-test-") + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_profile_discovery_when_s1_present(self): + from harness.sim_qemu_line import discover_s1_device_model + + profile = os.path.join(TOOL_ROOT, "sim", "profiles", "tu104-570.144.json") + if not os.path.isfile(profile): + self.skipTest("S1 profile absent") + s1 = discover_s1_device_model(REAL_IMAGE, firmware_sha256(REAL_IMAGE)) + self.assertEqual(os.path.abspath(s1.get("profile") or ""), profile) + self.assertTrue(any("greeneye-sim-profile" in note for note in s1["notes"])) + + def test_native_report_shape_and_determinism(self): + from harness.sim_qemu_line import run_harness + + report = run_harness( + image=REAL_IMAGE, + out_dir=self.tmp, + instructions=20_000, + repeat=2, + features=False, + run_qemu=False, + ) + self.assertEqual(report["schema"], "greeneye-sim-qemu-line/1") + self.assertTrue(report["firmware"]["sha256"]) + native = report["native"] + self.assertTrue(native["ran"]) + self.assertEqual(native["repeat"], 2) + self.assertTrue(native["identical"]) + runs = native["runs"] + self.assertEqual(len(runs), 2) + keys = ( + "instructions", "cycles", "stop", "first_unknown_mmio", "mmio_unknown", + "rung", "device_state_sha256", "milestones", + ) + self.assertEqual( + [{k: r.get(k) for k in keys} for r in runs], + [{k: runs[0].get(k) for k in keys}] * 2, + ) + for run in runs: + self.assertGreater(run["instructions"], 0) + self.assertLessEqual(run["instructions"], 20_000) + self.assertGreater(run["cycles"], 0) + self.assertTrue(run["session_hashes"]["session.json"]) + if native.get("device_model_used"): + # S1 semantic path: device model + recorded shim, B2 observed. + self.assertEqual(native["rung"], "B2") + self.assertIsNone(runs[0]["first_unknown_mmio"]) + self.assertEqual(runs[0]["mmio_unknown"], 0) + self.assertEqual(runs[0]["milestones"], ["bootargs_consumer", "relocation_copy"]) + self.assertTrue(runs[0]["device_state_sha256"]) + else: + # Lenient path (no S1 in the tree): B0 observation baseline. + self.assertEqual(native["rung"], "B0") + self.assertEqual(runs[0]["first_unknown_mmio"]["addr"], QEMU_REFERENCE["access_addr"]) + self.assertEqual(report["verdict"], "unverified_qemu_absent") + for name in ("sim-qemu-line.json", "sim-qemu-line.md"): + path = os.path.join(self.tmp, name) + self.assertTrue(os.path.isfile(path), path) + with open(os.path.join(self.tmp, "sim-qemu-line.json"), encoding="utf-8") as fh: + payload = json.load(fh) + self.assertEqual(payload["schema"], report["schema"]) + with open(os.path.join(self.tmp, "sim-qemu-line.md"), encoding="utf-8") as fh: + md = fh.read() + self.assertIn("| native run 1 |", md) + self.assertIn("Session hashes", md) + + def test_native_lenient_path_still_available(self): + from harness.sim_qemu_line import run_native_baseline + + native = run_native_baseline( + image=REAL_IMAGE, + out_dir=self.tmp, + instructions=20_000, + repeat=1, + profile=None, + device_map=None, + ) + self.assertTrue(native["ran"]) + self.assertFalse(native["device_model_used"]) + self.assertEqual(native["rung"], "B0") + self.assertEqual( + native["runs"][0]["first_unknown_mmio"]["addr"], QEMU_REFERENCE["access_addr"] + ) + + +@unittest.skipUnless( + os.path.isfile(REAL_IMAGE) and qemu_available(), + "TU104 570.144 fwimage or qemu-system-riscv64 absent", +) +class QemuIntegrationTests(unittest.TestCase): + """QEMU probe on the real image must reproduce its documented first fault.""" + + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="sim-qemu-line-test-") + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_qemu_first_fault_reproduced(self): + from harness.sim_qemu_line import run_qemu_baseline + + result = run_qemu_baseline(image=REAL_IMAGE, out_dir=self.tmp, duration=0.5) + self.assertTrue(result["ran"], result.get("reason")) + self.assertEqual(result["rung"], "pre-B0") + self.assertTrue(result["first_touch_match"], "documented first fault not reproduced") + first = result["first_fault"] + self.assertEqual(first["epc_fw"], QEMU_REFERENCE["firmware_vaddr"]) + self.assertEqual(first["access_addr"], QEMU_REFERENCE["access_addr"]) + self.assertTrue(result["session_hashes"]["session.json"]) + self.assertTrue(os.path.isfile(os.path.join(self.tmp, "sessions", "qemu-probe", "session.json"))) + + +@unittest.skipUnless(os.path.isfile(DEMO_ELF), "demo firmware absent (run make demo)") +class FeatureMatrixTests(unittest.TestCase): + """The native feature matrix runs on the bundled demo firmware.""" + + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="sim-qemu-line-test-") + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_feature_matrix_shape(self): + from harness.sim_qemu_line import run_feature_matrix + + result = run_feature_matrix(self.tmp) + self.assertTrue(result["ran"], result.get("reason")) + features = result["features"] + for name in ("hooks", "breakpoints", "watch", "memory", "struct", "sessions", "trace"): + self.assertIn(name, features) + self.assertEqual(features[name]["status"], "pass", features[name]) + self.assertEqual(features["mcp"]["status"], "not_run") + self.assertIn(features["injection"]["status"], ("pass", "not_run")) + self.assertIn(features["analyze_compare"]["status"], ("pass", "not_run")) + + +if __name__ == "__main__": + unittest.main() diff --git a/debug/risc-v-gsp-rm-vm/tests/test_triage.py b/debug/risc-v-gsp-rm-vm/tests/test_triage.py new file mode 100644 index 0000000..35ac128 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/tests/test_triage.py @@ -0,0 +1,264 @@ +"""Tests for the new-version triage workflow (harness/triage.py, docs/40). + +The orchestration tests use injected fake step runners: no QEMU, no native +boot, no firmware. The integration test uses the real TU104 570.144 fwimage +read-only with a tiny budget and skips cleanly when it (or +qemu-system-riscv64) is absent. Everything is bounded and touches no +hardware, GPU, module, guest or network. + +Run:: + + python3 -m unittest tests.test_triage -v +""" +from __future__ import annotations + +import contextlib +import io +import json +import os +import sys +import tempfile +import unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOL_ROOT = os.path.abspath(os.path.join(HERE, "..")) +REAL_IMAGE = os.path.normpath( + os.path.join(TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", "gsp-570.144.fwimage.bin") +) +if TOOL_ROOT not in sys.path: + sys.path.insert(0, TOOL_ROOT) + +from harness.common import qemu_available # noqa: E402 +from harness.triage import ( # noqa: E402 + DEFAULT_RUNNERS, + FORMAT, + PLAN_FORMAT, + TriageOptions, + build_plan, + extract_version, + main, + run_triage, +) + + +def _run_main(argv: list[str]) -> tuple[int, str, str]: + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + rc = main(argv) + return rc, out.getvalue(), err.getvalue() + + +def _fake_runners(fail: str | None = None, session_dir: str | None = None): + """Deterministic step runners; optionally one raises.""" + calls: list[str] = [] + + def make(name: str, **detail): + def fn(options, out): + calls.append(name) + payload = {"status": "ok"} + payload.update(detail) + if session_dir and name in ("probe", "native_boot"): + os.makedirs(session_dir, exist_ok=True) + with open(os.path.join(session_dir, "session.json"), "w", encoding="utf-8") as fh: + fh.write("{}\n") + payload["sessions"] = [{"name": name, "dir": session_dir}] + payload["session_hashes"] = {"session.json": "deadbeef"} + return payload + + return fn + + def boom(options, out): + calls.append(fail) + raise RuntimeError("injected failure") + + runners = [ + ("identity", boom if fail == "identity" else make("identity")), + ("probe", boom if fail == "probe" else make("probe", rung="pre-B0")), + ("native_boot", boom if fail == "native_boot" else make("native_boot", rung="B2")), + ("ab_eval", boom if fail == "ab_eval" else make("ab_eval", verdict="above_lenient_only", above_qemu_line=False)), + ("version_diff", boom if fail == "version_diff" else make("version_diff", sections={"image": "ok"})), + ("features", boom if fail == "features" else make("features", unavailable=[])), + ] + return runners, calls + + +class VersionExtractionTests(unittest.TestCase): + def test_explicit_label_wins(self): + version, source = extract_version("/does/not/matter.bin", "999.1") + self.assertEqual((version, source), ("999.1", "explicit --version")) + + def test_sidecar_convention(self): + with tempfile.TemporaryDirectory() as tmp: + fw = os.path.join(tmp, "gsp-x.fwimage.bin") + open(fw, "wb").close() + with open(os.path.join(tmp, "gsp_version.txt"), "w", encoding="utf-8") as fh: + fh.write("570.211W\n") + version, source = extract_version(fw) + self.assertEqual(version, "570.211W") + self.assertEqual(source, "gsp_version.txt sidecar") + + def test_parent_directory_fallback(self): + with tempfile.TemporaryDirectory() as tmp: + parent = os.path.join(tmp, "580.10") + os.makedirs(parent) + fw = os.path.join(parent, "gsp-580.10.fwimage.bin") + open(fw, "wb").close() + self.assertEqual(extract_version(fw)[0], "580.10") + + def test_stem_fallback(self): + with tempfile.TemporaryDirectory() as tmp: + fw = os.path.join(tmp, "mystery-image.bin") + open(fw, "wb").close() + version, source = extract_version(fw) + self.assertEqual(version, "mystery-image") + self.assertEqual(source, "file stem") + + +class OrchestrationTests(unittest.TestCase): + def _options(self, tmp: str, **overrides) -> TriageOptions: + fw = os.path.join(tmp, "new.fwimage.bin") + if not os.path.exists(fw): + with open(fw, "wb") as fh: + fh.write(b"\x00" * 16) + values = dict(firmware=fw, out=os.path.join(tmp, "triage-out"), budget=1000) + values.update(overrides) + return TriageOptions(**values) + + def test_report_shape_and_artifacts(self): + with tempfile.TemporaryDirectory() as tmp: + session_dir = os.path.join(tmp, "session") + runners, calls = _fake_runners(session_dir=session_dir) + report = run_triage(self._options(tmp), runners=runners) + self.assertEqual(report["format"], FORMAT) + self.assertEqual([s["name"] for s in report["steps"]], [n for n, _ in DEFAULT_RUNNERS]) + self.assertTrue(all(s["status"] == "ok" for s in report["steps"])) + self.assertEqual(report["errors"], []) + self.assertTrue(report["sessions"]) + self.assertEqual(report["sessions"][0]["hashes"]["session.json"], "deadbeef") + self.assertEqual(report["support_readiness"]["verdict"], "sim-observable") + self.assertFalse(report["support_readiness"]["baseline_compared"]) + self.assertEqual(report["threshold_checks"]["required_steps_missing"], []) + self.assertTrue(report["threshold_checks"]["beats_manual_baseline"]) + for key in ("report_json", "report_md"): + self.assertTrue(os.path.isfile(report["artifacts"][key]), key) + with open(report["artifacts"]["report_json"], "r", encoding="utf-8") as fh: + loaded = json.load(fh) + self.assertEqual(loaded["format"], FORMAT) + self.assertEqual(len(loaded["steps"]), len(DEFAULT_RUNNERS)) + with open(report["artifacts"]["report_md"], "r", encoding="utf-8") as fh: + md = fh.read() + self.assertIn("New-version triage", md) + self.assertIn("Support readiness", md) + + def test_partial_failure_does_not_crash(self): + with tempfile.TemporaryDirectory() as tmp: + runners, _calls = _fake_runners(fail="probe") + report = run_triage(self._options(tmp), runners=runners) + self.assertEqual(report["errors"], ["probe"]) + step = next(s for s in report["steps"] if s["name"] == "probe") + self.assertEqual(step["status"], "error") + self.assertIn("injected failure", step["reason"]) + self.assertEqual( + next(s for s in report["steps"] if s["name"] == "native_boot")["status"], "ok" + ) + self.assertTrue(any("QEMU probe failed" in b for b in report["support_readiness"]["blockers"])) + self.assertTrue(os.path.isfile(report["artifacts"]["report_json"])) + + def test_missing_firmware_is_reported_not_raised(self): + with tempfile.TemporaryDirectory() as tmp: + out = os.path.join(tmp, "out") + report = run_triage(TriageOptions(firmware=os.path.join(tmp, "nope.bin"), out=out)) + step = report["steps"][0] + self.assertEqual(step["name"], "identity") + self.assertEqual(step["status"], "error") + self.assertEqual(report["support_readiness"]["verdict"], "blocked") + self.assertTrue(os.path.isfile(report["artifacts"]["report_json"])) + + def test_budget_bounds(self): + with tempfile.TemporaryDirectory() as tmp: + options = self._options(tmp, budget=0) + with self.assertRaises(ValueError): + run_triage(options) + with self.assertRaises(ValueError): + run_triage(self._options(tmp, budget=3_000_000)) + + def test_plan_shape(self): + with tempfile.TemporaryDirectory() as tmp: + plan = build_plan(self._options(tmp)) + self.assertEqual(plan["format"], PLAN_FORMAT) + self.assertEqual([s["name"] for s in plan["steps"]], [n for n, _ in DEFAULT_RUNNERS]) + self.assertEqual(plan["side_effects"]["hardware"], "none") + + +class CliTests(unittest.TestCase): + def test_rm610_refusal(self): + with tempfile.TemporaryDirectory() as tmp: + fw = os.path.join(tmp, "gsp-rm610.fwimage.bin") + open(fw, "wb").close() + out = os.path.join(tmp, "out") + rc, _stdout, stderr = _run_main( + ["--firmware", fw, "--version", "RM-610", "--out", out] + ) + self.assertEqual(rc, 3) + self.assertIn("RM610", stderr) + self.assertFalse(os.path.exists(out)) + + def test_dry_run_has_no_side_effects(self): + with tempfile.TemporaryDirectory() as tmp: + fw = os.path.join(tmp, "new.fwimage.bin") + open(fw, "wb").close() + out = os.path.join(tmp, "out") + rc, stdout, _stderr = _run_main(["--firmware", fw, "--out", out, "--dry-run"]) + self.assertEqual(rc, 0) + plan = json.loads(stdout[stdout.index("{"):stdout.rindex("}") + 1]) + self.assertEqual(plan["format"], PLAN_FORMAT) + self.assertFalse(os.path.exists(out)) + + def test_missing_firmware_exits_2(self): + with tempfile.TemporaryDirectory() as tmp: + out = os.path.join(tmp, "out") + rc, _stdout, stderr = _run_main(["--firmware", os.path.join(tmp, "nope.bin"), "--out", out]) + self.assertEqual(rc, 2) + self.assertIn("firmware absent", stderr) + self.assertFalse(os.path.exists(out)) + + def test_bad_budget_is_a_parser_error(self): + with tempfile.TemporaryDirectory() as tmp: + fw = os.path.join(tmp, "new.fwimage.bin") + open(fw, "wb").close() + with self.assertRaises(SystemExit): + _run_main(["--firmware", fw, "--out", os.path.join(tmp, "out"), "--budget", "9999999"]) + + +@unittest.skipUnless(os.path.isfile(REAL_IMAGE), "real 570.144 fwimage absent") +class RealImageIntegrationTests(unittest.TestCase): + def test_triage_real_image_tiny_budget(self): + if not qemu_available(): + self.skipTest("qemu-system-riscv64 not installed") + with tempfile.TemporaryDirectory() as tmp: + report = run_triage( + TriageOptions( + firmware=REAL_IMAGE, + out=os.path.join(tmp, "out"), + budget=3000, + qemu_duration=0.2, + step_timeout=120.0, + ) + ) + identity = next(s for s in report["steps"] if s["name"] == "identity") + self.assertEqual(identity["status"], "ok") + self.assertIn("570.144", identity["firmware"]["version"]) + probe = next(s for s in report["steps"] if s["name"] == "probe") + self.assertEqual(probe["status"], "ok") + native = next(s for s in report["steps"] if s["name"] == "native_boot") + self.assertIn(native["status"], ("ok", "skipped")) + self.assertIn( + report["support_readiness"]["verdict"], ("sim-observable", "first-touch-only", "blocked") + ) + version_diff = next(s for s in report["steps"] if s["name"] == "version_diff") + self.assertEqual(version_diff["status"], "skipped") + self.assertTrue(os.path.isfile(report["artifacts"]["report_json"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/eng-ops.md b/docs/eng-ops.md index 43103a9..35b379d 100644 --- a/docs/eng-ops.md +++ b/docs/eng-ops.md @@ -602,7 +602,7 @@ authoritative for the literal default. | `gsp_phys_fault_relay` | 0 | On a relay timeout, drain the status queue for the engine fault (`0x1006`, corroborated by `0x1020`), match runlistId/chid and write the guest channel notifier + event. | Requires `gsp_phys_relay=1` and the three parameters above. | | `gsp_phys_fault_inject` | 0 | Debug: write the relayed pair's source PTE as zero to force an engine fault on the next relay submission; only relayed pairs are affected. | Requires the relay path; default 0 programs the real source HPA. | | `gsp_phys_rmcall_engine` | 0 | Ladder engine: 0 = COPY0 (CE copy submission), 1 = GR0 (compute object, BIND engineType 1, GR semaphore release). Snapshotted when stage 3 starts; mid-session changes are ignored. | Requires boot + init-done; CE forwarding paths refuse on a GR ladder. | -| `gsp_phys_rmcall_gr_promote` | 0 (clamp 0..18) | GR context-promote bisection: **0 never sends `PROMOTE_CTX`** (the ladder stops after the geometry queries, context buffer/PT allocation, PDB mapping and channel/compute alloc, before BIND); 1 = single MAIN entry, initialize/no-nonmapped, size raw+subctx; 2 = two-phase PA-only then VA-only with the same PA/VA/size; 3 = single entry with the exact raw size; 4 = two-phase with the reference client's zero-field VA-only step; 5 = one control with `entryCount=2` (PA-only init + zero-field VA-only); 6 = no-entry `hVirtMemory`: allocate an `NV01_MEMORY_VIRTUAL` (class 0x70) VA-range object on the ladder VAS at the context window, then `PROMOTE_CTX` with `hVirtMemory`/`virtAddress`/`size` and `entryCount=0`; 7 = no-entry bare `(virtAddress, size)` with `entryCount=0` and no `hVirtMemory`; 8 = single MAIN entry like 1 on the KMD-managed channel variant V6 (the V5 channel plus alloc flag `SKIP_CTXBUFFER_ALLOC` bit 31); 9 = same V6 channel with the stock client's zero-field VA-only MAIN entry (`bInitialize=0`); 10 = same V6 channel, `GPU_INITIALIZE_CTX` (`0x2080012d`) installs the physical context memdesc first, then `PROMOTE_CTX` `entryCount=0` bare `(virtAddress, size)`; 11 = mode 10 with the context buffer swapped to a server FBMEM allocation (class 0x40, VIDMEM `physAttr=0x4`, aperture-0 leaf PTEs); 12 = V5 UVM/vGPU-faithful PA-only MAIN entry (`bInitialize=1 bNonmapped=1`, no VA) and continue to BIND/SCHEDULE/submit; 13 = mode 12 plus a PATCH PA-only promote first (size from engine context properties engineId 0x10); 14 = V6 KMD channel + PATCH PA-only then the single-entry MAIN init+VA (the flow that books the context VA and calls the commit); 15 = mode 14 with a server FBMEM MAIN buffer and VIDMEM `physAttr 0x4`; 16 = mode 14 plus a PM ctxsw PA-only promote first (engineId 0x09); 17 = no client promote, paired with `gsp_phys_srv_ctxbuf=1` (pre-boot registry profile with `RMSetClientRMAllocatedCtxBuffer=0`) so the GSP-RM server-manages the context buffers; 18 = mode 8 shape plus a client `FERMI_CONTEXT_SHARE_A` (0x9067) on the TSG carried as `hContextShare`, whose construct allocates the GraphicsSubcontext. Live result: modes 1, 2 step 2 and 4 all wedged GSP-RM (`0x52a8b04`, `NV_ERR_OBJECT_NOT_FOUND` 0x57 in the LOGINIT stack) while the PA-only nonmapped step of modes 2/4 is accepted every time - the VA resolve, not the entry fields, is the wall (our external VAS has no server-side VA booking). Mode 6 (hVirtMemory, V5 channel) was refused **cleanly** (`status=0x1f` `NV_ERR_INVALID_ARGUMENT`, no assert, clean release) because the channel was not KMD-managed. Modes 8-11 all completed with **clean releases** (WPR2 down, driver rebound, 5 locks, pstore empty): the V6 channel alloc is accepted (`flags=0x80800000`); modes 8 and 9 (with and without an init entry) both return a clean `status=0x40` `NV_ERR_INVALID_STATE` (mode 9 without the server error log - entry shape irrelevant); mode 10's `GPU_INITIALIZE_CTX` is accepted (`status=0`) but the bare-VA promote still returns `0x40` with the same server log anchor; mode 11 (server FBMEM context buffer) returns `0x40` with a **different** server log anchor, so FBMEM moves the failure to a different structural check. Session 2 (modes 12-17, all clean): mode 12's V5 PA-only MAIN promote is **accepted** (`status=0`) and BIND succeeds, but the GR push faults `CTXNOTVALID` (`sem=0`, `GPGet=1`) - the accepted entry does not set `ENG_CTX_VALID`; mode 13 (PATCH first) and mode 14/15/16 (V6 KMD + PATCH, +PM, FBMEM/VIDMEM) all leave the MAIN promote at the same `0x40` rec-677 site as mode 8, so PATCH/PM/buffer type are not the missing prerequisite; mode 17 (registry `RMSetClientRMAllocatedCtxBuffer=0`, no promote) also faults `CTXNOTVALID`, so even server-managed context buffers leave the channel invalid; mode 18 (client context share, share alloc accepted but `subctxId` not written back) leaves the MAIN promote at the same `0x40` - the remaining wall is the engine-context commit/RAMFC path, not buffer ownership. Offline, the `0x40` candidate is the helper at `0x5aff328` (returns `NV_ERR_INVALID_STATE` when `*(u64*)(a0+0x1b90)==NULL` or `*(u8*)(a0+0x555)==0`). **Mode 19 (the stock guest's captured 9-entry set, MAIN 2 MB PA+VA on V5) is known-wedging**: fn76 never replied, the debugfs trigger dumped core, and the release fail-safe held WPR2/GPU until an approved reboot - never run it or any V5 promote with a MAIN VA entry again. Nonzero enables can still assert GSP-RM and wedge the device. | GR ladder only; value snapshotted at stage 3; **requires confirmed reboot readiness** before enabling. | +| `gsp_phys_rmcall_gr_promote` | 0 (clamp 0..18) | GR context-promote bisection: **0 never sends `PROMOTE_CTX`** (the ladder stops after the geometry queries, context buffer/PT allocation, PDB mapping and channel/compute alloc, before BIND); 1 = single MAIN entry, initialize/no-nonmapped, size raw+subctx; 2 = two-phase PA-only then VA-only with the same PA/VA/size; 3 = single entry with the exact raw size; 4 = two-phase with the reference client's zero-field VA-only step; 5 = one control with `entryCount=2` (PA-only init + zero-field VA-only); 6 = no-entry `hVirtMemory`: allocate an `NV01_MEMORY_VIRTUAL` (class 0x70) VA-range object on the ladder VAS at the context window, then `PROMOTE_CTX` with `hVirtMemory`/`virtAddress`/`size` and `entryCount=0`; 7 = no-entry bare `(virtAddress, size)` with `entryCount=0` and no `hVirtMemory`; 8 = single MAIN entry like 1 on the KMD-managed channel variant V6 (the V5 channel plus alloc flag `SKIP_CTXBUFFER_ALLOC` bit 31); 9 = same V6 channel with the stock client's zero-field VA-only MAIN entry (`bInitialize=0`); 10 = same V6 channel, `GPU_INITIALIZE_CTX` (`0x2080012d`) installs the physical context memdesc first, then `PROMOTE_CTX` `entryCount=0` bare `(virtAddress, size)`; 11 = mode 10 with the context buffer swapped to a server FBMEM allocation (class 0x40, VIDMEM `physAttr=0x4`, aperture-0 leaf PTEs); 12 = V5 UVM/vGPU-faithful PA-only MAIN entry (`bInitialize=1 bNonmapped=1`, no VA) and continue to BIND/SCHEDULE/submit; 13 = mode 12 plus a PATCH PA-only promote first (size from engine context properties engineId 0x10); 14 = V6 KMD channel + PATCH PA-only then the single-entry MAIN init+VA (the flow that books the context VA and calls the commit); 15 = mode 14 with a server FBMEM MAIN buffer and VIDMEM `physAttr 0x4`; 16 = mode 14 plus a PM ctxsw PA-only promote first (engineId 0x09); 17 = no client promote, paired with `gsp_phys_srv_ctxbuf=1` (pre-boot registry profile with `RMSetClientRMAllocatedCtxBuffer=0`) so the GSP-RM server-manages the context buffers; 18 = mode 8 shape plus a client `FERMI_CONTEXT_SHARE_A` (0x9067) on the TSG carried as `hContextShare`, whose construct allocates the GraphicsSubcontext. Live result: modes 1, 2 step 2 and 4 all wedged GSP-RM (`0x52a8b04`, `NV_ERR_OBJECT_NOT_FOUND` 0x57 in the LOGINIT stack) while the PA-only nonmapped step of modes 2/4 is accepted every time - the VA resolve, not the entry fields, is the wall (our external VAS has no server-side VA booking). Mode 6 (hVirtMemory, V5 channel) was refused **cleanly** (`status=0x1f` `NV_ERR_INVALID_ARGUMENT`, no assert, clean release) because the channel was not KMD-managed. Modes 8-11 all completed with **clean releases** (WPR2 down, driver rebound, 5 locks, pstore empty): the V6 channel alloc is accepted (`flags=0x80800000`); modes 8 and 9 (with and without an init entry) both return a clean `status=0x40` `NV_ERR_INVALID_STATE` (mode 9 without the server error log - entry shape irrelevant); mode 10's `GPU_INITIALIZE_CTX` is accepted (`status=0`) but the bare-VA promote still returns `0x40` with the same server log anchor; mode 11 (server FBMEM context buffer) returns `0x40` with a **different** server log anchor, so FBMEM moves the failure to a different structural check. Session 2 (modes 12-17, all clean): mode 12's V5 PA-only MAIN promote is **accepted** (`status=0`) and BIND succeeds, but the GR push faults `CTXNOTVALID` (`sem=0`, `GPGet=1`) - the accepted entry does not set `ENG_CTX_VALID`; mode 13 (PATCH first) and mode 14/15/16 (V6 KMD + PATCH, +PM, FBMEM/VIDMEM) all leave the MAIN promote at the same `0x40` rec-677 site as mode 8, so PATCH/PM/buffer type are not the missing prerequisite; mode 17 (registry `RMSetClientRMAllocatedCtxBuffer=0`, no promote) also faults `CTXNOTVALID`, so even server-managed context buffers leave the channel invalid; mode 18 (client context share, share alloc accepted but `subctxId` not written back) leaves the MAIN promote at the same `0x40` - the remaining wall is the engine-context commit/RAMFC path, not buffer ownership. Offline, the `0x40` candidate is the helper at `0x5aff328` (returns `NV_ERR_INVALID_STATE` when `*(u64*)(a0+0x1b90)==NULL` or `*(u8*)(a0+0x555)==0`). **Mode 19 (the stock guest's captured 9-entry set, MAIN 2 MB PA+VA on V5) is known-wedging**: fn76 never replied, the debugfs trigger dumped core, and the release fail-safe held WPR2/GPU until an approved reboot - never run it or any V5 promote with a MAIN VA entry again. Modes 20/21/22 (2026-10-02, all clean, no wedge): 20 = V6 KMD channel + no-entry `hVirtMemory`; 21 = same without `hVirtMemory` (bare `virtAddress`+`size`); 22 = mode 10's `GPU_INITIALIZE_CTX` (physical MAIN, accepted) followed by mode 20's `hVirtMemory` promote. All three return the same soft `NV_ERR_INVALID_STATE` (0x40) as the KMD entry modes and release cleanly; the KMD client-promote shape space is now exhausted. Nonzero enables can still assert GSP-RM and wedge the device. | GR ladder only; value snapshotted at stage 3; **requires confirmed reboot readiness** before enabling. | | `gsp_phys_repower` | 0 (latched) | Restore the pre-claim runtime-PM/D3cold policy when releasing the GPU; with 0 the D0 locks and runtime reference are deliberately left held until reboot. | **Never enable** (rule 3); re-arming D3cold after a claim can hard-freeze the host. | The emulation-side `sema_*` parameters (ring discovery, release recovery, diff --git a/kernel/Makefile b/kernel/Makefile index c035e41..da8f240 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -8,6 +8,7 @@ vgsp-y := \ gsp_emulation.o \ gsp_rm_emulation.o \ gsp_sema_service.o \ + gsp_cuda_bridge.o \ gsp_rpc_client.o \ gsp_rpc_integration.o \ memory.o \ diff --git a/kernel/gsp_cuda_bridge.c b/kernel/gsp_cuda_bridge.c new file mode 100644 index 0000000..aaad5ee --- /dev/null +++ b/kernel/gsp_cuda_bridge.c @@ -0,0 +1,1328 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Host CUDA bridge transport (v2): module side of the debugfs protocol under + * /sys/kernel/debug/vgsp/. The guest's queued kernel launches (inline-QMD + * method runs on the GR compute channel, see gsp_sema_service.c) are turned + * into CUBB2 job records; a userspace daemon executes them on the physical + * GPU and returns a CBD2 done record plus the output payload. Because the + * guest's CUDA buffers live in this module's emulated vidmem shadow, the + * daemon cannot reach them: the module marshals the inputs out and copies + * the daemon's output back into the resolved destination, then writes the + * QMD release semaphore so the guest's cudaDeviceSynchronize completes. + * + * Files (all under the existing vgsp debugfs directory): + * cuda_bridge_job 0444 module serves the oldest pending 256-byte job + * cuda_bridge_in 0444 module serves that job's input blob (ppos) + * cuda_bridge_out 0200 daemon writes its output blob (ppos) + * cuda_bridge_done 0200 daemon writes a 256-byte CBD2 record + * cuda_bridge_status 0444 text counters + * + * Everything is inert until a launch is detected; the whole path can be + * switched off with the cuda_bridge module parameter. + */ + +#define pr_fmt(fmt) "cuda-bridge: " fmt + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/debugfs.h> +#include <linux/fs.h> +#include <linux/mutex.h> +#include <linux/spinlock.h> +#include <linux/slab.h> +#include <linux/vmalloc.h> +#include <linux/uaccess.h> +#include <linux/jiffies.h> +#include <linux/string.h> + +#include "nvidia-vgpu-vfio.h" + +/* Records and limits (see tools/cuda-bridge/PROTOCOL.md, v2). */ +#define CUBB2_JOB_MAGIC 0x32424243u /* 'CBB2' */ +#define CUBB2_DONE_MAGIC 0x32444243u /* 'CBD2' */ +#define CUBB2_VERSION 2 +#define CUBB2_RECORD_SIZE 256 +#define CUBB2_IN_MAX (32u * 1024u * 1024u) +#define CUBB2_OUT_MAX (32u * 1024u * 1024u) +#define CUBB2_REQ_MAX 16 +#define CUBB2_PEND_MAX 4 +#define CUBB2_DONE_RING 16 +#define CUBB2_NAME_MAX 64 + +/* + * Parameter-bank location. CUDA writes the launch arguments into the QMD's + * parameter bank with GRCE LOAD_INLINE_DATA at an arbitrary offset (observed: + * mail args at bank+0x160 while bank+0 held unrelated constants), so the + * bridge searches the recorded recent inline-write destinations inside the + * bank, then scans the bank itself for a mail/vadd argument shape. + */ +#define CUBB2_INLINE_MAX 16 +#define CUBB2_PARAM_BLOB 40 +#define CUBB2_PARAM_SCAN_MAX (128u * 1024u) +#define CUBB2_PAGE 4096 + +/* + * Natural-alignment layout (x86_64 kernel and daemon agree): + * in_bytes at 0x38, sem_flags 0x58, prog_va 0x60, rsv at 0x70. + */ +struct cubb2_job { + u32 magic; + u32 version; + u64 seq; + u32 opcode; + u32 grid[3]; + u32 block[3]; + u32 shared; + u32 n; + u64 in_bytes; + u64 out_bytes; + u64 sem_va; + u64 sem_payload; + u32 sem_flags; + u32 pad; + u64 prog_va; + u32 param_count; + u32 pad2; + u8 rsv[0x100 - 0x70]; +}; + +/* status 0 = ok, 1 = bad job, 2 = CUDA error, 3 = RAM/mapping error. */ +struct cubb2_done { + u32 magic; + u32 version; + u64 seq; + u32 status; + u32 bad; + u64 checksum; + u64 kernel_ns; + u32 out_bytes_written; + u32 err_code; + char device_name[CUBB2_NAME_MAX]; + u64 host_ns; + u8 rsv[0x100 - 0x78]; +}; + +static_assert(sizeof(struct cubb2_job) == CUBB2_RECORD_SIZE, + "cubb2_job must be 256 bytes"); +static_assert(sizeof(struct cubb2_done) == CUBB2_RECORD_SIZE, + "cubb2_done must be 256 bytes"); +static_assert(offsetof(struct cubb2_job, prog_va) == 0x60, + "cubb2_job natural layout changed"); +static_assert(offsetof(struct cubb2_done, host_ns) == 0x70, + "cubb2_done natural layout changed"); + +/* Entry points (also declared in gsp_sema_service.c). */ +bool gsp_cuda_bridge_enabled(void); +void gsp_cuda_bridge_launch(struct nv_vgpu_device *vgpu, u32 h_object, + const u32 qmd[64], u64 qmd_dev_va, u64 push_va, + const u64 *recent_dsts, u32 n_recent_dsts); +void gsp_cuda_bridge_service(struct nv_vgpu_device *vgpu); +int gsp_cuda_bridge_init(void); +void gsp_cuda_bridge_fini(void); + +/* Helpers exported by gsp_sema_service.c (declared here, not in the header, + * so the bridge stays a leaf of the sema service). */ +int gsp_sema_service_va_read(struct nv_vgpu_device *vgpu, u64 va, void *buf, + size_t len); +int gsp_sema_service_va_write(struct nv_vgpu_device *vgpu, u64 va, + const void *buf, size_t len); +int gsp_sema_service_va_resolve(struct nv_vgpu_device *vgpu, u64 va, u64 *pa, + u32 *ap); +int gsp_sema_service_sem_read(struct nv_vgpu_device *vgpu, u64 va, u32 *val); +int gsp_sema_service_sem_write(struct nv_vgpu_device *vgpu, u32 h, u64 va, + u32 val); +extern bool gsp_sema_dbg; + +static bool cubb_enable = true; +module_param_named(cuda_bridge, cubb_enable, bool, 0644); +MODULE_PARM_DESC(cuda_bridge, + "Enable the host CUDA bridge launch transport (default 1)"); + +/* ------------------------------------------------------------------ */ +/* launch requests: decode-thread producer, service-thread consumer */ +/* ------------------------------------------------------------------ */ + +struct cubb_req { + u32 h_object; + u64 qmd_dev_va; + u64 push_va; + u32 qmd[64]; + u32 attempts; + u32 n_recent_dsts; + u64 recent_dsts[CUBB2_INLINE_MAX]; +}; + +static struct cubb_req cubb_req[CUBB2_REQ_MAX]; +static u32 cubb_req_head; /* producer */ +static u32 cubb_req_tail; /* consumer */ +static DEFINE_SPINLOCK(cubb_req_lock); + +/* + * A pin-batch rewind replays recently consumed GPFIFO entries, so the same + * launch push is decoded again and would queue a duplicate job (victory24 + * showed the same push three times). Dedupe on a content hash of the QMD: + * replaying one push hashes identically, a genuine relaunch has a different + * completion semaphore/payload inside the QMD. + */ +#define CUBB2_DEDUP 8 +#define CUBB2_DEDUP_MS 5000 +struct cubb_dedup { + u64 hash; + unsigned long when; +}; +static struct cubb_dedup cubb_dedup[CUBB2_DEDUP]; +static u32 cubb_dedup_next; +static u64 cubb_dedup_hits; + +/* ------------------------------------------------------------------ */ +/* pending jobs (daemon-visible) and daemon write-back state */ +/* ------------------------------------------------------------------ */ + +struct cubb_pend { + bool valid; + u64 seq; + u32 h_object; + u32 opcode; + u64 dst_va; + u64 out_bytes; + u64 sem_va; + u32 sem_payload; + u32 sem_flags; + struct cubb2_job job; + u8 *in; /* vmalloc'd input blob */ +}; + +static struct cubb_pend cubb_pend[CUBB2_PEND_MAX]; +static u64 cubb_seq; +static DEFINE_MUTEX(cubb_lock); + +/* daemon out blob, accumulated by cuda_bridge_out writes */ +static u8 *cubb_out_buf; +static unsigned int cubb_out_alloc; +static size_t cubb_out_len; + +/* done ring, producer = daemon write handler, consumer = service thread */ +static struct cubb2_done cubb_done_ring[CUBB2_DONE_RING]; +static u32 cubb_done_head; +static u32 cubb_done_tail; +static DEFINE_SPINLOCK(cubb_done_lock); + +/* counters (plain reads from the status file) */ +static u64 cubb_jobs; +static u64 cubb_completed; +static u64 cubb_failed; +static u64 cubb_dropped; /* request ring/marshal/allocation drops */ +static u64 cubb_stale; /* done records for unknown seq */ +static u64 cubb_malformed; /* done records with bad magic/version */ +static u64 cubb_last_seq; +static int cubb_last_err; +static u64 cubb_launch_log; /* verbose header budget */ + +/* ------------------------------------------------------------------ */ +/* debugfs */ +/* ------------------------------------------------------------------ */ + +static bool cubb_root_owned; +static bool cubb_debugfs_ok; + +static void cubb_debugfs_fini(void); +static const struct file_operations cubb_job_fops; +static const struct file_operations cubb_in_fops; +static const struct file_operations cubb_out_fops; +static const struct file_operations cubb_done_fops; +static const struct file_operations cubb_status_fops; + +static struct dentry *cubb_find_root(void) +{ + struct dentry *root = debugfs_lookup("vgsp", NULL); + + if (IS_ERR(root)) + return NULL; + return root; +} + +static void cubb_debugfs_init(void) +{ + struct dentry *root, *f; + + cubb_root_owned = false; + root = cubb_find_root(); + if (!root) { + root = debugfs_create_dir("vgsp", NULL); + if (IS_ERR_OR_NULL(root)) { + pr_warn("debugfs dir vgsp unavailable\n"); + return; + } + cubb_root_owned = true; + } + + f = debugfs_create_file("cuda_bridge_job", 0444, root, NULL, + &cubb_job_fops); + if (IS_ERR_OR_NULL(f)) + goto fail; + f = debugfs_create_file("cuda_bridge_in", 0444, root, NULL, + &cubb_in_fops); + if (IS_ERR_OR_NULL(f)) + goto fail; + f = debugfs_create_file("cuda_bridge_out", 0200, root, NULL, + &cubb_out_fops); + if (IS_ERR_OR_NULL(f)) + goto fail; + f = debugfs_create_file("cuda_bridge_done", 0200, root, NULL, + &cubb_done_fops); + if (IS_ERR_OR_NULL(f)) + goto fail; + f = debugfs_create_file("cuda_bridge_status", 0444, root, NULL, + &cubb_status_fops); + if (IS_ERR_OR_NULL(f)) + goto fail; + + cubb_debugfs_ok = true; + dput(root); + pr_info("debugfs ready (/sys/kernel/debug/vgsp/cuda_bridge_*)\n"); + return; + +fail: + dput(root); + cubb_debugfs_fini(); + pr_warn("debugfs file creation failed\n"); +} + +static void cubb_debugfs_fini(void) +{ + struct dentry *root; + + cubb_debugfs_ok = false; + root = cubb_find_root(); + if (!root) + return; + debugfs_lookup_and_remove("cuda_bridge_job", root); + debugfs_lookup_and_remove("cuda_bridge_in", root); + debugfs_lookup_and_remove("cuda_bridge_out", root); + debugfs_lookup_and_remove("cuda_bridge_done", root); + debugfs_lookup_and_remove("cuda_bridge_status", root); + dput(root); + if (cubb_root_owned) { + debugfs_lookup_and_remove("vgsp", NULL); + cubb_root_owned = false; + } +} + +/* ------------------------------------------------------------------ */ +/* staging helpers */ +/* ------------------------------------------------------------------ */ + +static u32 cubb_rd32(const u8 *p) +{ + u32 v; + + memcpy(&v, p, 4); + return v; +} + +static u64 cubb_rd64(const u8 *p) +{ + u64 v; + + memcpy(&v, p, 8); + return v; +} + +/* A pointer the guest could have given the GPU (user/device VA range). */ +static bool cubb_va_plausible(u64 v) +{ + return v >= 0x10000 && v < (1ULL << 49); +} + +/* mail arguments: {u64 output ptr, u32 len}, len 1..4096. */ +static bool cubb_mail_shape(const u8 *p, size_t len, u64 *ptr, u32 *n) +{ + u64 v; + u32 l; + + if (len < 12) + return false; + v = cubb_rd64(p); + l = cubb_rd32(p + 8); + if (!cubb_va_plausible(v) || !l || l > 4096) + return false; + if (ptr) + *ptr = v; + if (n) + *n = l; + return true; +} + +/* vadd arguments: {u64 a, b, c, u32 n}, n == 1 MiB (frozen launch shape). */ +static bool cubb_vadd_shape(const u8 *p, size_t len, u64 *out, u32 *n) +{ + u64 a, b, c; + u32 k; + + if (len < 32) + return false; + a = cubb_rd64(p); + b = cubb_rd64(p + 8); + c = cubb_rd64(p + 16); + k = cubb_rd32(p + 24); + if (!cubb_va_plausible(a) || !cubb_va_plausible(b) || + !cubb_va_plausible(c) || k != (1u << 20)) + return false; + if (out) + *out = c; + if (n) + *n = k; + return true; +} + +static int cubb_shape_kind(const u8 *p, size_t len) +{ + if (cubb_mail_shape(p, len, NULL, NULL)) + return 1; + if (cubb_vadd_shape(p, len, NULL, NULL)) + return 2; + return 0; +} + +/* + * Locate the launch's argument block inside the QMD parameter bank. CUDA + * writes the arguments with GRCE LOAD_INLINE_DATA at an arbitrary bank + * offset, so never assume +0: first try the recorded recent inline-write + * destinations that fall inside [bank_va, bank_va + bank_size), newest + * first, then scan the bank for a mail/vadd argument shape. `want` names the + * shape implied by grid/block (0 = unknown). On a match returns 0 and fills + * off/kind/params/src; -ENODATA means the bank was readable but no shape was + * found (caller may fall back to bank+0); -ENOENT means a read failed (the + * page resolve may still be in progress) and nothing matched, so the caller + * should retry. + */ +static int cubb_find_params(struct nv_vgpu_device *vgpu, u64 bank_va, + u32 bank_size, const u64 *dsts, u32 n_dsts, + u32 want, u64 *off, u32 *kind, u8 *params, + size_t plen, const char **src) +{ + u8 cand[CUBB2_PARAM_BLOB]; + u8 *page; + u64 hi = bank_va + bank_size; + u32 i, pgoff, o, scan; + bool read_fail = false; + + *off = 0; + *kind = 0; + if (bank_size < 16) + return -ENODATA; + + /* (a) most recently written inline destinations inside the bank */ + for (i = 0; i < n_dsts; i++) { + u64 va = dsts[i]; + size_t need = want == 2 ? 32 : 12; + size_t got = min_t(size_t, plen, sizeof(cand)); + int k; + + if (!va || va < bank_va || va + need > hi) + continue; + if (va + got > hi) + got = hi - va; + if (gsp_sema_service_va_read(vgpu, va, cand, got)) { + read_fail = true; + continue; + } + k = cubb_shape_kind(cand, got); + if (!k || (want && k != want)) + continue; + *off = va - bank_va; + *kind = k; + memcpy(params, cand, min_t(size_t, got, plen)); + *src = "inline"; + return 0; + } + + /* (b) scan the bank for an argument shape (straddle-safe per page) */ + scan = bank_size > CUBB2_PARAM_SCAN_MAX ? CUBB2_PARAM_SCAN_MAX : + bank_size; + page = vmalloc(CUBB2_PAGE + CUBB2_PARAM_BLOB); + if (!page) + return read_fail ? -ENOENT : -ENODATA; + for (pgoff = 0; pgoff + 32 <= scan; pgoff += CUBB2_PAGE) { + u32 available = scan - pgoff; + u32 read_len = available > CUBB2_PAGE + CUBB2_PARAM_BLOB ? + CUBB2_PAGE + CUBB2_PARAM_BLOB : available; + + if (gsp_sema_service_va_read(vgpu, bank_va + pgoff, page, + read_len)) { + read_fail = true; + continue; + } + for (o = 0; o < available && o < CUBB2_PAGE && + o + 32 <= read_len; o += 8) { + int k = cubb_shape_kind(page + o, read_len - o); + + if (!k || (want && k != want)) + continue; + *off = pgoff + o; + *kind = k; + memcpy(params, page + o, + min_t(size_t, read_len - o, plen)); + *src = "scan"; + vfree(page); + return 0; + } + } + vfree(page); + return read_fail ? -ENOENT : -ENODATA; +} + +static struct cubb_pend *cubb_pend_lookup(u64 seq, int *idx) +{ + int i; + + for (i = 0; i < CUBB2_PEND_MAX; i++) { + if (cubb_pend[i].valid && cubb_pend[i].seq == seq) { + if (idx) + *idx = i; + return &cubb_pend[i]; + } + } + return NULL; +} + +static int cubb_pend_slot(void) +{ + int i; + + for (i = 0; i < CUBB2_PEND_MAX; i++) + if (!cubb_pend[i].valid) + return i; + return -1; +} + +/* ------------------------------------------------------------------ */ +/* launch decode -> request queue */ +/* ------------------------------------------------------------------ */ + +bool gsp_cuda_bridge_enabled(void) +{ + return cubb_enable && cubb_debugfs_ok; +} + +/* + * Called from svc_decode_stream on the service thread when a complete inline + * QMD has been accumulated. Only queues the launch: marshalling guest memory + * happens on the next service pass so the push pump is never held. + */ +void gsp_cuda_bridge_launch(struct nv_vgpu_device *vgpu, u32 h_object, + const u32 qmd[64], u64 qmd_dev_va, u64 push_va, + const u64 *recent_dsts, u32 n_recent_dsts) +{ + unsigned long flags; + u64 hash = 1469598103934665603ULL; + int i; + + if (!gsp_cuda_bridge_enabled()) + return; + (void)vgpu; + + for (i = 0; i < 64; i++) { + hash ^= qmd[i]; + hash *= 1099511628211ULL; + } + hash ^= (u64)h_object << 32; + hash *= 1099511628211ULL; + hash ^= qmd_dev_va; + + spin_lock_irqsave(&cubb_req_lock, flags); + for (i = 0; i < CUBB2_DEDUP; i++) { + if (cubb_dedup[i].hash == hash && + time_before(jiffies, + cubb_dedup[i].when + + msecs_to_jiffies(CUBB2_DEDUP_MS))) { + spin_unlock_irqrestore(&cubb_req_lock, flags); + cubb_dedup_hits++; + return; + } + } + cubb_dedup[cubb_dedup_next % CUBB2_DEDUP].hash = hash; + cubb_dedup[cubb_dedup_next % CUBB2_DEDUP].when = jiffies; + cubb_dedup_next++; + + if (cubb_req_head - cubb_req_tail >= CUBB2_REQ_MAX) { + spin_unlock_irqrestore(&cubb_req_lock, flags); + cubb_dropped++; + pr_info_ratelimited("request ring full, launch dropped (push=0x%llx)\n", + push_va); + return; + } + { + struct cubb_req *r = + &cubb_req[cubb_req_head % CUBB2_REQ_MAX]; + + r->h_object = h_object; + r->qmd_dev_va = qmd_dev_va; + r->push_va = push_va; + r->attempts = 0; + memcpy(r->qmd, qmd, sizeof(r->qmd)); + r->n_recent_dsts = 0; + if (recent_dsts && n_recent_dsts) { + if (n_recent_dsts > CUBB2_INLINE_MAX) + n_recent_dsts = CUBB2_INLINE_MAX; + memcpy(r->recent_dsts, recent_dsts, + n_recent_dsts * sizeof(u64)); + r->n_recent_dsts = n_recent_dsts; + } + } + cubb_req_head++; + spin_unlock_irqrestore(&cubb_req_lock, flags); +} + +/* Consume the request the marshaller was working on (service thread only). */ +static void cubb_req_drop_head(void) +{ + unsigned long flags; + + spin_lock_irqsave(&cubb_req_lock, flags); + if (cubb_req_tail != cubb_req_head) + cubb_req_tail++; + spin_unlock_irqrestore(&cubb_req_lock, flags); +} + +/* + * A guest VA read can miss because the targeted rm_mem scan for that page is + * still in progress (it walks the RAM a slice per call and reports -EAGAIN -> + * -ENOENT). Keep the request at the queue head and retry on later passes. + */ +#define CUBB2_REQ_TRIES 300 + +static void cubb_req_note_attempt(const struct cubb_req *r) +{ + unsigned long flags; + + spin_lock_irqsave(&cubb_req_lock, flags); + if (cubb_req_tail != cubb_req_head) + cubb_req[cubb_req_tail % CUBB2_REQ_MAX].attempts = r->attempts; + spin_unlock_irqrestore(&cubb_req_lock, flags); +} + +static bool cubb_req_retry(const struct cubb_req *r, int rc) +{ + if ((rc == -ENOENT || rc == -EAGAIN) && + r->attempts < CUBB2_REQ_TRIES) { + cubb_req_note_attempt(r); + return true; + } + return false; +} + +/* ------------------------------------------------------------------ */ +/* request -> pending job (marshalling) */ +/* ------------------------------------------------------------------ */ + +static void cubb_log_raw_qmd(const u32 *qmd) +{ + int i; + + pr_info("qmd raw (64 words):\n"); + for (i = 0; i < 64; i += 4) + pr_info("qmd[%02d] %08x %08x %08x %08x\n", i, + qmd[i], qmd[i + 1], qmd[i + 2], qmd[i + 3]); +} + +static int cubb_marshal_one(struct nv_vgpu_device *vgpu) +{ + struct cubb_req r; + struct cubb2_job job; + unsigned long flags; + u32 gx, gy, gz, bx, by, bz, shared; + u64 param_va, sem_va, prog_va; + u32 param_size, sem_payload, sem_flags, red_en, one_word, enable; + u8 params[CUBB2_PARAM_BLOB]; + u32 plen, n = 0, opcode = 0, param_count = 0; + u32 hint = 0, found_kind = 0; + u64 params_off = 0; + const char *params_src = "bank+0"; + u64 dst_va = 0, in_bytes = 0, out_bytes = 0; + u64 dst_pa = 0; + u32 dst_ap = 0; + u8 *in = NULL; + int rc, slot; + + spin_lock_irqsave(&cubb_req_lock, flags); + if (cubb_req_tail == cubb_req_head) { + spin_unlock_irqrestore(&cubb_req_lock, flags); + return 0; + } + r = cubb_req[cubb_req_tail % CUBB2_REQ_MAX]; + r.attempts++; + spin_unlock_irqrestore(&cubb_req_lock, flags); + + gx = r.qmd[12]; + gy = r.qmd[13] & 0xffff; + gz = r.qmd[14] & 0xffff; + shared = r.qmd[17] & 0x3ffff; + bx = r.qmd[18] >> 16; + by = r.qmd[19] & 0xffff; + bz = r.qmd[19] >> 16; + enable = (r.qmd[4] >> 10) & 1; + sem_va = r.qmd[23] | ((u64)(r.qmd[24] & 0xff) << 32); + red_en = (r.qmd[24] >> 26) & 1; + one_word = (r.qmd[24] >> 31) & 1; + sem_payload = r.qmd[25]; + param_va = r.qmd[32] | ((u64)(r.qmd[33] & 0x1ffff) << 32); + param_size = (r.qmd[33] >> 19) << 4; + prog_va = r.qmd[48] | ((u64)(r.qmd[49] & 0x1ffff) << 32); + sem_flags = (red_en ? 1u : 0u) | (one_word ? 2u : 0u) | + (enable ? 4u : 0u); + + /* + * The parameter bank lives in the client's VA space, and CUDA writes + * the arguments into it with GRCE inline writes at an arbitrary + * offset (observed: mail args at bank+0x160, bank+0 unrelated). The + * frozen grid/block shapes hint at the kernel; locate the actual + * argument block by recent inline-write destination, then by shape + * scan of the bank. + */ + plen = sizeof(params); + if (gx == 1 && gy == 1 && gz == 1 && bx == 64 && by == 1 && bz == 1) + hint = 1; + else if (gx == 4096 && gy == 1 && gz == 1 && bx == 256 && by == 1 && + bz == 1) + hint = 2; + + rc = cubb_find_params(vgpu, param_va, param_size, r.recent_dsts, + r.n_recent_dsts, hint, ¶ms_off, &found_kind, + params, plen, ¶ms_src); + if (rc) { + if (cubb_req_retry(&r, rc)) + return 0; + if (rc != -ENODATA) { + cubb_req_drop_head(); + cubb_failed++; + cubb_last_err = rc; + pr_info_ratelimited("failed seq=0 param locate va=0x%llx size=0x%x rc=%d push=0x%llx h=0x%x\n", + param_va, param_size, rc, r.push_va, + r.h_object); + return 0; + } + /* No argument shape found: legacy bank+0 read. */ + rc = gsp_sema_service_va_read(vgpu, param_va, params, plen); + if (rc) { + if (cubb_req_retry(&r, rc)) + return 0; + cubb_req_drop_head(); + cubb_failed++; + cubb_last_err = rc; + pr_info_ratelimited("failed seq=0 op=? param bank va=0x%llx size=0x%x rc=%d push=0x%llx h=0x%x\n", + param_va, param_size, rc, r.push_va, + r.h_object); + return 0; + } + params_off = 0; + found_kind = 0; + params_src = "bank+0"; + } + + /* + * Classify: the two frozen launch shapes first; then the located + * argument block; otherwise the old content heuristic. + */ + if (hint) + opcode = hint; + else if (found_kind) + opcode = found_kind; + else if (cubb_va_plausible(cubb_rd64(params)) && + cubb_va_plausible(cubb_rd64(params + 8)) && + cubb_va_plausible(cubb_rd64(params + 16)) && + cubb_rd32(params + 24) && + cubb_rd32(params + 24) <= (64u << 20)) + opcode = 2; + else + opcode = 1; + + if (opcode == 1) { + /* mail: {ptr, len} */ + dst_va = cubb_rd64(params); + n = cubb_rd32(params + 8); + out_bytes = n; + in_bytes = 0; + param_count = 2; + if (n > 4096) { + cubb_req_drop_head(); + cubb_failed++; + cubb_last_err = -E2BIG; + pr_info_ratelimited("failed seq=0 op=mail len %u too large (dst=0x%llx)\n", + n, dst_va); + return 0; + } + } else { + /* vadd: {a, b, c, n} (+ 5th slot) */ + u64 a = cubb_rd64(params); + u64 b = cubb_rd64(params + 8); + + dst_va = cubb_rd64(params + 16); + n = cubb_rd32(params + 24); + out_bytes = 4ULL * n; + in_bytes = 8ULL * n; + param_count = 5; + if (!n || in_bytes > CUBB2_IN_MAX || + out_bytes > CUBB2_OUT_MAX) { + cubb_req_drop_head(); + cubb_failed++; + cubb_last_err = -E2BIG; + pr_info_ratelimited("failed seq=0 op=vadd n=%u in=%llu out=%llu too large\n", + n, in_bytes, out_bytes); + return 0; + } + in = vmalloc(in_bytes); + if (!in) { + cubb_req_drop_head(); + cubb_failed++; + cubb_last_err = -ENOMEM; + pr_info_ratelimited("failed seq=0 op=vadd in alloc %llu bytes\n", + in_bytes); + return 0; + } + rc = gsp_sema_service_va_read(vgpu, a, in, in_bytes / 2); + if (!rc) + rc = gsp_sema_service_va_read(vgpu, b, + in + in_bytes / 2, + in_bytes / 2); + if (rc) { + vfree(in); + if (cubb_req_retry(&r, rc)) + return 0; + cubb_req_drop_head(); + cubb_failed++; + cubb_last_err = rc; + pr_info_ratelimited("failed seq=0 op=vadd src a=0x%llx b=0x%llx rc=%d\n", + a, b, rc); + return 0; + } + } + + if (!n || !out_bytes || out_bytes > CUBB2_OUT_MAX) { + vfree(in); + cubb_req_drop_head(); + cubb_failed++; + cubb_last_err = -EINVAL; + pr_info_ratelimited("failed seq=0 op=%u bad sizes n=%u out=%llu\n", + opcode, n, out_bytes); + return 0; + } + + if (opcode == 1) + pr_info_ratelimited("params off=0x%llx kind=mail ptr=0x%llx len/n=%u\n", + params_off, dst_va, n); + else + pr_info_ratelimited("params off=0x%llx kind=vadd ptr=0x%llx len/n=%u\n", + params_off, dst_va, n); + + rc = gsp_sema_service_va_resolve(vgpu, dst_va, &dst_pa, &dst_ap); + + memset(&job, 0, sizeof(job)); + job.magic = CUBB2_JOB_MAGIC; + job.version = CUBB2_VERSION; + job.opcode = opcode; + job.grid[0] = gx; + job.grid[1] = gy; + job.grid[2] = gz; + job.block[0] = bx; + job.block[1] = by; + job.block[2] = bz; + job.shared = shared; + job.n = n; + job.in_bytes = in_bytes; + job.out_bytes = out_bytes; + job.sem_va = sem_va; + job.sem_payload = sem_payload; + job.sem_flags = sem_flags; + job.prog_va = prog_va; + job.param_count = param_count; + + mutex_lock(&cubb_lock); + slot = cubb_pend_slot(); + if (slot < 0) { + mutex_unlock(&cubb_lock); + vfree(in); + cubb_req_drop_head(); + cubb_dropped++; + pr_info_ratelimited("pending queue full, launch dropped\n"); + return 0; + } + job.seq = ++cubb_seq; + cubb_pend[slot].valid = true; + cubb_pend[slot].seq = job.seq; + cubb_pend[slot].h_object = r.h_object; + cubb_pend[slot].opcode = opcode; + cubb_pend[slot].dst_va = dst_va; + cubb_pend[slot].out_bytes = out_bytes; + cubb_pend[slot].sem_va = sem_va; + cubb_pend[slot].sem_payload = sem_payload; + cubb_pend[slot].sem_flags = sem_flags; + cubb_pend[slot].job = job; + cubb_pend[slot].in = in; + cubb_jobs++; + cubb_last_seq = job.seq; + mutex_unlock(&cubb_lock); + cubb_req_drop_head(); + + if (cubb_launch_log < 8) { + cubb_launch_log++; + pr_info("launch seq=%llu op=%s grid=%u,%u,%u block=%u,%u,%u shared=%u n=%u in=%llu out=%llu sem=0x%llx pay=%u flags=0x%x param=0x%llx psize=0x%x poff=0x%llx psrc=%s prog=0x%llx dst=0x%llx dstpa=0x%llx dstap=%u h=0x%x push=0x%llx qmddev=0x%llx\n", + job.seq, opcode == 1 ? "mail" : "vadd", + gx, gy, gz, bx, by, bz, shared, n, in_bytes, + out_bytes, sem_va, sem_payload, sem_flags, param_va, + param_size, params_off, params_src, prog_va, dst_va, + dst_pa, dst_ap, r.h_object, r.push_va, r.qmd_dev_va); + if (gsp_sema_dbg) { + cubb_log_raw_qmd(r.qmd); + print_hex_dump(KERN_INFO, "cuda-bridge: params ", + DUMP_PREFIX_OFFSET, 16, 1, params, + plen, false); + } + } else { + pr_info_ratelimited("launch seq=%llu op=%s n=%u out=%llu\n", + job.seq, opcode == 1 ? "mail" : "vadd", + n, out_bytes); + } + return 0; +} + +/* ------------------------------------------------------------------ */ +/* done records -> completion */ +/* ------------------------------------------------------------------ */ + +static bool cubb_done_pop(struct cubb2_done *out) +{ + unsigned long flags; + bool have = false; + + spin_lock_irqsave(&cubb_done_lock, flags); + if (cubb_done_tail != cubb_done_head) { + *out = cubb_done_ring[cubb_done_tail % CUBB2_DONE_RING]; + cubb_done_tail++; + have = true; + } + spin_unlock_irqrestore(&cubb_done_lock, flags); + return have; +} + +static void cubb_finish_one(struct nv_vgpu_device *vgpu, + const struct cubb2_done *d) +{ + struct cubb_pend *p; + int idx = -1; + int rc = 0; + u32 sem_val = 0; + u32 sem_rc = 0; + u64 wrote; + + mutex_lock(&cubb_lock); + p = cubb_pend_lookup(d->seq, &idx); + if (!p) { + mutex_unlock(&cubb_lock); + cubb_stale++; + pr_info_ratelimited("stale done seq=%llu (no pending job)\n", + d->seq); + return; + } + + if (d->status != 0) { + cubb_failed++; + cubb_last_err = (int)d->err_code; + pr_info("failed seq=%llu err=%u status=%u op=%u\n", + d->seq, d->err_code, d->status, p->opcode); + goto out; + } + + wrote = min_t(u64, p->out_bytes, cubb_out_len); + if (wrote < p->out_bytes) { + cubb_failed++; + cubb_last_err = -ENODATA; + pr_info("failed seq=%llu err=%d (daemon wrote %llu of %llu out bytes)\n", + d->seq, -ENODATA, wrote, p->out_bytes); + goto out; + } + + rc = gsp_sema_service_va_write(vgpu, p->dst_va, cubb_out_buf, wrote); + if (rc) { + cubb_failed++; + cubb_last_err = rc; + pr_info("failed seq=%llu err=%d (output write va=0x%llx len=%llu)\n", + d->seq, rc, p->dst_va, wrote); + goto out; + } + /* + * Read-back check over the whole output: proves the bytes landed + * through the translation the guest will read (vidmem shadow or + * guest RAM). A missing/mismatching copy must NOT be signalled + * complete, or the guest reads stale data and runs on. + */ + { + u8 check[256]; + size_t off = 0; + + while (off < wrote) { + size_t clen = min_t(size_t, wrote - off, + sizeof(check)); + + if (gsp_sema_service_va_read(vgpu, p->dst_va + off, + check, clen) || + memcmp(check, cubb_out_buf + off, clen)) { + cubb_failed++; + cubb_last_err = -EIO; + pr_info("failed seq=%llu err=%d (output read-back mismatch va=0x%llx off=%zu len=%zu)\n", + d->seq, -EIO, p->dst_va, off, clen); + goto out; + } + off += clen; + } + } + + if ((p->sem_flags & 4u) && p->sem_va) { + sem_val = p->sem_payload; + if (p->sem_flags & 1u) { + u32 cur = 0; + + if (!gsp_sema_service_sem_read(vgpu, p->sem_va, &cur)) + sem_val = cur + p->sem_payload; + } + sem_rc = gsp_sema_service_sem_write(vgpu, p->h_object, + p->sem_va, sem_val); + if (sem_rc) { + cubb_failed++; + cubb_last_err = sem_rc; + pr_info("failed seq=%llu err=%d sem_va=0x%llx pay=%u\n", + d->seq, sem_rc, p->sem_va, sem_val); + goto out; + } + } + + { + u64 dpa = 0; + u32 dap = 0; + u64 sempa = 0; + u32 semap = 0; + + (void)gsp_sema_service_va_resolve(vgpu, p->dst_va, &dpa, &dap); + if (p->sem_va) + (void)gsp_sema_service_va_resolve(vgpu, p->sem_va, + &sempa, &semap); + cubb_completed++; + pr_info("completed seq=%llu op=%u dst=0x%llx dstpa=0x%llx dstap=%u out_bytes=%llu/%llu sem_va=0x%llx sempa=0x%llx pay=%u bad=%u kernel_ns=%llu\n", + d->seq, p->opcode, p->dst_va, dpa, dap, wrote, + p->out_bytes, p->sem_va, sempa, sem_val, d->bad, + d->kernel_ns); + } + +out: + vfree(p->in); + p->in = NULL; + p->valid = false; + cubb_out_len = 0; + if (idx >= 0) { + /* compact: shift the younger entries down one slot */ + int j = idx; + + while (j + 1 < CUBB2_PEND_MAX && cubb_pend[j + 1].valid) { + cubb_pend[j] = cubb_pend[j + 1]; + j++; + } + cubb_pend[j].in = NULL; + cubb_pend[j].valid = false; + } + mutex_unlock(&cubb_lock); +} + +/* + * Service-thread hook: marshal at most one queued launch into a daemon-visible + * job, then resolve any completed jobs. Called every service pass; a no-op + * when no launch has been seen. + */ +void gsp_cuda_bridge_service(struct nv_vgpu_device *vgpu) +{ + struct cubb2_done d; + int guard = 0; + + if (!gsp_cuda_bridge_enabled()) + return; + + (void)cubb_marshal_one(vgpu); + while (guard++ < CUBB2_DONE_RING && cubb_done_pop(&d)) { + if (d.magic != CUBB2_DONE_MAGIC || + d.version != CUBB2_VERSION) { + cubb_malformed++; + continue; + } + cubb_finish_one(vgpu, &d); + } +} + +/* ------------------------------------------------------------------ */ +/* debugfs handlers */ +/* ------------------------------------------------------------------ */ + +static ssize_t cubb_job_read(struct file *f, char __user *ubuf, size_t count, + loff_t *ppos) +{ + u8 rec[CUBB2_RECORD_SIZE]; + int idx = -1; + struct cubb_pend *p = NULL; + size_t off = *ppos; + size_t n; + + if (!cubb_debugfs_ok) + return 0; + mutex_lock(&cubb_lock); + for (idx = 0; idx < CUBB2_PEND_MAX; idx++) { + if (cubb_pend[idx].valid) { + p = &cubb_pend[idx]; + break; + } + } + if (!p) { + mutex_unlock(&cubb_lock); + return 0; + } + memcpy(rec, &p->job, sizeof(rec)); + mutex_unlock(&cubb_lock); + + if (off >= sizeof(rec)) + return 0; + n = min_t(size_t, count, sizeof(rec) - off); + if (copy_to_user(ubuf, rec + off, n)) + return -EFAULT; + *ppos = off + n; + return n; +} + +static ssize_t cubb_in_read(struct file *f, char __user *ubuf, size_t count, + loff_t *ppos) +{ + struct cubb_pend *p = NULL; + u64 in_bytes = 0; + u8 *in = NULL; + size_t off = *ppos; + size_t n; + int rc = 0; + int idx; + + if (!cubb_debugfs_ok) + return 0; + mutex_lock(&cubb_lock); + for (idx = 0; idx < CUBB2_PEND_MAX; idx++) { + if (cubb_pend[idx].valid) { + p = &cubb_pend[idx]; + break; + } + } + if (!p) { + mutex_unlock(&cubb_lock); + return 0; + } + in = p->in; + in_bytes = p->job.in_bytes; + if (!in || off >= in_bytes) { + mutex_unlock(&cubb_lock); + return 0; + } + n = min_t(size_t, count, in_bytes - off); + rc = copy_to_user(ubuf, in + off, n); + mutex_unlock(&cubb_lock); + if (rc) + return -EFAULT; + *ppos = off + n; + return n; +} + +/* + * Accumulate the daemon's output blob. ppos 0 starts a new blob; growth is + * amortised (double until the record fits, capped at CUBB2_OUT_MAX). + */ +static ssize_t cubb_out_write(struct file *f, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + u64 off = *ppos; + u64 end; + int rc = 0; + + if (!cubb_debugfs_ok) + return -ENODEV; + if (!count) + return 0; + if (*ppos < 0) + return -EINVAL; + end = off + count; + if (end > CUBB2_OUT_MAX) + return -E2BIG; + + mutex_lock(&cubb_lock); + if (off == 0) + cubb_out_len = 0; + if (end > cubb_out_alloc) { + unsigned int want = cubb_out_alloc ? cubb_out_alloc : 4096; + u8 *nb; + + while (want < end && want < CUBB2_OUT_MAX) + want *= 2; + if (want > CUBB2_OUT_MAX) + want = CUBB2_OUT_MAX; + nb = vmalloc(want); + if (!nb) { + mutex_unlock(&cubb_lock); + return -ENOMEM; + } + if (cubb_out_len) + memcpy(nb, cubb_out_buf, cubb_out_len); + vfree(cubb_out_buf); + cubb_out_buf = nb; + cubb_out_alloc = want; + } + rc = copy_from_user(cubb_out_buf + off, ubuf, count); + if (!rc && end > cubb_out_len) + cubb_out_len = end; + mutex_unlock(&cubb_lock); + if (rc) + return -EFAULT; + *ppos = end; + return count; +} + +/* Accumulate a 256-byte CBD2 record (single write or ppos-chunked). */ +static u8 cubb_done_stage[CUBB2_RECORD_SIZE]; +static size_t cubb_done_stage_len; + +static ssize_t cubb_done_write(struct file *f, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + /* + * Accept both pwrite(fd, rec, 256, 0) and a long-lived fd writing + * records back to back: a write starting on a record boundary starts + * a fresh record. + */ + size_t off = (size_t)(*ppos % CUBB2_RECORD_SIZE); + unsigned long flags; + int rc; + + if (!cubb_debugfs_ok) + return -ENODEV; + if (!count) + return 0; + if (count > CUBB2_RECORD_SIZE - off) + count = CUBB2_RECORD_SIZE - off; + + mutex_lock(&cubb_lock); + if (off == 0) + cubb_done_stage_len = 0; + rc = copy_from_user(cubb_done_stage + off, ubuf, count); + if (rc) { + mutex_unlock(&cubb_lock); + return -EFAULT; + } + if (off + count > cubb_done_stage_len) + cubb_done_stage_len = off + count; + + if (cubb_done_stage_len >= CUBB2_RECORD_SIZE) { + struct cubb2_done *d = (struct cubb2_done *)cubb_done_stage; + + if (d->magic == CUBB2_DONE_MAGIC && + d->version == CUBB2_VERSION) { + spin_lock_irqsave(&cubb_done_lock, flags); + if (cubb_done_head - cubb_done_tail < + CUBB2_DONE_RING) { + cubb_done_ring[cubb_done_head % + CUBB2_DONE_RING] = *d; + cubb_done_head++; + } else { + cubb_dropped++; + } + spin_unlock_irqrestore(&cubb_done_lock, flags); + } else { + cubb_malformed++; + } + memset(cubb_done_stage, 0, sizeof(cubb_done_stage)); + cubb_done_stage_len = 0; + } + mutex_unlock(&cubb_lock); + *ppos = off + count; + return count; +} + +static ssize_t cubb_status_read(struct file *f, char __user *ubuf, + size_t count, loff_t *ppos) +{ + char buf[256]; + int len; + + len = scnprintf(buf, sizeof(buf), + "jobs=%llu completed=%llu failed=%llu last_seq=%llu last_err=%d dropped=%llu stale=%llu malformed=%llu dedup=%llu\n", + cubb_jobs, cubb_completed, cubb_failed, + cubb_last_seq, cubb_last_err, cubb_dropped, + cubb_stale, cubb_malformed, cubb_dedup_hits); + return simple_read_from_buffer(ubuf, count, ppos, buf, len); +} + +static const struct file_operations cubb_job_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = cubb_job_read, + .llseek = default_llseek, +}; + +static const struct file_operations cubb_in_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = cubb_in_read, + .llseek = default_llseek, +}; + +static const struct file_operations cubb_out_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = cubb_out_write, + .llseek = default_llseek, +}; + +static const struct file_operations cubb_done_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = cubb_done_write, + .llseek = default_llseek, +}; + +static const struct file_operations cubb_status_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = cubb_status_read, + .llseek = default_llseek, +}; + +/* ------------------------------------------------------------------ */ +/* lifecycle */ +/* ------------------------------------------------------------------ */ + +int gsp_cuda_bridge_init(void) +{ + cubb_debugfs_init(); + return cubb_debugfs_ok ? 0 : -ENODEV; +} + +void gsp_cuda_bridge_fini(void) +{ + int i; + + if (cubb_out_buf) { + vfree(cubb_out_buf); + cubb_out_buf = NULL; + cubb_out_alloc = 0; + cubb_out_len = 0; + } + mutex_lock(&cubb_lock); + for (i = 0; i < CUBB2_PEND_MAX; i++) { + vfree(cubb_pend[i].in); + cubb_pend[i].in = NULL; + cubb_pend[i].valid = false; + } + mutex_unlock(&cubb_lock); + cubb_req_head = cubb_req_tail = 0; + cubb_done_head = cubb_done_tail = 0; + cubb_debugfs_fini(); +} diff --git a/kernel/gsp_phys_booter.c b/kernel/gsp_phys_booter.c index 3d1ccea..bead218 100644 --- a/kernel/gsp_phys_booter.c +++ b/kernel/gsp_phys_booter.c @@ -440,8 +440,34 @@ bool gsp_phys_boot_requested(void) * carries both PA and VA in one entry, on the non-KMD V5 channel. */ #define GSP_PHYS_RMCALL_GR_PROMOTE_GUEST_SET 19u +/* + * Mode 20 (2026-10-02, untested): the mode-6 no-entry hVirtMemory promote on + * the V6 KMD-managed channel (SKIP_CTXBUFFER_ALLOC). Mode 6 was refused with + * NV_ERR_INVALID_ARGUMENT (0x1f) on the V5 channel because the server's + * client-promote path rejects the hVirtMemory (pVirtMemDesc) branch unless the + * channel is KMD-managed; V6 sets exactly that flag. The object alloc stays on + * the default external VAS, so the shared-management ctx-buffer map path that + * wedged grsvas2/grsvas3 never runs. The promote asks the server to create the + * virtual-context memdesc/vaList entry for the context window itself, after + * which the KMD commit path can validate ENG_CTX_VALID. + */ +#define GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT 20u +/* + * Mode 21 (2026-10-02, untested): the mode-7 no-entry bare (virtAddress, size) + * promote on the V6 KMD-managed channel - the counterpart of mode 20 without + * the hVirtMemory object. + */ +#define GSP_PHYS_RMCALL_GR_PROMOTE_KMD_BARE_VA 21u +/* + * Mode 22 (2026-10-02, untested): install the physical context memdesc with + * GPU_INITIALIZE_CTX (accepted alone in modes 10/11), then send mode 20's + * no-entry hVirtMemory promote. If mode 20's plain 0x40 was the promote's + * physical-memdesc precondition, this combination satisfies it before the + * virtual-memdesc promote. + */ +#define GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT 22u #define GSP_PHYS_RMCALL_GR_PROMOTE_MAX \ - (GSP_PHYS_RMCALL_GR_PROMOTE_GUEST_SET) + (GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT) /* * Work-submit token notifier slot. The default index is @@ -483,9 +509,17 @@ bool gsp_phys_boot_requested(void) */ #define GSP_PHYS_RMCALL_VAS_INDEX_GPU_NEW 0u #define GSP_PHYS_RMCALL_VAS_FLAG_IS_EXTERNALLY_OWNED BIT(3) +#define GSP_PHYS_RMCALL_VAS_FLAG_SHARED_MANAGEMENT BIT(2) #define GSP_PHYS_RMCALL_VAS_FLAG_ALLOW_ZERO_ADDRESS BIT(8) #define GSP_PHYS_RMCALL_VAS_SIZE 0x10000000ULL /* + * Shared-management VAS size: the normal KMD/UVM client passes vaBase=0 and + * rm_va_size = 128 TB on Volta (uvm_volta.c:48-49); the split-VAS server-RM + * carveout math (gpu_vaspace.c:440-470) underflows with a small (256 MB) + * size and asserts the alloc with NV_ERR_INVALID_ARGUMENT. + */ +#define GSP_PHYS_RMCALL_VAS_SIZE_SHARED 0x200000000000ULL +/* * Turing/TU104 uses the GP10X GMMU root format: the root level has 2 index * bits, i.e. exactly 4 entries covering 2^47 bytes each, and the server caps * the external root at vaLimitMax = 2^49-1 with vaLimitNew = @@ -2969,6 +3003,16 @@ static void gsp_phys_fault_corroborate(struct gsp_phys_fault_info *out); #define GSP_PHYS_NOCAT_OFF_DIAGBUF 180 #define GSP_PHYS_NOCAT_MIN_PAYLOAD GSP_PHYS_NOCAT_OFF_DIAGBUF #define GSP_PHYS_NOCAT_FULL_PAYLOAD 1208 +/* + * Assert diagnostic buffer (RM_NOCAT_ASSERT_DIAG_BUFFER, g_journal_nvoc.h): + * ten u32 call-stack entries (each already relative to the NOCAT load + * address) followed by a u32 repeat count. The stack entries are the exact + * call chain of the assert, so a dump makes the failing path nameable + * offline without another run. + */ +#define GSP_PHYS_NOCAT_DIAG_STACK 10 +#define GSP_PHYS_NOCAT_OFF_DIAGCOUNT (GSP_PHYS_NOCAT_OFF_DIAGBUF + 4 * GSP_PHYS_NOCAT_DIAG_STACK) +#define GSP_PHYS_NOCAT_DIAG_NEEDED (GSP_PHYS_NOCAT_OFF_DIAGCOUNT + 4) /* Local re-declaration of the reference record, only for the offset asserts. */ struct gsp_phys_nocat_journal_record { @@ -3116,10 +3160,33 @@ static void gsp_phys_nocat_record(const struct gsp_phys_rx_record *rec) gsp_phys_nocat_accumulate(rec_type, error_code, subsystem, bugcheck, source, engine, rec->seq_num); - if (gsp_phys_nocat_param) + if (gsp_phys_nocat_param) { + u32 diag_len = 0; + u32 count = 0; + u32 i; + char stack[GSP_PHYS_NOCAT_DIAG_STACK * 12]; + size_t pos = 0; + pr_info("vgsp-nocat: rec seq=%u recType=%u errorCode=0x%llx subsystem=0x%x bugcheck=0x%x payload=%u source=\"%s\" faultingEngine=\"%s\"\n", rec->seq_num, rec_type, error_code, subsystem, bugcheck, len, source, engine); + if (len >= GSP_PHYS_NOCAT_OFF_DIAGBUFLEN + 4) + diag_len = get_unaligned_le32(p + + GSP_PHYS_NOCAT_OFF_DIAGBUFLEN); + if (rec_type == 5 && diag_len >= 4 * GSP_PHYS_NOCAT_DIAG_STACK + 4 && + len >= GSP_PHYS_NOCAT_DIAG_NEEDED) { + for (i = 0; i < GSP_PHYS_NOCAT_DIAG_STACK; i++) { + u32 v = get_unaligned_le32(p + + GSP_PHYS_NOCAT_OFF_DIAGBUF + 4 * i); + pos += scnprintf(stack + pos, sizeof(stack) - pos, + "%s0x%x", i ? "," : "", v); + } + count = get_unaligned_le32(p + + GSP_PHYS_NOCAT_OFF_DIAGCOUNT); + pr_info("vgsp-nocat: rec seq=%u assertCallStack count=%u [%s] (offsets from load base 0x4000000)\n", + rec->seq_num, count, stack); + } + } } static void gsp_phys_nocat_summary_dump(void) @@ -6732,6 +6799,12 @@ static const char *gsp_phys_rmcall_gr_promote_name(void) return "mode 8 shape plus a client FERMI_CONTEXT_SHARE_A (hContextShare) that allocates the GraphicsSubcontext"; case GSP_PHYS_RMCALL_GR_PROMOTE_GUEST_SET: return "stock guest's 9-entry promote set (MAIN 2 MB PA+VA, PATCH/global/FECS/priv maps) on V5"; + case GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT: + return "KMD-managed channel (V6 SKIP_CTXBUFFER_ALLOC) + no-entry hVirtMemory promote"; + case GSP_PHYS_RMCALL_GR_PROMOTE_KMD_BARE_VA: + return "KMD-managed channel (V6 SKIP_CTXBUFFER_ALLOC) + no-entry bare VA+size promote"; + case GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT: + return "KMD-managed channel (V6 SKIP_CTXBUFFER_ALLOC) + GPU_INITIALIZE_CTX then no-entry hVirtMemory promote"; default: return "off (PROMOTE_CTX never sent)"; } @@ -6907,7 +6980,13 @@ static int gsp_phys_rmcall_alloc_channel(void __iomem *bar0) boot_stage.rmcall_gr_promote == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_PM || boot_stage.rmcall_gr_promote == - GSP_PHYS_RMCALL_GR_PROMOTE_CTXSHARE)) { + GSP_PHYS_RMCALL_GR_PROMOTE_CTXSHARE || + boot_stage.rmcall_gr_promote == + GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT || + boot_stage.rmcall_gr_promote == + GSP_PHYS_RMCALL_GR_PROMOTE_KMD_BARE_VA || + boot_stage.rmcall_gr_promote == + GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT)) { vars = &gsp_phys_rmcall_channel_variant_skipctx; attempts = 1; } @@ -7843,7 +7922,10 @@ static int gsp_phys_rmcall_gr_ctx_promote(void __iomem *bar0) mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD || mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_FB || mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_PM || - mode == GSP_PHYS_RMCALL_GR_PROMOTE_CTXSHARE) && + mode == GSP_PHYS_RMCALL_GR_PROMOTE_CTXSHARE || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_BARE_VA || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT) && boot_stage.rmcall_channel_variant != gsp_phys_rmcall_channel_variant_skipctx.id) { pr_err("vgsp-boot: rm-call: mode %u requires the SKIP_CTXBUFFER_ALLOC channel (V%u %s); winning variant is V%u; no PROMOTE_CTX is sent and the ladder aborts with no retry\n", @@ -7981,17 +8063,21 @@ static int gsp_phys_rmcall_gr_ctx_promote(void __iomem *bar0) } boot_stage.rmcall_gr_promote_step = 2; } else if (mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT || - mode == GSP_PHYS_RMCALL_GR_PROMOTE_BARE_VA) { + mode == GSP_PHYS_RMCALL_GR_PROMOTE_BARE_VA || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_BARE_VA) { /* - * Modes 6/7: the documented no-entry branches. Mode 6 first - * reserves the context window with an NV01_MEMORY_VIRTUAL - * object on the ladder VAS and passes it as hVirtMemory + * Modes 6/7/20: the documented no-entry branches. Modes 6/20 + * first reserve the context window with an NV01_MEMORY_VIRTUAL + * object on the ladder VAS and pass it as hVirtMemory * (hClient is its owner); mode 7 sends the bare * (virtAddress, size) pair with no hVirtMemory. entryCount=0 - * in both, so the server creates the memdesc/vaList entry - * instead of resolving one of our raw-PTE entries. + * in all, so the server creates the memdesc/vaList entry + * instead of resolving one of our raw-PTE entries. Mode 20 is + * the same shape as mode 6 but on the V6 KMD-managed channel. */ - if (mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT) { + if (mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT) { ret = gsp_phys_rmcall_alloc_gr_hvirt(bar0); if (ret) { kvfree(p); @@ -8009,11 +8095,13 @@ static int gsp_phys_rmcall_gr_ctx_promote(void __iomem *bar0) GSP_PHYS_RMCALL_CTRL_GPU_PROMOTE_CTX, p, sizeof(*p), 24, NULL, 0, &rpc_result, &status, - mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT ? + (mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT) ? "GPU_PROMOTE_CTX (hVirtMemory)" : "GPU_PROMOTE_CTX (bare VA+size)"); pr_info("vgsp-boot: rm-call: PROMOTE_CTX %s: entryCount=0 hVirtMemory=0x%x virtAddress=0x%llx size=0x%x status=0x%x rpc_result=0x%x (hClient=0x%x hObject=0x%x hChanClient=0x%x engineType=0x%x gr_promote_param=%u)\n", - mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT ? + (mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT) ? "hVirtMemory" : "bare VA+size", le32_to_cpu(p->h_virt_memory), boot_stage.rmcall_ctx_va, boot_stage.rmcall_ctx_size, @@ -8022,7 +8110,8 @@ static int gsp_phys_rmcall_gr_ctx_promote(void __iomem *bar0) GSP_PHYS_RMCALL_ENGINE_GR0, mode); if (ret) { pr_err("vgsp-boot: rm-call: PROMOTE_CTX %s failed (status=0x%x rpc_result=0x%x, %s, ret=%d); the GR context is not committed and the ladder aborts before BIND with no retry\n", - mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT ? + (mode == GSP_PHYS_RMCALL_GR_PROMOTE_HVIRT || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_HVIRT) ? "hVirtMemory" : "bare VA+size", status, rpc_result, gsp_phys_rmcall_status_str(status), ret); @@ -8063,9 +8152,10 @@ static int gsp_phys_rmcall_gr_ctx_promote(void __iomem *bar0) } boot_stage.rmcall_gr_promote_step = 1; } else if (mode == GSP_PHYS_RMCALL_GR_PROMOTE_OLDIFACE_VA || - mode == GSP_PHYS_RMCALL_GR_PROMOTE_OLDIFACE_FB) { + mode == GSP_PHYS_RMCALL_GR_PROMOTE_OLDIFACE_FB || + mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT) { /* - * Modes 10/11: the old-interface client-managed flow. Step 1 + * Modes 10/11/22: the old-interface client-managed flow. Step 1 * sends GPU_INITIALIZE_CTX (0x2080012d) with the physical * context buffer, which installs the PA/size/physAttr * memdesc on the GraphicsContext (grctxInitializeAPI) that @@ -8076,7 +8166,8 @@ static int gsp_phys_rmcall_gr_ctx_promote(void __iomem *bar0) * entryCount=0 and the bare (virtAddress, size) pair; the * server builds the virtual memdesc itself and, on the V6 * KMD-managed channel, binds it without the VA/bookkeeping - * resolve. + * resolve. Mode 22 is mode 10's step 1 followed by mode 20's + * hVirtMemory no-entry promote instead of the bare pair. */ struct gsp_phys_rmcall_init_ctx_params *ic; u32 ic_phys_attr = (mode == @@ -8126,6 +8217,16 @@ static int gsp_phys_rmcall_gr_ctx_promote(void __iomem *bar0) p->virt_address = cpu_to_le64(boot_stage.rmcall_ctx_va); p->size = cpu_to_le64(boot_stage.rmcall_ctx_size); p->entry_count = cpu_to_le32(0); + if (mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT) { + ret = gsp_phys_rmcall_alloc_gr_hvirt(bar0); + if (ret) { + kvfree(p); + return ret; + } + p->h_client = cpu_to_le32(boot_stage.rmcall_client); + p->h_virt_memory = + cpu_to_le32(boot_stage.rmcall_gr_hvirt); + } rpc_result = 0; status = 0; ret = gsp_phys_rmcall_control(bar0, @@ -8133,14 +8234,21 @@ static int gsp_phys_rmcall_gr_ctx_promote(void __iomem *bar0) GSP_PHYS_RMCALL_CTRL_GPU_PROMOTE_CTX, p, sizeof(*p), 24, NULL, 0, &rpc_result, &status, + (mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT) ? + "GPU_PROMOTE_CTX (hVirtMemory after init)" : "GPU_PROMOTE_CTX (KMD bare VA+size after init)"); - pr_info("vgsp-boot: rm-call: PROMOTE_CTX bare VA+size after init: entryCount=0 virtAddress=0x%llx size=0x%x status=0x%x rpc_result=0x%x (hObject=0x%x hChanClient=0x%x engineType=0x%x gr_promote_param=%u)\n", + pr_info("vgsp-boot: rm-call: PROMOTE_CTX %s: entryCount=0 hVirtMemory=0x%x virtAddress=0x%llx size=0x%x status=0x%x rpc_result=0x%x (hObject=0x%x hChanClient=0x%x engineType=0x%x gr_promote_param=%u)\n", + (mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT) ? + "hVirtMemory after init" : "bare VA+size after init", + le32_to_cpu(p->h_virt_memory), boot_stage.rmcall_ctx_va, boot_stage.rmcall_ctx_size, status, rpc_result, boot_stage.rmcall_channel, boot_stage.rmcall_client, GSP_PHYS_RMCALL_ENGINE_GR0, mode); if (ret) { - pr_err("vgsp-boot: rm-call: PROMOTE_CTX bare VA+size failed (status=0x%x rpc_result=0x%x, %s, ret=%d); last completed promote step: INITIALIZE_CTX accepted (1/2); the GR context is not committed and the ladder aborts before BIND with no retry\n", + pr_err("vgsp-boot: rm-call: PROMOTE_CTX %s failed (status=0x%x rpc_result=0x%x, %s, ret=%d); last completed promote step: INITIALIZE_CTX accepted (1/2); the GR context is not committed and the ladder aborts before BIND with no retry\n", + (mode == GSP_PHYS_RMCALL_GR_PROMOTE_KMD_INIT_HVIRT) ? + "hVirtMemory after init" : "bare VA+size", status, rpc_result, gsp_phys_rmcall_status_str(status), ret); kvfree(p); @@ -8632,17 +8740,41 @@ static int gsp_phys_rmcall_stage3(void __iomem *bar0) gsp_phys_rmcall_gr_promote_snapshot(); /* - * The TSG below carries this externally-owned FERMI_VASPACE_A, whose - * root page directory is committed from the zeroed VASPDB chunk before - * any channel alloc (the instance-block setup programs that base and - * fails 0x40 when it is NULL, kern_gmmu_gp100.c:150-152). Either call - * aborts the ladder: the variant walk cannot succeed without them. + * The TSG below carries this explicitly allocated FERMI_VASPACE_A, + * whose root page directory is committed from the zeroed VASPDB chunk + * before any channel alloc (the instance-block setup programs that + * base and fails 0x40 when it is NULL, kern_gmmu_gp100.c:150-152). + * Either call aborts the ladder: the variant walk cannot succeed + * without them. + * + * History (2026-10-02): the VAS was allocated IS_EXTERNALLY_OWNED to + * mirror the guest's UVM-owned address space. That flag tells the RM + * that the client owns all page tables and VA/context commitment, so + * the server never books the GR engine-context VA itself and every + * promote either asserts ("Externally owned object not found", + * kchannelMapEngineCtxBuf) or silently leaves the channel + * CTXNOTVALID. The normal KMD client instead allocates its VAS with + * SHARED_MANAGEMENT (nv_gpu_ops.c:2495): the client provides the root + * PDB (SET_PAGE_DIRECTORY still legal, gpu_vaspace.c:3108) while the + * RM manages the lower page tables and maps/commits engine contexts. + * gsp_phys_rmcall_vas_shared=1 selects that variant; the default 0 + * keeps the byte-identical external path. */ memset(&vas, 0, sizeof(vas)); vas.index = cpu_to_le32(GSP_PHYS_RMCALL_VAS_INDEX_GPU_NEW); - vas.flags = cpu_to_le32(GSP_PHYS_RMCALL_VAS_FLAG_IS_EXTERNALLY_OWNED | - GSP_PHYS_RMCALL_VAS_FLAG_ALLOW_ZERO_ADDRESS); - vas.va_size = cpu_to_le64(GSP_PHYS_RMCALL_VAS_SIZE); + vas.flags = cpu_to_le32(gsp_phys_rmcall_vas_shared_value() ? + (GSP_PHYS_RMCALL_VAS_FLAG_SHARED_MANAGEMENT | + GSP_PHYS_RMCALL_VAS_FLAG_ALLOW_ZERO_ADDRESS) : + (GSP_PHYS_RMCALL_VAS_FLAG_IS_EXTERNALLY_OWNED | + GSP_PHYS_RMCALL_VAS_FLAG_ALLOW_ZERO_ADDRESS)); + vas.va_size = cpu_to_le64(gsp_phys_rmcall_vas_shared_value() ? + GSP_PHYS_RMCALL_VAS_SIZE_SHARED : + GSP_PHYS_RMCALL_VAS_SIZE); + pr_info("vgsp-boot: rm-call: FERMI_VASPACE_A flags=0x%x va_size=0x%llx (%s)\n", + le32_to_cpu(vas.flags), le64_to_cpu(vas.va_size), + gsp_phys_rmcall_vas_shared_value() ? + "shared management (RM owns page tables/commit)" : + "externally owned (client owns page tables/commit)"); ret = gsp_phys_rmcall_alloc(bar0, boot_stage.rmcall_client, boot_stage.rmcall_device, GSP_PHYS_RMCALL_HANDLE_VAS, diff --git a/kernel/gsp_phys_gpu.c b/kernel/gsp_phys_gpu.c index 1fa1670..91f7a77 100644 --- a/kernel/gsp_phys_gpu.c +++ b/kernel/gsp_phys_gpu.c @@ -424,8 +424,8 @@ static int gsp_phys_rmcall_gr_promote_set(const char *val, return ret; if (gsp_phys_rmcall_gr_promote_param < 0 || - gsp_phys_rmcall_gr_promote_param > 19) { - pr_warn("vgsp: gsp_phys_rmcall_gr_promote=%d is outside 0..19; clamping to 0 (no PROMOTE_CTX)\n", + gsp_phys_rmcall_gr_promote_param > 22) { + pr_warn("vgsp: gsp_phys_rmcall_gr_promote=%d is outside 0..22; clamping to 0 (no PROMOTE_CTX)\n", gsp_phys_rmcall_gr_promote_param); gsp_phys_rmcall_gr_promote_param = 0; } @@ -444,7 +444,7 @@ module_param_cb(gsp_phys_rmcall_gr_promote, &gsp_phys_rmcall_gr_promote_ops, &gsp_phys_rmcall_gr_promote_param, 0644); MODULE_PARM_DESC(gsp_phys_rmcall_gr_promote, - "GR context-promote bisection (default 0: never send PROMOTE_CTX; 1: single entry init+nonmapped=0 size=raw+subctx, the p6h1b shape; 2: two-phase PA-only nonmapped then VA-only with the same fields; 3: single entry size=raw; 4: two-phase with a zero-field VA-only step; 5: batched entryCount=2 PA-only init + zero-field VA-only; 6: no-entry hVirtMemory, NV01_MEMORY_VIRTUAL (class 0x70) on the VAS at the context window + PROMOTE_CTX entryCount=0; 7: no-entry bare virtAddress+size, entryCount=0; 8: single MAIN entry like 1 on the KMD-managed channel (V6 SKIP_CTXBUFFER_ALLOC), server creates the virtual context memdesc itself; 9: same V6 channel with the stock client's zero-field VA-only MAIN entry, bInitialize=0; 10: same V6 channel, GPU_INITIALIZE_CTX (0x2080012d) installs the physical context memdesc then PROMOTE_CTX entryCount=0 bare virtAddress+size; 11: mode 10 with a server FBMEM context buffer, VIDMEM physAttr 0x4; 12: V5 UVM/vGPU-faithful PA-only MAIN entry (bInitialize=1 bNonmapped=1, no VA) and continue to BIND/SCHEDULE/submit; 13: mode 12 plus a PATCH PA-only promote first, sized from GET_ENGINE_CONTEXT_PROPERTIES engineId 0x10; 14: KMD channel (V6) + PATCH PA-only then the single-entry MAIN init+VA (the flow that books the context VA and commits ENG_CTX_VALID); 15: mode 14 with a server FBMEM MAIN context buffer and VIDMEM physAttr 0x4; 16: mode 14 plus a PM ctxsw PA-only promote first; 17: no client PROMOTE_CTX, paired with gsp_phys_srv_ctxbuf=1 so the GSP-RM server-manages and commits the context; 18: mode 8 shape plus a client FERMI_CONTEXT_SHARE_A on the TSG (hContextShare) whose construct allocates the GraphicsSubcontext; 19: the stock guest's exact 9-entry promote set (MAIN 2 MB PA+VA, PATCH, global buffers, FECS event, private access maps) on V5); values outside 0..19 clamp to 0 with a warning"); + "GR context-promote bisection (default 0: never send PROMOTE_CTX; 1: single entry init+nonmapped=0 size=raw+subctx, the p6h1b shape; 2: two-phase PA-only nonmapped then VA-only with the same fields; 3: single entry size=raw; 4: two-phase with a zero-field VA-only step; 5: batched entryCount=2 PA-only init + zero-field VA-only; 6: no-entry hVirtMemory, NV01_MEMORY_VIRTUAL (class 0x70) on the VAS at the context window + PROMOTE_CTX entryCount=0; 7: no-entry bare virtAddress+size, entryCount=0; 8: single MAIN entry like 1 on the KMD-managed channel (V6 SKIP_CTXBUFFER_ALLOC), server creates the virtual context memdesc itself; 9: same V6 channel with the stock client's zero-field VA-only MAIN entry, bInitialize=0; 10: same V6 channel, GPU_INITIALIZE_CTX (0x2080012d) installs the physical context memdesc then PROMOTE_CTX entryCount=0 bare virtAddress+size; 11: mode 10 with a server FBMEM context buffer, VIDMEM physAttr 0x4; 12: V5 UVM/vGPU-faithful PA-only MAIN entry (bInitialize=1 bNonmapped=1, no VA) and continue to BIND/SCHEDULE/submit; 13: mode 12 plus a PATCH PA-only promote first, sized from GET_ENGINE_CONTEXT_PROPERTIES engineId 0x10; 14: KMD channel (V6) + PATCH PA-only then the single-entry MAIN init+VA (the flow that books the context VA and commits ENG_CTX_VALID); 15: mode 14 with a server FBMEM MAIN context buffer and VIDMEM physAttr 0x4; 16: mode 14 plus a PM ctxsw PA-only promote first; 17: no client PROMOTE_CTX, paired with gsp_phys_srv_ctxbuf=1 so the GSP-RM server-manages and commits the context; 18: mode 8 shape plus a client FERMI_CONTEXT_SHARE_A on the TSG (hContextShare) whose construct allocates the GraphicsSubcontext; 19: the stock guest's exact 9-entry promote set (MAIN 2 MB PA+VA, PATCH, global buffers, FECS event, private access maps) on V5; 20: mode 6's no-entry hVirtMemory promote on the KMD-managed V6 channel (SKIP_CTXBUFFER_ALLOC), untested - the combination the V5 refusal (0x1f) points at); values outside 0..20 clamp to 0 with a warning"); int gsp_phys_rmcall_gr_promote_value(void) { @@ -453,6 +453,34 @@ int gsp_phys_rmcall_gr_promote_value(void) EXPORT_SYMBOL_GPL(gsp_phys_rmcall_gr_promote_value); /* + * Ladder VAS ownership selector (gsp_phys_rmcall_vas_shared; default 0). + * The H1 GR promote wall is a direct consequence of the ladder's + * externally-owned FERMI_VASPACE_A: with IS_EXTERNALLY_OWNED the RM treats + * the client (UVM) as the owner of all page tables, VA bookings and engine + * context commitment, so the server never creates the context VA booking + * itself and the kickoff either asserts in kchannelMapEngineCtxBuf + * ("Externally owned object not found") or leaves the channel CTXNOTVALID. + * The normal KMD client instead allocates the VAS with SHARED_MANAGEMENT + * (nv_gpu_ops.c:2495): SET_PAGE_DIRECTORY is still legal (gpu_vaspace.c: + * 3108) and migrates the ladder's committed root PDB into the RM page + * walker, while the RM allocates the lower tables and maps/commits engine + * contexts. 1 selects the shared-management VAS; 0 keeps the byte-identical + * external path used by every previous cycle. Read once, at the stage-3 + * VAS alloc; a mid-session change is ignored (the VAS already exists). + */ +static bool gsp_phys_rmcall_vas_shared; +module_param_named(gsp_phys_rmcall_vas_shared, gsp_phys_rmcall_vas_shared, + bool, 0644); +MODULE_PARM_DESC(gsp_phys_rmcall_vas_shared, + "Ladder FERMI_VASPACE_A ownership (default 0: IS_EXTERNALLY_OWNED, the client owns page tables and VA/context commitment - the state under which every GR promote failed; 1: SHARED_MANAGEMENT, the client supplies the root PDB but the RM owns the lower page tables and maps/commits engine contexts, like the normal KMD client)"); + +bool gsp_phys_rmcall_vas_shared_value(void) +{ + return gsp_phys_rmcall_vas_shared; +} +EXPORT_SYMBOL_GPL(gsp_phys_rmcall_vas_shared_value); + +/* * One-way power latch (default). Re-enabling runtime PM and D3cold after a * claim lets the board power-gate and the host hard-freeze on resume * (observed 2026-10-01); keep the pre-claim D0 policy instead and require diff --git a/kernel/gsp_phys_gpu.h b/kernel/gsp_phys_gpu.h index 7d9b695..74e3103 100644 --- a/kernel/gsp_phys_gpu.h +++ b/kernel/gsp_phys_gpu.h @@ -210,6 +210,19 @@ int gsp_phys_rmcall_engine_value(void); int gsp_phys_rmcall_gr_promote_value(void); /* + * Ladder VAS ownership selector (gsp_phys_rmcall_vas_shared; default 0). + * 0 = FERMI_VASPACE_A is allocated IS_EXTERNALLY_OWNED (historical ladder + * behaviour: the client owns page tables, VA bookings and engine-context + * commitment, which is why every host GR promote failed or wedged). + * 1 = SHARED_MANAGEMENT: the ladder still supplies and commits its root PDB + * via SET_PAGE_DIRECTORY (legal for shared management, gpu_vaspace.c:3108) + * while the RM owns the lower page tables and maps/commits engine contexts + * itself, like the normal KMD client (nv_gpu_ops.c:2495). Read at the + * stage-3 VAS alloc; a mid-session change is ignored. + */ +bool gsp_phys_rmcall_vas_shared_value(void); + +/* * Translate a guest GPA to the pinned host PA (gsp_phys_guest_mem ladder * support). The mdev is registered with vfio_register_emulated_iommu_dev(): * its VFIO type-1 container keeps the guest DMA mappings privately, so diff --git a/kernel/gsp_sema_service.c b/kernel/gsp_sema_service.c index ae74881..8b8708d 100644 --- a/kernel/gsp_sema_service.c +++ b/kernel/gsp_sema_service.c @@ -198,6 +198,9 @@ struct svc_channel { * (registered via the sema_counter debug param); * 0 = not registered */ u32 counter_last; /* last value published to counter_pa */ + u64 pushpg_va; /* last push page logged as pin-vs-window + * divergent (rate limit: one line per page) */ + u8 pushpg_n; /* pushpg lines emitted for this channel (cap) */ u32 pending_rel_pay; /* release payload decoded while counter_pa * was still unknown (the injected pin for the * counter page can land a moment after the @@ -246,6 +249,10 @@ struct svc_pin { u64 va_page; u64 pa_page; int ap; /* SVC_APERTURE_* of the mapped page */ + u8 win; /* linear push-window guess (svc_pb_pair): the page + * was NOT read back, only its window base was. Push + * reads skip these and resolve each page themselves; + * every other path still uses them as before. */ }; struct svc_vaspace { @@ -405,6 +412,8 @@ struct sema_svc { u64 entry_log; /* per-push summary budget */ 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 */ + u64 pushpg_fallback; /* injected pin rejected by content */ u64 gpfifo_skipped; /* stale entries consumed */ u64 releases_seen; u64 releases_applied; @@ -415,6 +424,12 @@ struct sema_svc { u64 releases_stale; /* payload <= current */ u64 releases_jumped; /* applied with gap > 1 */ u64 releases_faked; /* skipped-entry fallback */ + u64 releases_forwarded; /* exact slot known, not translatable: + * expectation advanced, write queued */ + u64 gpfifo_held; /* passes an entry was left pending + * for a translation/grace window */ + u64 gpfifo_starved; /* entries consumed without a + * deliverable exact slot */ u64 counter_rel_applied; /* releases to registered counters */ u64 counter_rel_dbg; /* rel-apply diagnostic budget */ u64 counter_pub_dbg; /* counter-publish diagnostic budget */ @@ -480,6 +495,7 @@ struct sema_svc { u64 fwd_hold_engaged; u64 fwd_hold_timeouts; u64 fwd_hold_canceled; + u64 rel_hold_direct; /* decoded releases applied without a forwarded copy */ /* * P6c completion relay (gsp_phys_relay=1 only): relay selections armed * on a deferred guest request (fwd_relay_requested), relays whose @@ -568,6 +584,19 @@ static int svc_write(struct nv_vgpu_device *vgpu, u64 gpa, void *buf, size_t len #define SVC_APERTURE_FB 2 /* + * Aperture selectors are codes (1=SYS, 2=FB), never booleans. The idiom + * `ap ? SVC_APERTURE_FB : SVC_APERTURE_SYS` turns a valid SYS result (1) + * into an FB access into the 256 MiB BAR1 vidmem aperture: for a guest PA + * above the aperture the access fails, and the pool-semaphore read path then + * erased the verified cache entry it had just stored. Normalise every + * resolver output through this (0 also normalises to SYS). + */ +static inline u32 svc_ap_of(int ap) +{ + return ap == SVC_APERTURE_FB ? SVC_APERTURE_FB : SVC_APERTURE_SYS; +} + +/* * Aperture-aware access: channels describe their USERD/instance blocks with an * address-space tag, and "frame buffer" addresses are offsets into the * emulated vidmem (BAR1 backing), not guest RAM. @@ -1261,7 +1290,7 @@ static void svc_lat_store(u64 va, u64 addr, int ap) } static int pin_lookup_tab(const struct svc_pin *tab, u32 slots, u32 probe_max, - u64 va, u64 *pa, int *ap) + u64 va, u64 *pa, int *ap, bool skip_win) { u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1); u32 mask = slots - 1; @@ -1274,6 +1303,13 @@ static int pin_lookup_tab(const struct svc_pin *tab, u32 slots, u32 probe_max, if (!p->va_page) return -ENOENT; if (p->va_page == page) { + /* + * The push read path asks for validated pins only: + * a window guess must not shadow the per-page + * sources it would otherwise resolve from. + */ + if (skip_win && p->win) + return -ENOENT; *pa = p->pa_page | (va & (SEMA_PAGE_SIZE - 1)); *ap = p->ap; return 0; @@ -1283,7 +1319,7 @@ static int pin_lookup_tab(const struct svc_pin *tab, u32 slots, u32 probe_max, } static void pin_store_tab(struct svc_pin *tab, u32 slots, u32 probe_max, - u32 *n, u64 *drops, u64 va, u64 pa, int ap) + u32 *n, u64 *drops, u64 va, u64 pa, int ap, u8 win) { u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1); u32 mask = slots - 1; @@ -1299,6 +1335,7 @@ static void pin_store_tab(struct svc_pin *tab, u32 slots, u32 probe_max, p->va_page = page; p->pa_page = pa & ~(u64)(SEMA_PAGE_SIZE - 1); p->ap = ap; + p->win = win; return; } } @@ -1307,13 +1344,32 @@ static void pin_store_tab(struct svc_pin *tab, u32 slots, u32 probe_max, static int svc_pin_lookup(u64 va, u64 *pa, int *ap) { - return pin_lookup_tab(svc.pin, SVC_PIN_SLOTS, SVC_PIN_PROBE, va, pa, ap); + return pin_lookup_tab(svc.pin, SVC_PIN_SLOTS, SVC_PIN_PROBE, va, pa, ap, + false); +} + +/* + * Validated self-pins only (skip_win): what the push read path may use + * without shadowing a better per-page source. Window guesses are served by + * svc_pb_win_lookup() as the explicit last-resort fallback instead. + */ +static int svc_pin_lookup_valid(u64 va, u64 *pa, int *ap) +{ + return pin_lookup_tab(svc.pin, SVC_PIN_SLOTS, SVC_PIN_PROBE, va, pa, ap, + true); } static void svc_pin_store(u64 va, u64 pa, int ap) { pin_store_tab(svc.pin, SVC_PIN_SLOTS, SVC_PIN_PROBE, &svc.pin_n, - &svc.pin_drop, va, pa, ap); + &svc.pin_drop, va, pa, ap, 0); +} + +/* The linear window convention of svc_pb_pair: a guess, never validated. */ +static void svc_pin_store_win(u64 va, u64 pa, int ap) +{ + pin_store_tab(svc.pin, SVC_PIN_SLOTS, SVC_PIN_PROBE, &svc.pin_n, + &svc.pin_drop, va, pa, ap, 1); } /* @@ -1327,13 +1383,14 @@ static void svc_pin_store(u64 va, u64 pa, int ap) */ static int svc_ipin_lookup(u64 va, u64 *pa, int *ap) { - return pin_lookup_tab(svc.ipin, SVC_IPIN_SLOTS, SVC_IPIN_PROBE, va, pa, ap); + return pin_lookup_tab(svc.ipin, SVC_IPIN_SLOTS, SVC_IPIN_PROBE, va, pa, + ap, false); } static void svc_ipin_store(u64 va, u64 pa, int ap) { pin_store_tab(svc.ipin, SVC_IPIN_SLOTS, SVC_IPIN_PROBE, &svc.ipin_n, - &svc.ipin_drop, va, pa, ap); + &svc.ipin_drop, va, pa, ap, 0); } /* Any pinned translation: injected truth first, then self-discovered pins. */ @@ -1393,6 +1450,22 @@ static void svc_tr_note_failure(u64 page_va) tr_fail_next = (tr_fail_next + 1) % SVC_TR_FAIL_MAX; } +/* + * A successful resolve invalidates any earlier failure hold for the page: + * the hold exists to stop rescanning a VA that was not resolvable, and a + * fresh verified translation proves the guest has since mapped it. Without + * this, a page resolved through a pin/cache after a failed scan stayed + * blocked from the targeted scan for the rest of the 60 s window. + */ +static void svc_tr_clear_failure(u64 page_va) +{ + u32 i; + + for (i = 0; i < SVC_TR_FAIL_MAX; i++) + if (tr_failed[i].va == page_va) + tr_failed[i].va = 0; +} + static int svc_targeted_resolve(struct nv_vgpu_device *vgpu, u64 gpu_va, u64 *pa_out) { @@ -1475,6 +1548,7 @@ static int svc_targeted_resolve(struct nv_vgpu_device *vgpu, u64 gpu_va, *pa_out = pa | (gpu_va & (SEMA_PAGE_SIZE - 1)); svc_pin_store(page_va, pa, SVC_APERTURE_SYS); svc_lat_store(page_va, pa, SVC_APERTURE_SYS); + svc_tr_clear_failure(page_va); tr_va = 0; tr_scans++; pr_info("sema-svc: targeted resolve va=0x%llx -> pa=0x%llx (gpa=0x%llx off=%u, scan #%llu)\n", @@ -1608,34 +1682,369 @@ static int svc_resolve_va(struct nv_vgpu_device *vgpu, u64 va, u64 *addr, * resolvable stays resolvable for the boot. */ svc_pin_store(va, *addr, ap); - *aperture = (ap == SVC_APERTURE_FB) ? 1 : 0; + /* + * Raw aperture code, matching svc_lookup_trusted/svc_pool_pa_resolve: + * a boolean 0/1 encoding here invited truthiness conversions + * downstream that mapped SYS (1) to FB. + */ + *aperture = svc_ap_of(ap); return 0; } +/* + * CE channel-pool semaphores: the completion words UVM allocates for the CE + * channels (observed at 0x121000000 + 4*ce_index inside the CE window + * 0x120000000..0x121400000). They are sysmem objects the client polls + * directly, and the learned cache / content-scanned VA map can hold a + * stale/wrong PA for them: a release written through such a translation + * lands in a page the client never reads (observed: the pool semaphore + * stayed at 1 while the guest spun in + * uvm_page_table_range_vec_init -> uvm_tracker_wait). Pool releases resolve + * pin -> verified cache -> targeted rm_mem record, never the learned + * sources first; the GR/CE completion counters (0x2040xxxxx) are outside + * this range and keep their exact-slot eager logic untouched. + */ +#define SVC_POOL_SEM_VA_LO 0x121000000ULL +#define SVC_POOL_SEM_VA_HI 0x122000000ULL + +static inline bool svc_pool_sem_va(u64 va) +{ + return va >= SVC_POOL_SEM_VA_LO && va < SVC_POOL_SEM_VA_HI; +} + +/* + * Verified pool-semaphore translations, keyed by page VA. An entry is only + * created from an injected pin or from a targeted rm_mem resolve whose page + * was read back successfully; the learned cache and the discovered map are + * never cached here. Plain per-page state on the service thread, mirroring + * counter_pa / svc_cnt_der lifetime; a failed write or an unreadable cached + * page forgets the entry so the next pass re-resolves from the live record. + */ +#define SVC_POOL_PA_MAX 64 +#define SVC_POOL_PA_LOG_MAX 32 +#define SVC_POOL_PA_HIT_LOG_MAX 32 +#define SVC_POOL_WR_LOG_MAX 64 + +struct svc_pool_pa { + u64 va_page; + u64 pa_page; + int ap; +}; + +static struct svc_pool_pa svc_pool_pa[SVC_POOL_PA_MAX]; +static u32 svc_pool_pa_n; +static u32 svc_pool_pa_next; +static u64 svc_pool_pa_logged[SVC_POOL_PA_LOG_MAX]; +static u32 svc_pool_pa_logged_n; +static u64 svc_pool_pa_miss_log; +static u32 svc_pool_pa_hit_log; +static u32 svc_pool_wr_log; + +/* First-use-per-page proof line; bounded for the boot. */ +static void svc_pool_pa_note(u32 h, u64 va, u64 pa, const char *src) +{ + u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1); + u32 i; + + for (i = 0; i < svc_pool_pa_logged_n; i++) + if (svc_pool_pa_logged[i] == page) + return; + if (svc_pool_pa_logged_n >= SVC_POOL_PA_LOG_MAX) + return; + svc_pool_pa_logged[svc_pool_pa_logged_n++] = page; + pr_info("vgsp-sema: pool-pa ch=0x%x va=0x%llx pa=0x%llx src=%s\n", + h, va, pa, src); +} + +static void svc_pool_pa_miss_note(u64 va) +{ + if (svc_pool_pa_miss_log >= 16) + return; + svc_pool_pa_miss_log++; + pr_info("vgsp-sema: sem-pa-miss va=0x%llx (using fallback)\n", va); +} + +/* + * Bounded per-write proof line: the first 64 pool writes always print, later + * ones are rate-limited. Every release apply funnels through svc_sem_write + * (direct release, deferred pending apply with h=0, and hold-expired apply), + * so this covers all three with the handle of the originating call site. + */ +static void svc_pool_wr_note(u32 h, u64 va, u64 pa, const char *src, + u32 cur, u32 val, const char *res) +{ + if (svc_pool_wr_log < SVC_POOL_WR_LOG_MAX) { + svc_pool_wr_log++; + pr_info("vgsp-sema: pool-wr ch=0x%x va=0x%llx pa=0x%llx src=%s cur=%u -> val=%u %s\n", + h, va, pa, src, cur, val, res); + } else { + pr_info_ratelimited("vgsp-sema: pool-wr ch=0x%x va=0x%llx pa=0x%llx src=%s cur=%u -> val=%u %s\n", + h, va, pa, src, cur, val, res); + } +} + +static int svc_pool_pa_lookup(u64 va, u64 *pa, int *ap) +{ + u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1); + u32 i; + + for (i = 0; i < svc_pool_pa_n; i++) { + if (svc_pool_pa[i].va_page == page) { + *pa = svc_pool_pa[i].pa_page | + (va & (SEMA_PAGE_SIZE - 1)); + *ap = svc_pool_pa[i].ap; + return 0; + } + } + return -ENOENT; +} + +static void svc_pool_pa_store(u64 va, u64 pa, int ap) +{ + u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1); + u32 i; + + for (i = 0; i < svc_pool_pa_n; i++) { + if (svc_pool_pa[i].va_page == page) { + svc_pool_pa[i].pa_page = + pa & ~(u64)(SEMA_PAGE_SIZE - 1); + svc_pool_pa[i].ap = ap; + return; + } + } + if (svc_pool_pa_n < SVC_POOL_PA_MAX) { + i = svc_pool_pa_n++; + } else { + i = svc_pool_pa_next++ % SVC_POOL_PA_MAX; + } + svc_pool_pa[i].va_page = page; + svc_pool_pa[i].pa_page = pa & ~(u64)(SEMA_PAGE_SIZE - 1); + svc_pool_pa[i].ap = ap; +} + +static void svc_pool_pa_forget(u64 va) +{ + u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1); + u32 i; + + for (i = 0; i < svc_pool_pa_n; i++) + if (svc_pool_pa[i].va_page == page) + svc_pool_pa[i].va_page = 0; +} + +/* + * Resolve one pool-semaphore target the way the successful deferred apply did: + * injected pin (host-verified ground truth), the verified cache, then the + * targeted rm_mem record scan, whose result is read back before it is cached. + * The learned cache, the discovered map and the page-directory walks are + * deliberately skipped: each can answer with a stale page for a pool + * semaphore, and a write there is silently lost. Returns -ENOENT when no + * source applies; the caller then uses the historical order. The cache is + * keyed by VA page only and shared by every caller: the handle is logging + * only, never part of the key. `src`, when non-NULL, names the source that + * answered (for the write proof line). + */ +static int svc_pool_pa_resolve(struct nv_vgpu_device *vgpu, u32 h, u64 va, + u64 *addr, int *ap, const char **src) +{ + u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1); + u64 pa = 0; + int a = SVC_APERTURE_SYS; + u32 probe = 0; + u64 wpa = 0; + int wap = SVC_APERTURE_SYS; + + if (!svc_ipin_lookup(va, &pa, &a)) { + svc_tr_clear_failure(page); + if (src) + *src = "pin"; + svc_pool_pa_note(h, va, pa, "pin"); + *addr = pa; + *ap = a; + return 0; + } + if (!svc_pool_pa_lookup(va, &pa, &a)) { + svc_tr_clear_failure(page); + if (svc_pool_pa_hit_log < SVC_POOL_PA_HIT_LOG_MAX) { + svc_pool_pa_hit_log++; + pr_info("vgsp-sema: pool-pa hit ch=0x%x va=0x%llx pa=0x%llx\n", + h, va, pa); + } + if (src) + *src = "cache"; + *addr = pa; + *ap = a; + return 0; + } + /* + * Structural ground truth before the content scan: the client's own + * page tables (the self-discovered directory; the driver-published + * directories are tried by svc_lookup_trusted). A walk result is + * deterministic and cannot pick a stale rm_mem duplicate. It is only + * accepted when the exact release offset is readable through it, the + * same read-back proof every other source must pass. On every boot + * where the directory does not contain this VA the walk misses and + * the sequence falls through unchanged. + */ + if (!svc_walk_discovered(vgpu, page, &wpa, &wap) && + wap == SVC_APERTURE_SYS) { + u64 exact = (wpa & ~(u64)(SEMA_PAGE_SIZE - 1)) | + (va & (SEMA_PAGE_SIZE - 1)); + + if (!svc_read_ap(vgpu, exact, SVC_APERTURE_SYS, &probe, + sizeof(probe))) { + svc_pool_pa_store(va, exact, SVC_APERTURE_SYS); + svc_tr_clear_failure(page); + if (src) + *src = "walk"; + svc_pool_pa_note(h, va, exact, "walk"); + *addr = exact; + *ap = SVC_APERTURE_SYS; + return 0; + } + } + if (!svc_targeted_resolve(vgpu, va, &pa)) { + /* Read back before trusting: an unreadable page is not kept. */ + if (!svc_read_ap(vgpu, pa, SVC_APERTURE_SYS, &probe, + sizeof(probe))) { + svc_pool_pa_store(va, pa, SVC_APERTURE_SYS); + svc_tr_clear_failure(page); + if (src) + *src = "targeted"; + svc_pool_pa_note(h, va, pa, "targeted"); + *addr = pa; + *ap = SVC_APERTURE_SYS; + return 0; + } + } + return -ENOENT; +} + static int svc_sem_read(struct nv_vgpu_device *vgpu, u64 va, u32 *val) { u64 addr = 0; u32 ap = 0; + if (svc_pool_sem_va(va)) { + if (!svc_pool_pa_resolve(vgpu, 0, va, &addr, &ap, NULL)) { + if (!svc_read_ap(vgpu, addr, svc_ap_of(ap), val, + sizeof(*val))) + return 0; + /* + * The verified translation became unreadable (the + * guest reallocated the pool). Re-resolve the live + * record once and retry: only a targeted resolve that + * also fails drops the entry, so one transient read + * failure cannot evict a translation the following + * write depends on (this was the v17 bypass). + */ + if (!svc_targeted_resolve(vgpu, va, &addr)) { + svc_pool_pa_store(va, addr, SVC_APERTURE_SYS); + if (!svc_read_ap(vgpu, addr, + SVC_APERTURE_SYS, val, + sizeof(*val))) + return 0; + } + svc_pool_pa_forget(va); + } + } if (svc_resolve_va(vgpu, va, &addr, &ap)) return -ENOENT; - return svc_read_ap(vgpu, addr, - ap ? SVC_APERTURE_FB : SVC_APERTURE_SYS, - val, sizeof(*val)); + return svc_read_ap(vgpu, addr, svc_ap_of(ap), val, sizeof(*val)); } -static int svc_sem_write(struct nv_vgpu_device *vgpu, u64 va, u32 val) +static int svc_sem_write(struct nv_vgpu_device *vgpu, u32 h, u64 va, u32 val) { - u64 addr = 0; + u64 addr = 0, old_addr; u32 ap = 0; + u32 back = 0; + u32 cur = 0; + const char *src = "fallback"; + bool is_pool = svc_pool_sem_va(va); + int wap, rc; + bool verified = false; - if (svc_resolve_va(vgpu, va, &addr, &ap)) - return -ENOENT; - if (ap) + /* + * Pool semaphores resolve pin -> verified cache -> targeted rm_mem + * record before any learned translation; the learned cache and the + * discovered map may hold a stale PA for them and a write through it + * is lost (see svc_pool_pa_resolve). Every other VA keeps the + * historical order below. + */ + if (is_pool) { + if (!svc_pool_pa_resolve(vgpu, h, va, &addr, &ap, &src)) + verified = true; + else + svc_pool_pa_miss_note(va); + } + if (!verified && svc_resolve_va(vgpu, va, &addr, &ap)) { + /* + * No trusted translation at all. One last exact resolve: the + * targeted rm_mem scan validates the live record header for + * this VA and pins the page. Bounded: one slice per call and a + * full-scan miss is cached for SVC_TR_FAIL_HOLD, so a genuinely + * unmapped semaphore is not rescanned on every pass. + */ + if (svc_targeted_resolve(vgpu, va, &addr)) + return -ENOENT; + ap = SVC_APERTURE_SYS; + src = "targeted-direct"; + } + /* + * Raw aperture code, never a truthiness test: a SYS resolve (1) used + * to turn into FB here and the release landed in the vidmem shadow. + */ + wap = svc_ap_of(ap); + if (wap == SVC_APERTURE_FB) svc.vidmem_writes++; - return svc_write_ap(vgpu, addr, - ap ? SVC_APERTURE_FB : SVC_APERTURE_SYS, - &val, sizeof(val)); + old_addr = addr; + if (is_pool) + (void)svc_read_ap(vgpu, addr, wap, &cur, sizeof(cur)); + rc = svc_write_ap(vgpu, addr, wap, &val, sizeof(val)); + if (!rc && !svc_read_ap(vgpu, addr, wap, &back, sizeof(back)) && + back == val) { + if (is_pool) + svc_pool_wr_note(h, va, addr, src, cur, val, "ok"); + return 0; + } + + /* + * The value is not visible through the translation we just wrote: + * the entry can be a stale content-scanned cache line or an + * unvalidated self-pin whose page is not the one the client polls. + * Re-resolve the exact VA through the targeted rm_mem record scan + * (the live record names the client's current PA) and rewrite once. + * Bounded: exactly one re-resolve; its result is read back but never + * re-resolved again, so a hopeless VA costs one extra attempt. + */ + if (is_pool) + svc_pool_pa_forget(va); /* stale: rebuild from the live record */ + if (!svc_targeted_resolve(vgpu, va, &addr)) { + back = 0; + if (!svc_write_ap(vgpu, addr, SVC_APERTURE_SYS, &val, + sizeof(val)) && + !svc_read_ap(vgpu, addr, SVC_APERTURE_SYS, &back, + sizeof(back)) && back == val) { + if (is_pool) { + svc_pool_pa_store(va, addr, SVC_APERTURE_SYS); + svc_pool_wr_note(h, va, addr, "targeted", cur, + val, "reresolve"); + } + svc.sem_write_reresolve++; + pr_info_ratelimited("vgsp-sema: sem-write re-resolve va=0x%llx pa=0x%llx old=0x%llx new=0x%x\n", + va, addr, old_addr, val); + return 0; + } + } + if (!rc) { + if (is_pool) + svc_pool_wr_note(h, va, old_addr, src, cur, val, + "advisory"); + return 0; /* the original write landed; read-back is advisory */ + } + if (is_pool) + svc_pool_wr_note(h, va, old_addr, src, cur, val, "fail"); + return rc; } /* ------------------------------------------------------------------ */ @@ -1981,8 +2390,12 @@ static void svc_rebuild(struct nv_vgpu_device *vgpu) svc.memsets_fail, svc.ring_ops, svc.ops_denied, svc.false_trans, svc.gpfifo_skipped, svc.releases_faked, svc.releases_recovered, svc.counter_rel_applied); - pr_info("sema-svc: counter pub_calls=%llu gr_rel=%llu\n", - svc.counter_pub_calls, svc.gr_rel_applied); + pr_info("sema-svc: drain held=%llu starved=%llu forwarded=%llu faked=%llu rec=%llu\n", + svc.gpfifo_held, svc.gpfifo_starved, svc.releases_forwarded, + svc.releases_faked, svc.releases_recovered); + pr_info("sema-svc: counter pub_calls=%llu gr_rel=%llu reresolve=%llu pushpg_fb=%llu\n", + svc.counter_pub_calls, svc.gr_rel_applied, + svc.sem_write_reresolve, svc.pushpg_fallback); pr_info("sema-svc: gpfifo done=%llu nomap=%llu vidmem_writes=%llu ring_auto=%u pb=%u/%u pins=%u/%llu ipins=%u/%llu\n", svc.gpfifo_entries_done, svc.gpfifo_nomap, svc.vidmem_writes, svc_ring_n, svc_pb_n, svc_pb_passes, @@ -2010,13 +2423,13 @@ static void svc_rebuild(struct nv_vgpu_device *vgpu) svc.fwd_rel_fallback || svc.fwd_rel_pending || svc.fwd_rel_overflow || svc.fwd_hold_engaged || svc.fwd_hold_timeouts || svc.fwd_hold_canceled) - pr_info("sema-svc: work_guard deferred=%llu applied=%llu fallback=%llu pending=%llu overflow=%llu engaged=%llu timeouts=%llu canceled=%llu copy_skipped=%llu copy_soft=%llu copy_fail=%llu\n", + pr_info("sema-svc: work_guard deferred=%llu applied=%llu fallback=%llu pending=%llu overflow=%llu engaged=%llu timeouts=%llu canceled=%llu copy_skipped=%llu copy_soft=%llu copy_fail=%llu direct=%llu\n", svc.fwd_rel_deferred, svc.fwd_rel_applied, svc.fwd_rel_fallback, svc.fwd_rel_pending, svc.fwd_rel_overflow, svc.fwd_hold_engaged, svc.fwd_hold_timeouts, svc.fwd_hold_canceled, svc.fwd_copy_skipped, svc.fwd_copy_soft, - svc.fwd_copy_fail); + svc.fwd_copy_fail, svc.rel_hold_direct); /* show a few entries so mappings can be verified from dmesg */ if (gsp_sema_dbg) { u32 shown; @@ -2063,6 +2476,28 @@ static void svc_rebuild(struct nv_vgpu_device *vgpu) #define GRCE_M_LAUNCH_DMA 0x1b0 #define GRCE_M_LOAD_INLINE_DATA 0x1b4 +/* + * C5C0 (compute class) inline-QMD launch methods: SET_INLINE_QMD_ADDRESS_A/B + * carry the shifted QMD device address, LOAD_INLINE_QMD_DATA the 64 QMD words. + * The driver emits them as one INC run covering 0x318..0x41c (observed: + * op=1 count=66 at 0x318) or as several runs; the host CUDA bridge + * (gsp_cuda_bridge.c) accumulates the words and turns a completed QMD into a + * daemon job. + */ +#define SVC_QMD_ADDR_A 0x318 +#define SVC_QMD_ADDR_B 0x31c +#define SVC_QMD_DATA 0x320 +#define SVC_QMD_WORDS 64 + +/* + * Recent GRCE LOAD_INLINE_DATA destinations, newest at (next - 1). CUDA + * writes the launch arguments into the QMD's parameter bank at an arbitrary + * offset with these writes (observed: mail args at bank+0x160), so the host + * CUDA bridge searches this ring for the argument block instead of reading + * bank+0. Service thread only, so no lock. + */ +#define SVC_GRCE_DST_MAX 32 + /* LAUNCH_DMA fields. */ #define DMA_DATA_TRANSFER_TYPE(v) ((v) & 0x3) #define DMA_SEMAPHORE_TYPE(v) (((v) >> 3) & 0x3) @@ -2116,8 +2551,37 @@ struct svc_push_state { u64 rel_va; /* last release decoded in this push */ u32 rel_payload; u32 have_rel; + /* inline-QMD launch accumulation (GR compute channel) */ + u64 qmd_dev_va; /* devVA decoded from ADDRESS_A/B */ + u32 qmd_hi, qmd_lo; /* ADDRESS_A / ADDRESS_B payloads */ + u32 qmd_have; /* A/B seen bitmap */ + u32 qmd_n; /* QMD words collected so far */ + u64 qmd_bits; /* received-word bitmap */ + u64 push_va; /* push VA, for launch logging */ + u32 qmd[SVC_QMD_WORDS]; }; +/* Host CUDA bridge (gsp_cuda_bridge.c): launch transport + service hook. */ +void gsp_cuda_bridge_launch(struct nv_vgpu_device *vgpu, u32 h_object, + const u32 qmd[SVC_QMD_WORDS], u64 qmd_dev_va, + u64 push_va, const u64 *recent_dsts, + u32 n_recent_dsts); +bool gsp_cuda_bridge_enabled(void); +void gsp_cuda_bridge_service(struct nv_vgpu_device *vgpu); +int gsp_cuda_bridge_init(void); +void gsp_cuda_bridge_fini(void); + +/* VA/semaphore helpers exported to gsp_cuda_bridge.c. */ +int gsp_sema_service_va_read(struct nv_vgpu_device *vgpu, u64 va, void *buf, + size_t len); +int gsp_sema_service_va_write(struct nv_vgpu_device *vgpu, u64 va, + const void *buf, size_t len); +int gsp_sema_service_va_resolve(struct nv_vgpu_device *vgpu, u64 va, u64 *pa, + u32 *ap); +int gsp_sema_service_sem_read(struct nv_vgpu_device *vgpu, u64 va, u32 *val); +int gsp_sema_service_sem_write(struct nv_vgpu_device *vgpu, u32 h, u64 va, + u32 val); + /* ------------------------------------------------------------------ */ /* CE execution */ /* ------------------------------------------------------------------ */ @@ -2782,15 +3246,35 @@ static bool svc_stream_parse(const u8 *buf, u32 len) if (off + 4 > len) break; memcpy(&hdr, buf + off, 4); - if (hdr == 0) + if (hdr == 0) { + /* + * UVM's large page-table pushes begin with a + * zero-filled header region (observed 0x1004 + * bytes) before the first method, so zeros before + * the first method are leading padding; zeros + * after one are trailing padding and end the + * stream. + */ + if (!methods) { + off += 4; + continue; + } break; /* trailing zero padding */ + } op = (hdr >> 29) & 0x7; count = (hdr >> 16) & 0xfff; switch (op) { case 1: /* INC_METHOD */ case 3: /* NON_INC_METHOD */ case 5: /* ONE_INC */ - if (!count || count > 64 || + /* + * The count field is 12 bits, and UVM emits + * NON_INC runs of 1024 words (the page-table + * pushes); the old 64 cap rejected exactly those, + * which made the content rescue unable to ever + * validate a large push. + */ + if (!count || count > 0xfff || off + 4 + count * 4 > len) return false; off += 4 + count * 4; @@ -2851,7 +3335,15 @@ static u64 svc_rel_va_seen; static u32 svc_rel_count; static u32 svc_dbg_streams; -static void svc_counter_learn(u32 h, u64 sem_va); +/* + * Ring of recent GRCE inline-write destinations (newest at next-1), consumed + * by the CUDA bridge to locate a launch's argument block inside the QMD + * parameter bank. Written and read on the service thread only. + */ +static u64 svc_grce_dst_ring[SVC_GRCE_DST_MAX]; +static u32 svc_grce_dst_ring_next; + +static void svc_counter_learn(struct nv_vgpu_device *vgpu, u32 h, u64 sem_va); static void svc_pending_add(u64 va, u32 payload, u32 op); static int svc_rescue_push_page(struct nv_vgpu_device *vgpu, struct svc_channel *ch, u64 entry_va, @@ -2902,7 +3394,7 @@ static void svc_counter_release_apply(struct nv_vgpu_device *vgpu, u32 h, * it from the pin table first. */ if (sem_va >= 0x204000000ULL && sem_va < 0x204100000ULL) - svc_counter_learn(h, sem_va); + svc_counter_learn(vgpu, h, sem_va); spin_lock_irqsave(&svc.chan_lock, flags); for (i = 0; i < SVC_MAX_CHANNELS; i++) { @@ -3019,12 +3511,88 @@ static void svc_counter_flush_pending(struct nv_vgpu_device *vgpu, } /* + * In-kernel counter-slot derivation. + * + * The authoritative counter translation is the external injected pin, but the + * pin watcher can land tens of seconds after the first release of a channel + * (observed: a GR release at 7586.5 s whose counter was only learned at + * 7609.9 s), and every release queued in that window leaves the client's + * tracker polling a value that never arrives. On an injected-pin miss, + * resolve the slot from the module's own translations and register it - but + * only translations this service already treats as structural truth: + * + * - the page directories the driver itself published (svc_walk_vaspaces), + * - the self-discovered directory (svc_walk_discovered; accepted only after + * it reproduced several already-known VA->PA pairs), and + * - the targeted rm_mem resolve (svc_targeted_resolve; validates the rm_mem + * record header for this exact VA). + * + * The content-scanned VA map and the learned cache are deliberately NOT + * consulted: both can hold a stale translation for a counter page, and a + * write through one lands in a page the client never polls (see + * svc_counter_autoreg). A successful derivation is proven readable before it + * is returned, cached per page so the next release does not rescan, and never + * overrides an injected pin: svc_counter_learn re-checks the pin table first + * and re-registers when the injected PA differs. + * + * Service-thread only (like every other release/decode path); the cache is + * plain per-page state, mirroring counter_pa's lifetime. + */ +#define SVC_CNT_DER_MAX 16 +static struct { + u64 va_page; + u64 pa_page; +} svc_cnt_der[SVC_CNT_DER_MAX]; +static u32 svc_cnt_der_n; + +static int svc_counter_derive(struct nv_vgpu_device *vgpu, u64 va, u64 *pa_out) +{ + u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1); + u64 off = va & (SEMA_PAGE_SIZE - 1); + u64 pa = 0; + int ap = SVC_APERTURE_SYS; + u32 probe = 0; + u32 i; + bool fresh = true; + + for (i = 0; i < svc_cnt_der_n; i++) { + if (svc_cnt_der[i].va_page == page) { + pa = svc_cnt_der[i].pa_page | off; + fresh = false; + break; + } + } + if (fresh) { + if (svc_walk_vaspaces(vgpu, page, &pa, &ap) && + svc_walk_discovered(vgpu, page, &pa, &ap) && + svc_targeted_resolve(vgpu, page, &pa)) + return -ENOENT; + if (ap != SVC_APERTURE_SYS) + return -ENOENT; + pa = (pa & ~(u64)(SEMA_PAGE_SIZE - 1)) | off; + } + /* Read the slot before trusting the mapping: an unreadable page is + * not registered, so the caller keeps the "slot pending" fallback. */ + if (svc_read_ap(vgpu, pa, SVC_APERTURE_SYS, &probe, sizeof(probe))) + return -EIO; + if (fresh && svc_cnt_der_n < SVC_CNT_DER_MAX) { + svc_cnt_der[svc_cnt_der_n].va_page = page; + svc_cnt_der[svc_cnt_der_n].pa_page = + pa & ~(u64)(SEMA_PAGE_SIZE - 1); + svc_cnt_der_n++; + } + *pa_out = pa; + return 0; +} + +/* * Auto-register a GR channel's completion-counter slot from the observed * layout, without waiting for a readable push: the client zips GR channels * in ascending handle order with descending counter slots, so with n_gr - * channels the rank-r channel owns slot 0x20400fec0 + (n_gr-1-r)*0x10. Only - * a host-verified (pinned) translation is used, so the write target is the - * page the client actually polls. This lets the publish path advance every + * channels the rank-r channel owns slot 0x20400fec0 + (n_gr-1-r)*0x10. The + * slot PA comes from a host-verified pin or from the in-kernel structural + * derivation (svc_counter_derive), so the write target is a page the client + * actually polls. This lets the publish path advance every * channel's counter even when some GR pushes are unreadable (observed: the * lower ranks' 676-byte context-init pushes stayed unresolved while the * other 13 channels' counters advanced, and the client timed out waiting). @@ -3034,6 +3602,7 @@ static void svc_counter_autoreg(struct nv_vgpu_device *vgpu, { u64 va, pa = 0; int ap = SVC_APERTURE_SYS; + bool derived = false; if (ch->engine_type || n_gr <= 0) return; @@ -3045,52 +3614,76 @@ static void svc_counter_autoreg(struct nv_vgpu_device *vgpu, return; va = 0x20400fec0ULL + (u64)(n_gr - 1 - ch->gr_index) * 0x10ULL; /* - * Only the authoritative injected-pin table may register a counter - * slot: the self-discovered map can hold a stale translation for this - * VA (content-scanned page), and a write to that page never reaches - * the client. If no injected pin exists yet, leave counter_pa zero so - * a later pass (once the pin lands) registers it. + * The injected-pin table is the authoritative source; only it may + * shadow a self-discovered map entry (which can hold a stale + * translation for this VA - a content-scanned page - and a write + * there never reaches the client). On a miss, derive the slot + * in-kernel from the structural translations and the read-back proof + * instead of waiting for the pin watcher; the layout formula only + * names which slot belongs to this rank. If no translation exists, + * leave counter_pa zero so a later pass registers it. */ - if (svc_ipin_lookup(va, &pa, &ap) || ap != SVC_APERTURE_SYS) - return; + if (svc_ipin_lookup(va, &pa, &ap) || ap != SVC_APERTURE_SYS) { + if (!gsp_sema_counter_derive || + svc_counter_derive(vgpu, va, &pa)) + return; + derived = true; + } ch->counter_pa = pa & ~0x3ULL; - pr_info("sema-chan: counter auto h=0x%x rank=%u pa=0x%llx (slot va=0x%llx)\n", - ch->h_object, ch->gr_index, ch->counter_pa, va); + if (derived) + pr_info("sema-chan: counter derived h=0x%x rank=%u pa=0x%llx (slot va=0x%llx)\n", + ch->h_object, ch->gr_index, ch->counter_pa, va); + else + pr_info("sema-chan: counter auto h=0x%x rank=%u pa=0x%llx (slot va=0x%llx)\n", + ch->h_object, ch->gr_index, ch->counter_pa, va); svc_counter_flush_pending(vgpu, ch); } /* * Learn a GR channel's completion-counter guest PA from a release VA the * push carried, so the publish path can advance the exact slot the client - * polls without any guest-side enumeration. Only a host-verified translation - * (pin/ipin) is trusted; the release VA of a GR push is the counter slot - * itself (0x20400fXX observed). No-op once registered. + * polls without any guest-side enumeration. The release VA of a GR push is + * the counter slot itself (0x20400fXX observed). An injected pin is the + * authoritative source; on a miss the slot is derived in-kernel + * (svc_counter_derive) so the release is authored immediately rather than + * waiting for the external pin watcher. No-op once registered at the same PA. */ -static void svc_counter_learn(u32 h, u64 sem_va) +static void svc_counter_learn(struct nv_vgpu_device *vgpu, u32 h, u64 sem_va) { unsigned long flags; u64 pa = 0; int ap = SVC_APERTURE_SYS; + bool derived = false; int i; if (!h || sem_va < 0x204000000ULL || sem_va >= 0x204100000ULL) return; /* - * Authoritative injected pins only (see svc_counter_autoreg): a stale - * self-discovered entry would make every release land on a page the - * client never polls. Re-learn when the injected PA differs, so a pin - * that lands after an earlier stale registration still wins. + * Injected pins first (see svc_counter_autoreg): they stay + * authoritative and re-learn when their PA differs, so a pin that + * lands after a derived/stale registration still wins. On a miss, + * derive from the structural translations only - never from the + * content-scanned map or the learned cache, which can hold a stale + * page for this VA. */ - if (svc_ipin_lookup(sem_va, &pa, &ap) || ap != SVC_APERTURE_SYS) - return; + if (svc_ipin_lookup(sem_va, &pa, &ap) || ap != SVC_APERTURE_SYS) { + if (!gsp_sema_counter_derive || + svc_counter_derive(vgpu, sem_va, &pa)) + return; + derived = true; + } spin_lock_irqsave(&svc.chan_lock, flags); for (i = 0; i < SVC_MAX_CHANNELS; i++) { if (svc.chan[i].valid && svc.chan[i].h_object == h) { if (svc.chan[i].counter_pa != (pa & ~0x3ULL)) { svc.chan[i].counter_pa = pa & ~0x3ULL; - pr_info("sema-chan: counter learned h=0x%x pa=0x%llx (from release va=0x%llx)\n", - h, pa & ~0x3ULL, sem_va); + if (derived) + pr_info("sema-chan: counter derived h=0x%x pa=0x%llx (from release va=0x%llx)\n", + h, pa & ~0x3ULL, sem_va); + else + pr_info("sema-chan: counter learned h=0x%x pa=0x%llx (from release va=0x%llx)\n", + h, pa & ~0x3ULL, sem_va); } break; } @@ -3114,14 +3707,14 @@ static bool svc_rel_apply_906f(struct nv_vgpu_device *vgpu, u32 cur = 0; if (ch->h_object && svc_counter_va(va)) { - svc_counter_learn(ch->h_object, va); + svc_counter_learn(vgpu, ch->h_object, va); svc_counter_release_apply(vgpu, ch->h_object, va, payload); return false; } if (!svc_sem_read(vgpu, va, &cur) && payload > cur) { svc.pushes_pending++; cur = (op == HOST_SEM_OP_REDUCTION) ? cur + payload : payload; - if (!svc_sem_write(vgpu, va, cur)) { + if (!svc_sem_write(vgpu, ch->h_object, va, cur)) { svc.releases_applied++; svc.post_hint_until = jiffies + 10 * HZ; return true; @@ -3137,7 +3730,7 @@ static bool svc_rel_apply_reportsem(struct nv_vgpu_device *vgpu, u32 cur = 0; if (ch->h_object && svc_counter_va(va)) { - svc_counter_learn(ch->h_object, va); + svc_counter_learn(vgpu, ch->h_object, va); svc_counter_release_apply(vgpu, ch->h_object, va, payload); return false; } @@ -3148,7 +3741,7 @@ static bool svc_rel_apply_reportsem(struct nv_vgpu_device *vgpu, if (payload <= cur) return false; svc.pushes_pending++; - if (!svc_sem_write(vgpu, va, payload)) { + if (!svc_sem_write(vgpu, ch->h_object, va, payload)) { svc.releases_applied++; svc.post_hint_until = jiffies + 10 * HZ; return true; @@ -3175,14 +3768,14 @@ static bool svc_rel_apply_launch(struct nv_vgpu_device *vgpu, */ if (ch->engine_type == 0 && svc_counter_va(sem_va)) { /* handled by the exact-slot path */ - } else if (!svc_sem_write(vgpu, sem_va, cur)) { + } else if (!svc_sem_write(vgpu, ch->h_object, sem_va, cur)) { svc.releases_applied++; svc.post_hint_until = jiffies + 10 * HZ; return true; } } if (ch->engine_type == 0) - svc_counter_learn(ch->h_object, sem_va); + svc_counter_learn(vgpu, ch->h_object, sem_va); svc_counter_release_apply(vgpu, ch->h_object, sem_va, payload); return false; } @@ -3235,19 +3828,109 @@ static void svc_fwd_relay_note(struct nv_vgpu_device *vgpu, DMA_SEM_REDUCTION(launch_dma) == SEM_REDUCTION_INC) return; } - if (svc_resolve_va(vgpu, r->va, &addr, &ap)) + if (svc_pool_sem_va(r->va)) { + if (svc_pool_pa_resolve(vgpu, ch->h_object, r->va, &addr, + &ap, NULL)) + return; + } else if (svc_resolve_va(vgpu, r->va, &addr, &ap)) { return; + } if (svc_sem_read(vgpu, r->va, &cur)) return; /* Forward-only pre-check: only a word behind the payload is relayed. */ if (cur >= r->payload) return; r->tgt_ok = true; - r->tgt_kind = ap ? SVC_APERTURE_FB : SVC_APERTURE_SYS; + r->tgt_kind = svc_ap_of(ap); r->tgt_page = addr & ~(u64)(SEMA_PAGE_SIZE - 1); r->tgt_off = (u32)(addr & (SEMA_PAGE_SIZE - 1)); } +/* Whole-boot budget for the "no forwarded copy" proof line. */ +static u32 svc_fwd_rel_direct_log; + +/* + * One rate-limited line per decoded release that was applied directly because + * no forwarded hardware copy ever engaged (or the bounded hold window expired + * without one). A decoded release is never left to a copy that will not run; + * this line is the next-run proof of that path. + */ +static void svc_fwd_rel_direct_note(u32 h, u64 va, u32 payload) +{ + svc.rel_hold_direct++; + if (svc_fwd_rel_direct_log >= 32) + return; + svc_fwd_rel_direct_log++; + pr_info("vgsp-work: rel-hold direct h=0x%x va=0x%llx pay=%u (no work)\n", + h, va, payload); +} + +/* + * Accumulate one method run's inline-QMD words; hand a complete 64-word QMD + * to the host CUDA bridge. The run may cover the address registers and the + * data registers as a single INC/NON_INC/ONE_INC method (observed: op=1 + * count=66 starting at 0x318), so every data word is mapped to its register + * offset. Each completed QMD resets the accumulator, so a push may carry + * more than one launch. + */ +static void svc_qmd_accum(struct nv_vgpu_device *vgpu, struct svc_channel *ch, + struct svc_push_state *st, u32 addr, u32 count, + const u8 *data) +{ + u32 i; + + for (i = 0; i < count; i++) { + u32 a = addr + 4 * i; + u32 v; + + memcpy(&v, data + 4 * i, 4); + if (a == SVC_QMD_ADDR_A) { + st->qmd_hi = v; + st->qmd_have |= 1; + } else if (a == SVC_QMD_ADDR_B) { + st->qmd_lo = v; + st->qmd_have |= 2; + } else if (a >= SVC_QMD_DATA && + a < SVC_QMD_DATA + SVC_QMD_WORDS * 4) { + u32 idx = (a - SVC_QMD_DATA) / 4; + + if (!(st->qmd_bits & (1ULL << idx))) { + st->qmd_bits |= 1ULL << idx; + st->qmd[idx] = v; + st->qmd_n++; + } + } + } + if (st->qmd_have == 3) + st->qmd_dev_va = (((u64)st->qmd_hi << 32) | st->qmd_lo) << 8; + if (st->qmd_n == SVC_QMD_WORDS) { + u64 dsts[SVC_GRCE_DST_MAX]; + u32 have, i, n = 0; + + /* + * Hand the bridge the recent inline-write destinations, + * newest first, so it can find the argument block CUDA + * placed inside the QMD parameter bank (never assume + * bank+0). The current push's writes are already in the + * ring because the QMD run follows them. + */ + have = svc_grce_dst_ring_next < SVC_GRCE_DST_MAX ? + svc_grce_dst_ring_next : SVC_GRCE_DST_MAX; + for (i = 0; i < have; i++) { + u32 idx = (svc_grce_dst_ring_next - 1 - i) % + SVC_GRCE_DST_MAX; + + dsts[n++] = svc_grce_dst_ring[idx]; + } + gsp_cuda_bridge_launch(vgpu, ch->h_object, st->qmd, + st->qmd_dev_va, st->push_va, + dsts, n); + st->qmd_n = 0; + st->qmd_bits = 0; + st->qmd_have = 0; + } +} + static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf, u32 len, u64 page_pa, bool allow_ops, struct svc_channel *ch, u32 entry_idx, @@ -3279,6 +3962,7 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf, } memset(&st, 0, sizeof(st)); + st.push_va = push_va; /* * P6b wave 1: clear the guest-forwarding candidate at the start of * every decode pass; the first eligible copy captured below is the one @@ -3364,6 +4048,19 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf, trusted, allow_ops, st.have_out, st.line_length, ch->h_object); } + /* + * Record the destination for the CUDA bridge's + * argument-block search, whether or not the write was + * executed: the launch QMD later in this push needs it + * to find where CUDA placed the kernel arguments in + * the QMD parameter bank. + */ + if (st.have_out && st.offset_out) { + svc_grce_dst_ring[svc_grce_dst_ring_next % + SVC_GRCE_DST_MAX] = + st.offset_out; + svc_grce_dst_ring_next++; + } off += 4 + count * 4; continue; } @@ -3375,12 +4072,28 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf, continue; } - if ((op != 1 && op != 3 && op != 5) || !count || count > 64 || + if ((op != 1 && op != 3 && op != 5) || !count || count > 0xfff || off + 4 + count * 4 > len) { off += 4; /* resync */ continue; } + /* + * CUDA launch (C5C0 compute class, subchannel >= 1): capture + * the inline-QMD method run for the host CUDA bridge. The run + * is skipped whole exactly as before (release/decode behaviour + * is untouched); a completed 64-word QMD queues a daemon job. + */ + if (ch->engine_type == 0 && subch >= 1 && + gsp_cuda_bridge_enabled() && + addr >= SVC_QMD_ADDR_A && + addr <= SVC_QMD_DATA + (SVC_QMD_WORDS - 1) * 4) { + svc_qmd_accum(vgpu, ch, &st, addr, count, + buf + off + 4); + off += 4 + count * 4; + continue; + } + if (op == 3 || op == 5) { /* * NON_INC / ONE_INC methods carry their own data @@ -3502,7 +4215,8 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf, * only the apply moves. Overflow applies inline. */ if (gsp_phys_work_defer_release_enabled() && - svc_fwd_cand.valid) { + svc_fwd_cand.valid && + gsp_phys_guest_mem_enabled()) { if (svc_fwd_rel_n < SVC_FWD_REL_MAX) { struct svc_fwd_rel *r = &svc_fwd_rel[svc_fwd_rel_n++]; @@ -3617,7 +4331,8 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf, * Overflow applies inline. */ if (gsp_phys_work_defer_release_enabled() && - svc_fwd_cand.valid) { + svc_fwd_cand.valid && + gsp_phys_guest_mem_enabled()) { if (svc_fwd_rel_n < SVC_FWD_REL_MAX) { struct svc_fwd_rel *r = &svc_fwd_rel[svc_fwd_rel_n++]; @@ -3952,7 +4667,8 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf, * release as decoded. Overflow applies inline. */ if (gsp_phys_work_defer_release_enabled() && - svc_fwd_cand.valid) { + svc_fwd_cand.valid && + gsp_phys_guest_mem_enabled()) { if (svc_fwd_rel_n < SVC_FWD_REL_MAX) { struct svc_fwd_rel *r = &svc_fwd_rel[svc_fwd_rel_n++]; @@ -3996,12 +4712,29 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf, svc_rel_count++; } } - } else if (svc_rel_apply_launch(vgpu, ch, sem_va, - st.sem_payload, - cur, pending, inc, - inc_apply)) { - svc_rel_va_seen = sem_va; - svc_rel_count++; + } else { + /* + * Deferral is enabled but no forwarded + * hardware copy can run for this push + * (guest-memory copy path off, or no + * candidate): apply the decoded release + * directly, exactly once. The note is + * the proof line that a decoded release + * is never left to a copy that will + * not engage. + */ + if (gsp_phys_work_defer_release_enabled() && + svc_fwd_cand.valid) + svc_fwd_rel_direct_note(ch->h_object, + sem_va, + st.sem_payload); + if (svc_rel_apply_launch(vgpu, ch, sem_va, + st.sem_payload, + cur, pending, inc, + inc_apply)) { + svc_rel_va_seen = sem_va; + svc_rel_count++; + } } /* release ends the push */ n_ops = 0; @@ -4096,7 +4829,7 @@ static void svc_apply_pending(struct nv_vgpu_device *vgpu) svc_pending[i] = svc_pending[--svc_pending_n]; continue; } - if (!svc_sem_write(vgpu, p->va, + if (!svc_sem_write(vgpu, 0, p->va, p->op ? cur + p->payload : p->payload)) { svc.releases_applied++; svc.post_hint_until = jiffies + 10 * HZ; @@ -4239,7 +4972,7 @@ static u32 __maybe_unused __svc_apply_page_data_disabled(struct nv_vgpu_device * (lo & 0xfff) < 0x20 && svc_pb_first_push(buf)) svc_pb_add(pa_page, lo & 0xfff); - if (svc_sem_write(vgpu, va, payload) == 0) { + if (svc_sem_write(vgpu, 0, va, payload) == 0) { svc.releases_applied++; svc.post_hint_until = jiffies + 10 * HZ; applied++; @@ -4595,6 +5328,223 @@ static int svc_read_va_trusted(struct nv_vgpu_device *vgpu, u64 va, void *buf, } /* + * Linear fallback for a push-buffer window page. + * + * svc_pb_pair() pairs the window base VA with the base PA it found by content + * and pins page N at base_pa + N*0x1000. That holds only while the guest's + * push-buffer allocation is physically contiguous, which is not guaranteed + * and was observed to change run-to-run. This returns the guess for a page in + * the channel's window; callers consult it only after every validated + * per-page source missed. Returns 0 with the page-aligned PA, -ENOENT when + * the page is outside every window known for the channel's rank. + */ +static int svc_pb_win_lookup(const struct svc_channel *ch, u64 va_page, + u64 *pa_page) +{ + u32 slot, i; + + if (!ch || !ch->engine_type || ch->ce_index >= 32) + return -ENOENT; + slot = 4 * ch->ce_index; + for (i = 0; i < svc_pb_n; i++) { + u64 pa; + + if (svc_pb[i].slot != slot || !svc_pb[i].va || + !svc_pb[i].pa_page) + continue; + if (va_page < svc_pb[i].va || + va_page >= svc_pb[i].va + 0x100000ULL) + continue; + pa = svc_pb[i].pa_page + (va_page - svc_pb[i].va); + if (pa >= SEMA_PA_MAX) + return -ENOENT; + *pa_page = pa; + return 0; + } + return -ENOENT; +} + +#define SVC_PUSH_PG_LOG_MAX 4 + +/* + * One line when a push page resolved from a validated pin while the channel's + * linear window guess would have answered differently - the non-contiguous + * case the window convention gets wrong. Capped per channel and one line per + * page so a boot log shows whether pins carried the read without per-push + * spam. Behind sema_dbg like the other decode diagnostics. + */ +static void svc_pushpg_note(struct svc_channel *ch, u64 page_va, u64 pin_pa, + u64 win_pa) +{ + if (!gsp_sema_dbg || ch->pushpg_n >= SVC_PUSH_PG_LOG_MAX) + return; + if ((pin_pa & ~0xfffULL) == (win_pa & ~0xfffULL)) + return; /* the window guess already agrees */ + if (ch->pushpg_va == page_va) + return; + ch->pushpg_va = page_va; + ch->pushpg_n++; + pr_info("sema-pushpg: h=0x%x va=0x%llx pin=1 pa=0x%llx win=0x%llx\n", + ch->h_object, page_va, pin_pa & ~0xfffULL, + win_pa & ~0xfffULL); +} + +/* + * Cheap candidate-arbitration prefix: a real push starts with a method header + * (op 1/3/5 with a non-zero count, or a header-only 4/7), optionally after + * the zero-filled header region UVM emits before large page-table pushes + * (observed up to 0x1004 bytes). A zero page or unrelated data practically + * never starts with one, so a wrong injected pin can be rejected before the + * pump's full-stream parse. An all-zero chunk cannot refute the pin and is + * accepted; the full parse is the final arbiter. The run length is + * deliberately not bounded by the first chunk: a valid method run may span + * into the next page and the full parse settles that. + */ +static bool svc_push_prefix_ok(const u8 *buf, size_t len) +{ + u32 hdr, op, count; + size_t off = 0; + + while (off + 4 <= len) { + memcpy(&hdr, buf + off, 4); + if (hdr != 0) + break; + off += 4; + } + if (off + 4 > len) + return true; /* all zeros: not refutable here */ + op = (hdr >> 29) & 0x7; + if (op == 4 || op == 7) /* header-only methods */ + return true; + if (op != 1 && op != 3 && op != 5) + return false; + count = (hdr >> 16) & 0xfff; + return count != 0; +} + +/* Fallback-line budget for the whole boot. */ +static u32 svc_pushpg_fb_log; + +/* + * Read a push the way the engine does: every 4 KB page of the range is + * translated independently. + * + * The old push read trusted the window's linear mapping for the whole range + * (through the pins svc_pb_pair installed). When the guest's push-buffer + * allocation is not physically contiguous, the continuation page answered + * with the wrong PA, the method-stream plausibility check failed, and the + * release the client waited on was never applied (observed: push #25 on + * h=0xcaf00012 at va=0x120000f58 crossing into a page whose PA was not + * base+0x1000; the pool semaphore stalled at 24 and the guest UVM hung). + * + * Order per page: + * 1. injected pin (svc_ipin_lookup): host-verified ground truth, but the + * content arbitrates - a pin whose first chunk does not start like a + * 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 + * (content rescue, first-push pairing), window guesses excluded; + * 3. 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 + * (svc_lookup_trusted), exactly the old behaviour for GR/other pushes. + * + * For a contiguous allocation every path resolves to the same PA the old + * read returned, so the normal case is byte-for-byte unchanged; only a + * non-contiguous or wrongly-pinned page changes behaviour (to its next + * validated candidate). Reads stay defensively copied through + * svc_read_ap()/vfio_dma_rw(), which enforce the aperture bounds. + */ +static int svc_read_va_push_ex(struct nv_vgpu_device *vgpu, + struct svc_channel *ch, u64 va, void *buf, + size_t len, bool skip_ipin) +{ + u8 *p = buf; + + while (len) { + u64 page_va = va & ~(u64)(SEMA_PAGE_SIZE - 1); + size_t chunk = min_t(size_t, len, + SEMA_PAGE_SIZE - (va & (SEMA_PAGE_SIZE - 1))); + u64 pa = 0, win_pa = 0; + int ap = SVC_APERTURE_SYS; + bool have_win, done = false; + bool first = (p == buf); + int last_rc = -ENOENT; + u32 src; + + have_win = !svc_pb_win_lookup(ch, page_va, &win_pa); + + for (src = 0; src < 4 && !done; src++) { + bool via_ipin = false; + + pa = 0; + ap = SVC_APERTURE_SYS; + switch (src) { + case 0: + if (skip_ipin || + svc_ipin_lookup(va, &pa, &ap)) + continue; + via_ipin = true; + if (have_win) + svc_pushpg_note(ch, page_va, pa, + win_pa); + break; + case 1: + if (svc_pin_lookup_valid(va, &pa, &ap)) + continue; + if (have_win) + svc_pushpg_note(ch, page_va, pa, + win_pa); + break; + case 2: + if (!have_win) + continue; + pa = win_pa + (va & (SEMA_PAGE_SIZE - 1)); + ap = SVC_APERTURE_SYS; + break; + default: + /* learned cache / discovered map / page + * directories / targeted rm_mem resolve */ + if (svc_lookup_trusted(vgpu, va, &pa, &ap)) + continue; + break; + } + + if (ap == SVC_APERTURE_SYS && pa >= SEMA_PA_MAX) { + last_rc = -EIO; + continue; + } + if (svc_read_ap(vgpu, pa, + ap == SVC_APERTURE_FB ? + SVC_APERTURE_FB : + SVC_APERTURE_SYS, + p, chunk)) { + last_rc = -EIO; + continue; + } + if (via_ipin && first && + !svc_push_prefix_ok(p, chunk)) + continue; /* content says: wrong pin */ + done = true; + } + if (!done) + return last_rc; + p += chunk; + va += chunk; + len -= chunk; + } + return 0; +} + +static int svc_read_va_push(struct nv_vgpu_device *vgpu, + struct svc_channel *ch, u64 va, void *buf, + size_t len) +{ + return svc_read_va_push_ex(vgpu, ch, va, buf, len, false); +} + +/* * Read the GPFIFO get/put pair from a candidate USERD. Returns 0 when the * values are plausible for a channel with `entries` GPFIFO slots. */ @@ -4815,8 +5765,18 @@ static int svc_chan_entry0(struct nv_vgpu_device *vgpu, struct svc_channel *ch, */ #define SVC_GR_RING_STRIDE 0x3000ULL #define SVC_GR_USERD_OFF 0x2000ULL - bool gsp_sema_gr_rings = true; + +/* + * In-kernel GR completion-counter slot derivation (svc_counter_derive): on an + * injected-pin miss, resolve a release's counter slot from the structural + * translations (published page directories, self-discovered directory, + * targeted rm_mem record) instead of holding the release until the external + * pin watcher lands. Default on; set 0 to restore the injected-pin-only + * behaviour for an A/B without a rebuild. + */ +bool gsp_sema_counter_derive = true; + /* * GR (compute) entries whose push page is not translatable are left pending * for this long before the pump consumes them as stale. The push pages are @@ -4968,12 +5928,54 @@ static void svc_gr_ring_autopin(struct nv_vgpu_device *vgpu, /* * The candidate chunk did not validate. If another source already - * translates this ring VA (bridge or manual pin), trust it. + * translates this ring VA (bridge or manual pin), do not just trust + * it: read the first GPFIFO entry through that translation exactly + * like the FB candidate above and validate it with the same checks. + * Previously this branch latched the rank without storing the + * translation or the USERD, so for a sysmem pin svc_chan_userd() had + * no candidate (its FB-only ring offset path is skipped) and the + * pump stalled before ever draining the ring: the GR push decode, + * its release and the client's completion counter all never + * happened. Only a validated read may latch the rank, so a stale or + * wrong pin still falls through to the "not at fb" retry. */ if (!svc_pin_lookup_any(ch->gpfifo_va, &pin_pa, &pin_ap)) { - set_bit(ch->gr_index, &svc_gr_ring_done); - svc_ring_n++; - return; + if (!svc_read_ap(vgpu, pin_pa, svc_ap_of(pin_ap), e, + sizeof(e))) { + memcpy(&e0, e, 4); + memcpy(&e1, e + 4, 4); + va = ((u64)(e1 & 0xff) << 32) | + (u64)(e0 & 0xfffffffc); + len = ((e1 >> 10) & 0x1fffff) << 2; + if (!(va & 0xfffff) && va >= SEMA_VA_LO && + va < SEMA_VA_HI && len >= 4 && len <= 0x10000) { + /* + * Best effort only: a wrapped GP_PUT + * legitimately exceeds the entry count, so a + * failed probe must not reject the ring; the + * pump reads the raw put/get itself and + * reduces put modulo the entry count. + */ + (void)svc_userd_probe(vgpu, + pin_pa + SVC_GR_USERD_OFF, + svc_ap_of(pin_ap), + ch->gpfifo_entries, &put, + &get); + svc_pin_store(ch->gpfifo_va, pin_pa, pin_ap); + svc_lat_store(ch->gpfifo_va, pin_pa, pin_ap); + ch->userd_pa = pin_pa + SVC_GR_USERD_OFF; + ch->userd_as = svc_ap_of(pin_ap); + ch->userd_ok = true; + svc_gr_ring_base = base; + set_bit(ch->gr_index, &svc_gr_ring_done); + svc_ring_n++; + pr_info("sema-chan: auto-pinned GR ring h=0x%x rank=%u va=0x%llx -> pa=0x%llx (entry0 va=0x%llx len=%u put=%u get=%u)\n", + ch->h_object, ch->gr_index, + ch->gpfifo_va, pin_pa, va, len, put, + get); + return; + } + } } if (!test_and_set_bit(ch->gr_index, &svc_gr_ring_tried)) @@ -5053,8 +6055,19 @@ static void svc_pb_pair(struct nv_vgpu_device *vgpu, struct svc_channel *ch) len < 4 || len > 0x10000) return; + /* + * Install the window mapping as tagged *guesses*: only the base page + * was paired against real content (svc_chan_entry0 read the first + * push there). The rest of the 1 MB window is NOT assumed physically + * contiguous - a push may cross into a page whose PA is not + * base+N*0x1000 (observed: a push at window offset 0xf58 whose + * continuation page lived one page *below* the base). Push reads + * skip these guesses and resolve each page on its own + * (svc_read_va_push), using this window mapping only as a last-resort + * fallback; every other consumer still sees the pins as before. + */ for (off = 0; off < 0x100000; off += SEMA_PAGE_SIZE) { - svc_pin_store(va + off, hit_pa + off, SVC_APERTURE_SYS); + svc_pin_store_win(va + off, hit_pa + off, SVC_APERTURE_SYS); svc_lat_store(va + off, hit_pa + off, SVC_APERTURE_SYS); svc_mark_hot(hit_pa + off); } @@ -5433,14 +6446,47 @@ static int svc_rescue_push_page(struct nv_vgpu_device *vgpu, static void svc_init_expected_payload(struct nv_vgpu_device *vgpu, struct svc_channel *ch) { - u64 pool_va = 0x121000000ULL + 4ULL * ch->ce_index; + /* + * The exact slot is the release VA this channel's last decoded push + * carried. Until one is decoded, the rank-derived VA is a guess (the + * UVM pool index is allocated in channel creation order, not by + * rank); it is only used to seed the expectation early. Once a push + * decodes, last_rel_va is the authoritative slot and the expectation + * (and the automatic recovery write) follows it. + */ + u64 pool_va; u64 pa = 0; int ap = SVC_APERTURE_SYS; u32 cur = 0; u32 want; - if (svc_lookup_trusted(vgpu, pool_va, &pa, &ap)) + /* + * Only CE channels own a UVM tracking-semaphore pool slot; a GR + * channel's completion signal is its per-channel counter. Deriving a + * pool VA for a GR channel and resolving it here used to start a + * targeted RAM scan per GR channel per pass, starving the pump. + */ + if (!ch->engine_type) + return; + + pool_va = ch->last_rel_va ? ch->last_rel_va : + 0x121000000ULL + 4ULL * ch->ce_index; + + /* + * The expectation is a hint, never a gate. A known exact slot may go + * through the verified pool resolver (injected pin -> validated cache + * -> walk -> targeted scan); a rank-derived guess uses the learned + * map only, so seeding an unknown slot can never launch a multi- + * hundred-millisecond RAM scan from the per-pass hot path. + */ + if (ch->last_rel_va) { + if (svc_pool_pa_resolve(vgpu, ch->h_object, pool_va, &pa, &ap, + NULL) && + svc_lookup_trusted(vgpu, pool_va, &pa, &ap)) + return; /* pool page not mapped yet */ + } else if (svc_lookup_trusted(vgpu, pool_va, &pa, &ap)) { return; /* pool page not mapped yet */ + } if (svc_read_ap(vgpu, pa, ap == SVC_APERTURE_FB ? SVC_APERTURE_FB : SVC_APERTURE_SYS, &cur, sizeof(cur))) @@ -5518,10 +6564,23 @@ static void svc_counter_publish(struct nv_vgpu_device *vgpu, /* * Acknowledge a CE tracking-semaphore release without reading the push that - * carried it: write the expected payload into the semaphore the channel last - * released to (learned from a readable push; the pool index is allocated by - * UVM and does not match the channel rank). Returns false when there is - * nothing to fall back on. Caller owns expected_sem_payload bookkeeping. + * carried it: write the expected payload into the exact semaphore slot the + * channel last released to. That slot is push/CE-side evidence: the decoded + * release VA itself (uvm_channel_tracking_semaphore_get_gpu_va() = + * pool-page GPU VA + semaphore->index * 4; gsp_sema_service.c pool-wr lines + * show one slot per CE channel). The pool index is allocated by UVM in + * channel creation order and does NOT generally match the channel rank, so + * the rank-derived VA (`0x121000000 + 4*ce_index`) is a guess: it is only + * used when the operator explicitly asked for it via gsp_sema_fake_rel / + * gsp_sema_rel_recover. The automatic recovery path (no module parameter) + * requires the decoded VA and never writes into an unproven slot. + * + * Returns false only when there is no exact slot to fall back on at all + * (no decoded release VA and no operator-selected rank guess). A known + * slot is always acknowledged: when its page is not translatable the + * expected write is queued for the pending-release retry pass, the + * expectation still advances and the caller consumes the GPFIFO entry. + * A missing translation must never hold/starve the channel. */ static bool svc_fake_release(struct nv_vgpu_device *vgpu, struct svc_channel *ch) { @@ -5529,31 +6588,82 @@ static bool svc_fake_release(struct nv_vgpu_device *vgpu, struct svc_channel *ch u64 spa = 0; int sap = SVC_APERTURE_SYS; u32 scur = 0; + u32 deliver; + bool have_pa = true; + bool delivered = false; if (!ch->engine_type || !ch->expected_sem_payload) return false; - pool_va = ch->last_rel_va ? ch->last_rel_va : - 0x121000000ULL + 4ULL * ch->ce_index; + pool_va = ch->last_rel_va; + if (!pool_va) { + if (!gsp_sema_fake_rel && !gsp_sema_rel_recover) + return false; + pool_va = 0x121000000ULL + 4ULL * ch->ce_index; + } + deliver = ch->expected_sem_payload; /* * The release can only be delivered when the pool page translates. - * A missing translation is transient (the client's pool page is pinned - * a moment after the channel appears), so report failure and let the - * caller keep the GPFIFO entry pending: consuming it without writing - * the semaphore leaves the client waiting forever. + * svc_pool_pa_resolve() is the verified resolver (injected pin -> + * read-back-validated cache -> walk -> targeted rm_mem record with + * read-back); a translated page is still read at the exact release + * offset before the write. Failure here is not fatal: the exact slot + * is known, so the write is queued and the entry is released. */ - if (svc_lookup_trusted(vgpu, pool_va, &spa, &sap)) - return false; - if (svc_read_ap(vgpu, spa, sap, &scur, sizeof(scur))) - return false; - - if (scur < ch->expected_sem_payload) { - u32 nv = ch->expected_sem_payload; + if (svc_pool_sem_va(pool_va)) { + if (svc_pool_pa_resolve(vgpu, ch->h_object, pool_va, &spa, + &sap, NULL) && + svc_lookup_trusted(vgpu, pool_va, &spa, &sap)) + have_pa = false; + } else if (svc_lookup_trusted(vgpu, pool_va, &spa, &sap)) { + have_pa = false; + } + if (have_pa && + !svc_read_ap(vgpu, spa, svc_ap_of(sap), &scur, sizeof(scur))) { + if (scur >= ch->expected_sem_payload) { + /* Another path already delivered this payload. */ + delivered = true; + } else { + u32 nv = ch->expected_sem_payload; + u32 back = 0; - if (svc_write_ap(vgpu, spa, sap, &nv, sizeof(nv))) - return false; - svc.releases_applied++; + /* + * UVM's update_completed_value() asserts on a jump + * beyond UVM_GPU_SEMAPHORE_MAX_JUMP (a stale + * expectation must never slam the whole page): if the + * expectation is far ahead, deliver exactly one step + * and re-seed it. + */ + if (nv - scur > 0x1000) { + nv = scur + 1; + ch->expected_sem_payload = nv + 1; + } + deliver = nv; + if (!svc_write_ap(vgpu, spa, svc_ap_of(sap), &nv, + sizeof(nv)) && + !svc_read_ap(vgpu, spa, svc_ap_of(sap), &back, + sizeof(back)) && + back == nv) { + delivered = true; + if (svc_pool_sem_va(pool_va)) + svc_pool_pa_store(pool_va, spa, + svc_ap_of(sap)); + svc.releases_applied++; + if (!gsp_sema_fake_rel) + svc.releases_recovered++; + svc.post_hint_until = jiffies + 10 * HZ; + } + } + } + if (!delivered) { + /* + * The exact target is known but not translatable right now. + * Never hold the channel: queue the exact-slot write for the + * retry pass so it still lands when the mapping appears. + */ + svc_pending_add(pool_va, deliver, 0); + svc.releases_forwarded++; } svc.releases_faked++; svc_rel_count++; @@ -5969,6 +7079,22 @@ static void svc_fwd_rel_apply_all(struct nv_vgpu_device *vgpu, svc_fwd_rel_apply_all_ex(vgpu, ch, rel, n, SVC_FWD_REL_SKIP_NONE); } +/* + * Proof line for a held record set resolved without a successful forwarded + * copy (failure/timeout/abort): the releases below are applied directly, so + * none is lost to a hardware copy that never ran. Called with svc_fwd_hold + * populated, before svc_fwd_hold_clear(). + */ +static void svc_fwd_hold_direct_note(void) +{ + u8 i; + + for (i = 0; i < svc_fwd_hold.nrel; i++) + svc_fwd_rel_direct_note(svc_fwd_hold.ch_handle, + svc_fwd_hold.rel[i].va, + svc_fwd_hold.rel[i].payload); +} + static bool svc_fwd_hold_expired(void) { return time_after(jiffies, svc_fwd_hold.since + @@ -6194,6 +7320,8 @@ static void svc_fwd_hold_expire_check(struct nv_vgpu_device *vgpu, svc_fwd_rel_apply_all(hv, &shim, svc_fwd_hold.rel, svc_fwd_hold.nrel); } + if (!(done && svc_fwd_req.hw_rc == 0)) + svc_fwd_hold_direct_note(); if (done && svc_fwd_req.hw_rc == 0) { svc.fwd_rel_applied++; } else { @@ -6295,6 +7423,7 @@ static bool svc_fwd_hold_poll(struct nv_vgpu_device *vgpu, svc.fwd_relay_fallback++; svc_fwd_rel_apply_all(hv, ch, svc_fwd_hold.rel, svc_fwd_hold.nrel); + svc_fwd_hold_direct_note(); svc.fwd_rel_fallback++; if (!done) svc.fwd_hold_timeouts++; @@ -6320,58 +7449,52 @@ static bool svc_fwd_hold_poll(struct nv_vgpu_device *vgpu, } /* - * Channel vanished while a hold was active. A successfully completed work - * item or an abort (quiesce / channel delete seen before the snapshot lost - * the channel) applies the recorded releases through a shim channel - holds - * exist only for CE channels and the vanished channel object is not required; - * only a copy that did not complete successfully (timeout/failure/cancel) - * drops without applying. The stored engage-time device is preferred over the - * caller's so the releases never land in a different device's address space. + * Channel vanished while a hold was active. Any resolved hold (completed, + * aborted, failed or expired) applies the recorded releases through a shim + * channel - holds exist only for CE channels and the vanished channel object + * is not required. A copy that did not complete successfully is no longer + * dropped: the releases land through the same bounded, forward-only apply, so + * a decoded release is never lost to a hardware copy that did not run. The + * stored engage-time device is preferred over the caller's so the releases + * never land in a different device's address space. */ static void svc_fwd_hold_gone_finish(struct nv_vgpu_device *vgpu) { struct nv_vgpu_device *hv = svc_fwd_hold.vgpu ? svc_fwd_hold.vgpu : vgpu; + struct svc_channel shim; bool done = smp_load_acquire(&svc_fwd_req.done); bool aborted = READ_ONCE(svc_fwd_hold.aborted); if (!done && !aborted && !svc_fwd_hold_expired()) return; - if ((done && svc_fwd_req.hw_rc == 0) || aborted) { - struct svc_channel shim; - - /* - * P6b wave 2: an abort (or a vanished channel after a - * failure/timeout) applies its releases through the shim; - * author the skipped data once first unless the hardware copy - * itself completed successfully. - */ - if (!(done && svc_fwd_req.hw_rc == 0)) - svc_fwd_hold_soft_copy(hv); - if (hv) { - memset(&shim, 0, sizeof(shim)); - shim.valid = true; - shim.h_object = svc_fwd_hold.ch_handle; - shim.engine_type = 1; /* holds are CE-only */ - svc_fwd_rel_apply_all(hv, &shim, svc_fwd_hold.rel, - svc_fwd_hold.nrel); - } - if (done && svc_fwd_req.hw_rc == 0) - svc.fwd_rel_applied++; - else { - svc.fwd_rel_fallback++; - if (!done) - svc.fwd_hold_timeouts++; - } + /* + * P6b wave 2: author the skipped data once first unless the hardware + * copy itself completed successfully. + */ + if (!(done && svc_fwd_req.hw_rc == 0)) + svc_fwd_hold_soft_copy(hv); + if (hv) { + memset(&shim, 0, sizeof(shim)); + shim.valid = true; + shim.h_object = svc_fwd_hold.ch_handle; + shim.engine_type = 1; /* holds are CE-only */ + svc_fwd_rel_apply_all(hv, &shim, svc_fwd_hold.rel, + svc_fwd_hold.nrel); + } + if (done && svc_fwd_req.hw_rc == 0) { + svc.fwd_rel_applied++; } else { /* - * Canceled (expired without a successful copy): wave 1.5 - * drops the releases, but the skipped data is still authored - * once before the hold is released so the destination is - * correct if anything else observes it. + * No successful hardware copy ever engaged (failed, + * aborted or the bounded window expired): the recorded + * releases were applied directly above, exactly once. */ - svc_fwd_hold_soft_copy(hv); + svc_fwd_hold_direct_note(); + svc.fwd_rel_fallback++; svc.fwd_hold_canceled++; + if (!done) + svc.fwd_hold_timeouts++; } /* * P6c: a vanished channel cannot run the relay verification edge; a @@ -6409,10 +7532,12 @@ static void svc_fwd_hold_stop_flush(struct nv_vgpu_device *vgpu) svc_fwd_rel_apply_all(hv, &shim, svc_fwd_hold.rel, svc_fwd_hold.nrel); } - if (done && svc_fwd_req.hw_rc == 0) + if (done && svc_fwd_req.hw_rc == 0) { svc.fwd_rel_applied++; - else + } else { + svc_fwd_hold_direct_note(); svc.fwd_rel_fallback++; + } if (svc_fwd_hold.relay_armed) svc.fwd_relay_fallback++; svc_fwd_hold_clear(); @@ -6976,6 +8101,22 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) bool held = false; /* + * Safety net: a GR channel's completion-counter slot must be + * registered - and any release decoded before the slot was + * known flushed - even when this pass cannot read the ring or + * USERD and one of the early-outs below skips the publish at + * the end of the pass. Without it a release recorded while + * push decode stayed broken never lands and the client's + * userspace tracker polls forever. Idempotent and cheap: once + * the slot is registered only the pending-payload check + * remains. + */ + if (!ch->engine_type) { + svc_counter_autoreg(vgpu, ch, n_gr); + svc_counter_flush_pending(vgpu, ch); + } + + /* * P6c: while a relay hold owns this channel, the hold's * verified success edge is the single completion post; skip * the per-pass notifier so it cannot fire before the guest @@ -7146,8 +8287,8 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) if (pb) { int rescue_rc; bool unreadable = - svc_read_va_trusted(vgpu, va, - pb, len) != 0; + svc_read_va_push(vgpu, ch, va, + pb, len) != 0; bool injected = false; bool allow_ops = (ch->engine_type != 0); @@ -7157,14 +8298,16 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) * (content-scanned map; observed firmware * and kernel data pages). Handle it like * an unreadable push so the content rescue - * can find the real page. Exception: a - * host-injected translation is verified - * truth, and real CE release pushes start - * with an INC_METHOD whose address is not - * zero (observed 0x200381c0, addr 0x700): - * decode those release-only instead of - * discarding them, so the client's - * tracking-semaphore releases still apply. + * can find the real page. An injected pin + * is treated as verified truth by default, + * but content is the arbiter: a pin whose + * page does not parse is re-read from the + * remaining candidates (validated pin / + * window / trusted) and only a plausible + * read replaces it; if that also fails the + * old release-only decode of the injected + * page stands, so the client's tracking- + * semaphore releases still apply. */ if (!unreadable) { u64 ipa; @@ -7174,8 +8317,34 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) &iap) == 0); if (!svc_method_stream_plausible(pb, len)) { svc.false_trans++; - if (!injected) + if (injected) { + u8 *fb = kmalloc(len, + GFP_KERNEL); + + if (fb) { + if (!svc_read_va_push_ex(vgpu, ch, va, + fb, len, true) && + svc_method_stream_plausible(fb, len)) { + memcpy(pb, fb, len); + injected = false; + svc.pushpg_fallback++; + if (gsp_sema_dbg && + svc_pushpg_fb_log < 16) { + u64 wpa = 0; + bool win = !svc_pb_win_lookup(ch, + va & ~(u64)(SEMA_PAGE_SIZE - 1), &wpa); + + svc_pushpg_fb_log++; + pr_info("sema-pushpg: h=0x%x va=0x%llx fallback pin=0 win=%d\n", + ch->h_object, va, + win ? 1 : 0); + } + } + kfree(fb); + } + } else { unreadable = true; + } } } @@ -7306,6 +8475,16 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) svc_fwd_rel_apply_all(vgpu, ch, svc_fwd_rel, svc_fwd_rel_n); + { + u8 ri; + + for (ri = 0; + ri < svc_fwd_rel_n; + ri++) + svc_fwd_rel_direct_note(ch->h_object, + svc_fwd_rel[ri].va, + svc_fwd_rel[ri].payload); + } /* * P6c: a * selected @@ -7339,7 +8518,7 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) u32 rel_n2 = svc_rel_count; if (!svc_rescue_push_page(vgpu, ch, va, len) && - !svc_read_va_trusted(vgpu, va, pb, len)) + !svc_read_va_push(vgpu, ch, va, pb, len)) svc_decode_stream(vgpu, pb, len, 0, false, ch, idx, va, true); @@ -7383,6 +8562,7 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) if (time_before(jiffies, ch->defer_since + msecs_to_jiffies(gsp_sema_ce_grace_ms))) { + svc.gpfifo_held++; svc.gpfifo_nomap++; kfree(pb); break; @@ -7390,16 +8570,15 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) pr_info("sema-chan: skipping stale entry h=0x%x va=0x%llx len=%u after %u ms grace\n", ch->h_object, va, len, gsp_sema_ce_grace_ms); - if ((gsp_sema_fake_rel || - gsp_sema_rel_recover) && - !svc_fake_release(vgpu, ch)) { - if (!ch->defer_since) - ch->defer_since = - jiffies; - svc.gpfifo_nomap++; - kfree(pb); - break; - } + /* + * Grace expired: deliver through the + * exact-slot fallback and consume the + * entry regardless. A missing + * translation is queued for the retry + * pass; it must never hold the channel. + */ + if (!svc_fake_release(vgpu, ch)) + svc.gpfifo_starved++; } else { ch->expected_sem_payload++; } @@ -7423,28 +8602,35 @@ static void svc_pump_channels(struct nv_vgpu_device *vgpu) * lets the client read unstaged * memory (observed: cuMemcpyDtoH * returning zeros after its HtoD push - * was skipped unreadable). The push - * page's translation usually lands - * within one service pass (content - * discovery or the host watcher), so - * keep the entry pending for the grace - * window first and only then fall back - * to acknowledging the release. + * was skipped unreadable). With a + * decoded release VA (last_rel_va) + * the exact slot is already known: + * deliver or queue it and consume the + * entry at once - never hold the + * channel for a translation that may + * never arrive. Only a channel with no + * exact slot gets one bounded grace so + * a late translation can teach it the + * slot; after that it is consumed + * either way (the release content + * sweep is the safety net). */ - if (gsp_sema_fake_rel && ch->engine_type) { + if (ch->engine_type && + !ch->last_rel_va && + !gsp_sema_fake_rel) { if (!ch->defer_since) ch->defer_since = jiffies; if (time_before(jiffies, ch->defer_since + msecs_to_jiffies(gsp_sema_ce_grace_ms))) { + svc.gpfifo_held++; svc.gpfifo_nomap++; kfree(pb); break; } } -if (gsp_sema_fake_rel && - svc_fake_release(vgpu, ch)) { + if (svc_fake_release(vgpu, ch)) { ch->defer_since = 0; svc_replay_note(ch, idx, put); ch->last_get = (idx + 1) % @@ -7454,29 +8640,6 @@ if (gsp_sema_fake_rel && kfree(pb); continue; } - if (gsp_sema_fake_rel && - ch->engine_type) { - /* - * CE channel and the - * pool page is not - * translated yet: the - * pin lands a moment - * later, so keep the - * entry pending - * instead of burning a - * multi-second RAM - * rescue. Consuming it - * now would drop the - * release the client - * waits on. - */ - if (!ch->defer_since) - ch->defer_since = - jiffies; - svc.gpfifo_nomap++; - kfree(pb); - break; - } if (svc.false_trans && svc.false_trans < 8) pr_info("sema-svc: false translation for push va=0x%llx (h=0x%x len=%u) - rescuing\n", va, ch->h_object, len); @@ -7485,8 +8648,8 @@ if (gsp_sema_fake_rel && if (!rescue_rc) { ch->rescue_fail = 0; /* Rescue succeeded, retry the read. */ - if (!svc_read_va_trusted(vgpu, va, pb, - len)) { + if (!svc_read_va_push(vgpu, ch, va, pb, + len)) { /* * Never run work decoded from a * rescued page: the rescue matches @@ -7574,6 +8737,7 @@ if (gsp_sema_fake_rel && if (time_before(jiffies, ch->defer_since + msecs_to_jiffies(gsp_sema_ce_grace_ms))) { + svc.gpfifo_held++; svc.gpfifo_nomap++; kfree(pb); break; @@ -7581,37 +8745,26 @@ if (gsp_sema_fake_rel && pr_info("sema-chan: skipping stale entry h=0x%x va=0x%llx len=%u after %u ms grace\n", ch->h_object, va, len, gsp_sema_ce_grace_ms); - if (gsp_sema_fake_rel || - gsp_sema_rel_recover) { + { u64 faked_before = svc.releases_faked; - if (!svc_fake_release(vgpu, ch)) { - /* - * The release could not be - * delivered (pool page not - * translated yet): keep the - * entry pending so a pin that - * lands shortly still applies - * it instead of silently - * dropping the client's wait. - */ - if (!ch->defer_since) - ch->defer_since = - jiffies; - svc.gpfifo_nomap++; - kfree(pb); - break; - } + /* + * Deliver through the + * exact-slot fallback and + * consume regardless; a + * missing translation is + * queued for the retry pass. + */ + if (!svc_fake_release(vgpu, ch)) + svc.gpfifo_starved++; if (!gsp_sema_fake_rel && svc.releases_faked != - faked_before) { - svc.releases_recovered++; + faked_before) pr_info("sema-chan: recovery release h=0x%x rank=%u expected=%u (push page unreadable after rescue)\n", ch->h_object, ch->ce_index, ch->expected_sem_payload); - } } ch->rescue_fail = 0; ch->defer_since = 0; @@ -7657,24 +8810,19 @@ if (gsp_sema_fake_rel && pr_info("sema-chan: skipping stale entry h=0x%x va=0x%llx len=%u after %u failed rescues\n", ch->h_object, va, len, ch->rescue_fail); - if (gsp_sema_fake_rel && - !svc_fake_release(vgpu, ch)) { - /* - * The release could not be - * delivered (pool page not - * translated yet): keep the - * entry pending so a pin that - * lands shortly still applies - * it instead of silently - * dropping the client's wait. - */ - if (!ch->defer_since) - ch->defer_since = - jiffies; - svc.gpfifo_nomap++; - kfree(pb); - break; - } + /* + * Never hold on a missing + * exact slot: consume the + * entry and let the content + * sweep / counter publish carry + * the client's wait. GR + * channels have no exact slot + * (svc_fake_release returns + * false) and are counted as + * consumed-without-delivery. + */ + if (!svc_fake_release(vgpu, ch)) + svc.gpfifo_starved++; ch->rescue_fail = 0; ch->defer_since = 0; if (gsp_sema_fake_rel || @@ -7738,11 +8886,15 @@ if (gsp_sema_fake_rel && held = svc_fwd_hold.active && svc_fwd_hold.vgpu == vgpu && svc_fwd_hold.ch_handle == ch->h_object; - /* Advance the client-visible completion counter. */ - if (!held) { - svc_counter_autoreg(vgpu, ch, n_gr); + /* + * Advance the client-visible completion counter. The slot was + * already registered (and any pending release flushed) by the + * safety net at the top of the pass, so this must not + * re-register: once per pass is enough and the derivation is + * not free. + */ + if (!held) svc_counter_publish(vgpu, ch, put); - } spin_lock_irqsave(&svc.chan_lock, flags); for (k = 0; k < SVC_MAX_CHANNELS; k++) { @@ -8365,6 +9517,12 @@ static int svc_thread(void *arg) } /* + * Host CUDA bridge: marshal one queued launch into a daemon + * job and resolve completions. Inert until a launch is seen. + */ + gsp_cuda_bridge_service(vgpu); + + /* * Releases whose target VA was not translatable when the scan * saw them: retry every pass, so a pin or a learned pair applies * them immediately instead of on the next full sweep. @@ -8511,6 +9669,8 @@ int gsp_sema_service_start(void) svc.task = NULL; return ret; } + if (gsp_cuda_bridge_init()) + pr_warn("sema-svc: cuda bridge debugfs unavailable\n"); return 0; } @@ -8544,4 +9704,97 @@ void gsp_sema_service_stop(void) destroy_workqueue(svc_fwd_wq); svc_fwd_wq = NULL; } + /* Thread is gone: release the bridge's pending jobs and debugfs files. */ + gsp_cuda_bridge_fini(); +} + +/* ------------------------------------------------------------------ */ +/* host CUDA bridge exports (gsp_cuda_bridge.c) */ +/* ------------------------------------------------------------------ */ + +/* + * Page-wise GPU-VA read for the bridge: resolve each page through the same + * trusted sources every operand read uses (pins, learned cache, discovered + * map, published page directories, targeted rm_mem resolve) and read through + * the aperture the VA maps to (vidmem shadow vs guest RAM via vfio_dma_rw). + */ +int gsp_sema_service_va_read(struct nv_vgpu_device *vgpu, u64 va, void *buf, + size_t len) +{ + u8 *p = buf; + + while (len) { + size_t chunk = min_t(size_t, len, + SEMA_PAGE_SIZE - + (va & (SEMA_PAGE_SIZE - 1))); + u64 addr = 0; + int ap = SVC_APERTURE_SYS; + + if (svc_lookup(vgpu, va, &addr, &ap)) + return -ENOENT; + if (svc_read_ap(vgpu, addr, svc_ap_of(ap), p, chunk)) + return -EIO; + p += chunk; + va += chunk; + len -= chunk; + } + return 0; +} + +int gsp_sema_service_va_write(struct nv_vgpu_device *vgpu, u64 va, + const void *buf, size_t len) +{ + const u8 *p = buf; + + while (len) { + size_t chunk = min_t(size_t, len, + SEMA_PAGE_SIZE - + (va & (SEMA_PAGE_SIZE - 1))); + u64 addr = 0; + int ap = SVC_APERTURE_SYS; + + if (svc_lookup(vgpu, va, &addr, &ap)) + return -ENOENT; + if (svc_write_ap(vgpu, addr, svc_ap_of(ap), (void *)p, chunk)) + return -EIO; + p += chunk; + va += chunk; + len -= chunk; + } + return 0; +} + +int gsp_sema_service_va_resolve(struct nv_vgpu_device *vgpu, u64 va, u64 *pa, + u32 *ap) +{ + u64 addr = 0; + int iap = SVC_APERTURE_SYS; + + if (svc_lookup(vgpu, va, &addr, &iap)) + return -ENOENT; + if (pa) + *pa = addr; + if (ap) + *ap = svc_ap_of(iap); + return 0; +} + +int gsp_sema_service_sem_read(struct nv_vgpu_device *vgpu, u64 va, u32 *val) +{ + return svc_sem_read(vgpu, va, val); +} + +int gsp_sema_service_sem_write(struct nv_vgpu_device *vgpu, u32 h, u64 va, + u32 val) +{ + int rc = svc_sem_write(vgpu, h, va, val); + + /* + * A bridge-completed job is a work completion like any pump release: + * keep the completion hint window open so a guest that starts waiting + * after the write still receives the completion event. + */ + if (!rc) + svc.post_hint_until = jiffies + 10 * HZ; + return rc; } diff --git a/kernel/gsp_sema_service.h b/kernel/gsp_sema_service.h index d5a62e9..530663e 100644 --- a/kernel/gsp_sema_service.h +++ b/kernel/gsp_sema_service.h @@ -155,6 +155,16 @@ void gsp_sema_service_fwd_fault_stats(u64 *matched, u64 *unmatched, extern bool gsp_sema_gr_rings; /* + * In-kernel GR completion-counter slot derivation (svc_counter_derive): on an + * injected-pin miss, resolve a counter slot from the structural translations + * (the driver-published page directories, the self-discovered directory and + * the targeted rm_mem record) and author the release there instead of waiting + * for the external pin watcher. Default on; set 0 to restore the + * injected-pin-only behaviour for an A/B without a rebuild. + */ +extern bool gsp_sema_counter_derive; + +/* * Acknowledge tracking-semaphore releases from CE pushes the pump could not * read: write the expected payload into the channel's pool semaphore so the * driver's wait clears immediately instead of after the content sweep. diff --git a/kernel/main.c b/kernel/main.c index 39c71c3..7001b3d 100644 --- a/kernel/main.c +++ b/kernel/main.c @@ -2551,6 +2551,16 @@ module_param_named(sema_gr_rings, gsp_sema_gr_rings, bool, 0644); MODULE_PARM_DESC(sema_gr_rings, "Auto-discover+pin GR channel rings (default 1)"); /* + * In-kernel GR completion-counter slot derivation from the structural + * translations (published page directories, self-discovered directory, + * targeted rm_mem record), used when a release's counter page has no injected + * pin yet. Default on; set 0 to restore the injected-pin-only behaviour for + * an A/B without a rebuild. + */ +module_param_named(sema_counter_derive, gsp_sema_counter_derive, bool, 0644); +MODULE_PARM_DESC(sema_counter_derive, "Derive GR completion-counter slots in-kernel from structural translations (default 1)"); + +/* * Acknowledge releases from unreadable CE pushes directly (tracking-semaphore * fallback). The push's data writes are lost either way when the content * rescue cannot find the page; writing the expected payload keeps the diff --git a/tools/cuda-ab/ab.sh b/tools/cuda-ab/ab.sh index 07f6b24..8ae05ee 100755 --- a/tools/cuda-ab/ab.sh +++ b/tools/cuda-ab/ab.sh @@ -11,6 +11,7 @@ # tools/cuda-ab/ab.sh guest # push the binary + run in the running VM # tools/cuda-ab/ab.sh compare # diff the stable result lines # tools/cuda-ab/ab.sh all # host + guest + compare +# tools/cuda-ab/ab.sh victory # push victory_mail to the VM (runner executes it) # # The binary links the CUDA runtime statically, so the guest needs no CUDA # toolkit - only the guest driver's libcuda.so. @@ -20,8 +21,9 @@ HERE="$(cd "$(dirname "$0")" && pwd)" REPO="$(cd "$HERE/../.." && pwd)" VMEXEC="$REPO/scripts/vgpu_vm.py" RUN="$HERE/run" -SRC="$HERE/vector_add.cu" -BIN="$RUN/vector_add" +WORKLOAD="${WORKLOAD:-vector_add}" +SRC="$HERE/$WORKLOAD.cu" +BIN="$RUN/$WORKLOAD" mkdir -p "$RUN" @@ -44,19 +46,19 @@ push_bin() { off=0 size=70000 echo "[*] pushing $(stat -c%s "$BIN") bytes to guest" - "$VMEXEC" exec "rm -f /tmp/vadd.b64" >/dev/null 2>&1 + "$VMEXEC" exec "rm -f /tmp/$WORKLOAD.b64" >/dev/null 2>&1 while [ $off -lt $total ]; do chunk=${b64:$off:$size} - "$VMEXEC" exec "echo -n '$chunk' >> /tmp/vadd.b64" >/dev/null 2>&1 + "$VMEXEC" exec "echo -n '$chunk' >> /tmp/$WORKLOAD.b64" >/dev/null 2>&1 off=$((off + size)) done - "$VMEXEC" exec "base64 -d /tmp/vadd.b64 > /tmp/vector_add && chmod +x /tmp/vector_add && rm -f /tmp/vadd.b64 && echo PUSH_OK" + "$VMEXEC" exec "base64 -d /tmp/$WORKLOAD.b64 > /tmp/$WORKLOAD && chmod +x /tmp/$WORKLOAD && rm -f /tmp/$WORKLOAD.b64 && echo PUSH_OK" } run_guest() { push_bin echo "[*] guest run (emulated vGPU)" - "$VMEXEC" exec "/tmp/vector_add" 2>&1 | tee "$RUN/guest.txt" + "$VMEXEC" exec "/tmp/$WORKLOAD" 2>&1 | tee "$RUN/guest.txt" } compare() { @@ -84,5 +86,10 @@ host) run_host ;; guest) run_guest ;; compare) compare ;; all) run_host; run_guest; compare ;; -*) echo "usage: $0 [host|guest|compare|all]"; exit 1 ;; +victory) + WORKLOAD=victory_mail + SRC="$HERE/$WORKLOAD.cu" + BIN="$RUN/$WORKLOAD" + push_bin ;; +*) echo "usage: $0 [host|guest|compare|all|victory]"; exit 1 ;; esac diff --git a/tools/cuda-ab/run/victory_mail b/tools/cuda-ab/run/victory_mail Binary files differnew file mode 100755 index 0000000..b04f59e --- /dev/null +++ b/tools/cuda-ab/run/victory_mail diff --git a/tools/cuda-ab/victory_mail.cu b/tools/cuda-ab/victory_mail.cu new file mode 100644 index 0000000..e83fc1b --- /dev/null +++ b/tools/cuda-ab/victory_mail.cu @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * victory_mail.cu - the Open-IOV victory program. + * + * Run from the guest. Two kernels execute on the physical GPU: + * + * mail_kernel - the GPU itself writes every character of the message into + * device memory; the host only copies the bytes back. + * vadd - the classic vectorAdd acceptance check, verified element + * by element against the host-computed result. + * + * The host prints the message only after it has been authored by GPU + * execution and copied back byte-for-byte, so the line below is proof that a + * kernel really ran on the device: + * + * the little gpu man says you've got mail + * + * Output lines are stable and greppable: + * + * DEVICE <name> cc=<major>.<minor> sm=<count> mem=<bytes> + * MAIL <message> + * VADD n=<count> bad=<mismatches> checksum=<sum> + * STATUS OK|FAIL + * + * Build with a static CUDA runtime (--cudart=static) so the guest only needs + * the driver's libcuda.so, not a CUDA toolkit installation: + * + * nvcc -O2 --cudart=static -o victory_mail victory_mail.cu + * + * Then, from the guest: /tmp/victory_mail + */ + +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <cuda_runtime.h> + +#define MAIL "the little gpu man says you've got mail" + +#define CHECK(x) \ + do { \ + cudaError_t e_ = (x); \ + if (e_ != cudaSuccess) { \ + printf("CUDA_ERROR %s at line %d\n", \ + cudaGetErrorString(e_), __LINE__); \ + printf("STATUS FAIL\n"); \ + return 1; \ + } \ + } while (0) + +/* The message lives in device code: the GPU both provides and writes it. */ +__device__ const char kMail[] = MAIL; + +/* Each thread writes exactly one character of the message. */ +__global__ void mail_kernel(char *out, int len) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + + if (i < len) + out[i] = kMail[i]; +} + +__global__ void vadd(const float *a, const float *b, float *c, int n) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + + if (i < n) + c[i] = a[i] + b[i]; +} + +int main(void) +{ + cudaDeviceProp prop; + const int len = (int)strlen(MAIL); + const int n = 1 << 20; + size_t bytes = (size_t)n * sizeof(float); + char *d_mail, h_mail[128]; + float *h_a, *h_b, *h_c; + float *d_a, *d_b, *d_c; + double sum = 0.0; + int bad = 0, i; + + /* Unbuffered: progress must be visible even if a later call blocks. */ + setvbuf(stdout, NULL, _IONBF, 0); + + CHECK(cudaGetDeviceProperties(&prop, 0)); + printf("DEVICE %s cc=%d.%d sm=%d mem=%llu\n", prop.name, + prop.major, prop.minor, prop.multiProcessorCount, + (unsigned long long)prop.totalGlobalMem); + + /* The GPU writes the mail. */ + CHECK(cudaMalloc(&d_mail, len)); + mail_kernel<<<1, 64>>>(d_mail, len); + CHECK(cudaGetLastError()); + CHECK(cudaDeviceSynchronize()); + memset(h_mail, 0, sizeof(h_mail)); + CHECK(cudaMemcpy(h_mail, d_mail, len, cudaMemcpyDeviceToHost)); + if (memcmp(h_mail, MAIL, len) != 0) { + printf("MAIL bad=<kernel output did not match>\n"); + printf("STATUS FAIL\n"); + return 1; + } + printf("MAIL %s\n", h_mail); + + /* The classic acceptance check: vectorAdd. */ + h_a = (float *)malloc(bytes); + h_b = (float *)malloc(bytes); + h_c = (float *)malloc(bytes); + if (!h_a || !h_b || !h_c) + return 1; + + for (i = 0; i < n; i++) { + h_a[i] = (float)(i % 1000) * 0.5f; + h_b[i] = (float)(i % 997) * 0.25f; + } + + CHECK(cudaMalloc(&d_a, bytes)); + CHECK(cudaMalloc(&d_b, bytes)); + CHECK(cudaMalloc(&d_c, bytes)); + CHECK(cudaMemcpy(d_a, h_a, bytes, cudaMemcpyHostToDevice)); + CHECK(cudaMemcpy(d_b, h_b, bytes, cudaMemcpyHostToDevice)); + + vadd<<<(n + 255) / 256, 256>>>(d_a, d_b, d_c, n); + CHECK(cudaGetLastError()); + CHECK(cudaDeviceSynchronize()); + CHECK(cudaMemcpy(h_c, d_c, bytes, cudaMemcpyDeviceToHost)); + + for (i = 0; i < n; i++) { + if (h_c[i] != h_a[i] + h_b[i]) + bad++; + sum += h_c[i]; + } + + printf("VADD n=%d bad=%d checksum=%.6f\n", n, bad, sum); + + if (bad != 0) { + printf("STATUS FAIL\n"); + return 1; + } + + printf("the little gpu man says you've got mail\n"); + printf("STATUS OK\n"); + + CHECK(cudaFree(d_mail)); + CHECK(cudaFree(d_a)); + CHECK(cudaFree(d_b)); + CHECK(cudaFree(d_c)); + free(h_a); + free(h_b); + free(h_c); + return 0; +} diff --git a/tools/cuda-bridge/INTEGRATION.md b/tools/cuda-bridge/INTEGRATION.md new file mode 100644 index 0000000..fcdce80 --- /dev/null +++ b/tools/cuda-bridge/INTEGRATION.md @@ -0,0 +1,115 @@ +# vgsp.ko integration (v2, live path) + +The bridge is a userspace daemon with a fixed-size file protocol so the vGPU +kernel module drives it without sharing kernel/userspace memory. The v2 records +are frozen in `PROTOCOL.md` and mirrored by static asserts in +`kernel/gsp_cuda_bridge.c` and `tools/cuda-bridge/cuda_bridge.c`. + +## Expected flow + +1. **Detect launch.** `vgsp.ko`'s service thread decodes the guest's inline QMD + on the emulated GR compute channel (`gsp_sema_service.c`), classifies the + launch shape (mail vs vadd) and reads the parameter bank's pointers/count. +2. **Marshal + publish.** The module resolves the guest source VAs, copies the + inputs into its own buffer (vadd: `a` then `b`; mail: none) and publishes a + 256-byte `CUBB2` job with a fresh `seq` in `cuda_bridge_job` while serving + the input blob from `cuda_bridge_in`. One job is published at a time + (`CUBB2_PEND_MAX` slots, oldest first). +3. **Daemon executes.** `cuda_bridge --bridge --job + /sys/kernel/debug/vgsp/cuda_bridge_job --in + /sys/kernel/debug/vgsp/cuda_bridge_in --out + /sys/kernel/debug/vgsp/cuda_bridge_out --done + /sys/kernel/debug/vgsp/cuda_bridge_done` polls the job file (default 50 ms), + reads the inputs, executes the requested kernel on the physical GPU + (embedded PTX; `cuMemcpyHtoD` / `cuLaunchKernel` / `cuMemcpyDtoH`), writes + the output blob to `cuda_bridge_out`, then always writes a `CBD2` done + record to `cuda_bridge_done`. +4. **Module completes the guest.** `vgsp.ko` pops done records on the service + thread, matches `seq`, copies the output blob through the VA translation + back into the guest destination, applies the QMD release semaphore, and the + guest's `cudaDeviceSynchronize` completes. + +Everything is inert until a launch is seen; the path can be switched off with +the `cuda_bridge` module parameter (default on). + +## Daemon invocation (frozen CLI) + +``` +cuda_bridge --bridge \ + --job /sys/kernel/debug/vgsp/cuda_bridge_job \ + --in /sys/kernel/debug/vgsp/cuda_bridge_in \ + --out /sys/kernel/debug/vgsp/cuda_bridge_out \ + --done /sys/kernel/debug/vgsp/cuda_bridge_done +``` + +`--poll-ms` overrides the 50 ms default; `--once` exits after one job +(bring-up only). The legacy v1 `--job J --done D --ram R` GPA mode is separate +and unchanged. + +## Debugfs permissions + +`cuda_bridge_job`/`_in` are `0444` and `_out`/`_done` are `0200`, owned by +root. When the daemon runs unprivileged, the runner opens them first: + +``` +for f in job in out done status; do + sudo chmod 666 /sys/kernel/debug/vgsp/cuda_bridge_$f 2>/dev/null +done +``` + +## seq semantics + +* The module's counter starts at 1 and only increases; a job record is + immutable while pending. +* The daemon records `last_seq` and ignores any valid record with + `seq <= last_seq` (duplicate/replay protection). The first valid record + after daemon startup always executes, so a daemon restart can re-run at most + the single stale job still parked in the interface. +* A done record whose `seq` matches no pending module job is counted `stale` + and dropped; malformed magic/version is counted `malformed`. Both are + visible in `cuda_bridge_status`. + +## Logging + +The daemon logs one line per executed job (stdout only; correctness never +depends on it): + +``` +CUBB2 seq=42 op=2 n=1048576 gpu="NVIDIA GeForce RTX 2080 Super with Max-Q Design" \ + kernel_ns=41919 status=ok checksum=392344090.500000 bad=0 out_bytes=4194304 +``` + +`status=err` lines are preceded by a `bridge: job seq=... rejected/failed: ...` +stderr line naming the cause (bounds, bad opcode, input transport, output +transport). Every job — success or failure — gets a done record; failures +never leave the module waiting. + +## Limits and error mapping + +* `n <= 1<<26`; `in_bytes`, `out_bytes <= 32 MiB`; mail `n <= 4096` (module + param-length limit). Violations: `status = 1` (`bad job`), `err_code = 22` + (`EINVAL`). +* Bad opcode: `status = 1`, `err_code = opcode`. +* Input read failure: `status = 3`, `err_code = 5` (`EIO`); allocation + failure: `err_code = 12` (`ENOMEM`). +* CUDA failure: `status = 2`, `err_code = CUresult`. +* Output write failure: `status = 3`, `err_code = errno`. +* `bad` is data quality, not execution failure: `status` stays 0 when the + kernel ran and the output was transported. + +## Risks / open questions + +* **Single writer per job interface.** One job is visible at a time and the + module serialises publication; the daemon's `last_seq` only protects + duplicates. Multiple daemons against one mdev are unsupported. +* **Daemon lifetime / restarts.** On restart `last_seq` resets and the one + parked job may run again. The module can clear or bump the job when (re)starting + the daemon; the re-run is idempotent for mail/vadd. +* **Timeouts.** A stuck job has no protocol-level timeout; the module bounds + `n`/bytes and the runner has bounded waits. A watchdog belongs on the + module side. +* **Device selection.** The daemon always uses `cuDeviceGet(0)`; multi-GPU + hosts need a `--device` selector and the module must agree on the physical + GPU behind the mdev. +* **PTX target.** The embedded PTX is `sm_75` (JIT-ed by the driver for the + physical GPU); a newer-only GPU needs the build-time `PTX_ARCH` bumped. diff --git a/tools/cuda-bridge/Makefile b/tools/cuda-bridge/Makefile new file mode 100644 index 0000000..d75a7c5 --- /dev/null +++ b/tools/cuda-bridge/Makefile @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Build the host-side CUDA bridge daemon. +# +# Kernels are written in CUDA C, compiled to PTX at build time by nvcc, and +# embedded into the binary by embed_ptx.py, so cuda_bridge needs only the +# driver's libcuda at runtime. kernels_ptx.h is a generated file; if it is +# already present the build works without nvcc. + +CC ?= cc +NVCC ?= nvcc +PTX_ARCH ?= sm_75 +CFLAGS ?= -O2 -Wall -Wextra -std=gnu11 +LDLIBS ?= -lcuda + +all: cuda_bridge + +kernels.ptx: kernels.cu + $(NVCC) -arch=$(PTX_ARCH) -ptx $< -o $@ + +kernels_ptx.h: kernels.ptx embed_ptx.py + python3 embed_ptx.py kernels.ptx kernels_ptx.h + +cuda_bridge: cuda_bridge.c kernels_ptx.h + $(CC) $(CFLAGS) cuda_bridge.c -o $@ $(LDLIBS) + +test: cuda_bridge + ./cuda_bridge --test + +bridge-test: cuda_bridge + python3 bridge_selftest.py + +layout-check: + python3 check_module_layout.py + +mutate-test: cuda_bridge + ./cuda_bridge --mutation-test + +bench: cuda_bridge + ./cuda_bridge --bench + +clean: + rm -f cuda_bridge kernels.ptx kernels_ptx.h + +.PHONY: all test bridge-test layout-check mutate-test bench clean diff --git a/tools/cuda-bridge/PROTOCOL.md b/tools/cuda-bridge/PROTOCOL.md new file mode 100644 index 0000000..c54b8e7 --- /dev/null +++ b/tools/cuda-bridge/PROTOCOL.md @@ -0,0 +1,116 @@ +# CUDA Bridge Protocol v2 (CUBB2 / CBD2) + +Frozen transport between `vgsp.ko` (`kernel/gsp_cuda_bridge.c`, debugfs +producer/consumer) and the host daemon (`cuda_bridge --bridge`). All fields are +**little-endian**, records are **256 bytes** with **natural alignment** (no +packing; both sides compile for x86_64). The daemon and the module each carry +static asserts pinning every offset below, so a layout drift fails the build on +either side. + +* job magic `0x32424243` (`'CBB2'` LE), done magic `0x32444243` (`'CBD2'` LE), + version `2`. +* Limits: `n <= 1<<26` (67 108 864) and `in_bytes`/`out_bytes <= 32 MiB` + (33 554 432). Violations are rejected with `status = 1` and a clear log line + before any GPU work or input read. + +## Job record `CUBB2` (producer: `vgsp.ko`; consumer: `cuda_bridge --bridge`) + +| Offset | Size | Type | Field | Meaning | +|---|---|---|---|---| +| 0x00 | 4 | u32 | `magic` | `0x32424243` (`'CBB2'` LE) | +| 0x04 | 4 | u32 | `version` | 2 | +| 0x08 | 8 | u64 | `seq` | module job id, monotonic from 1; daemon ignores `seq <= last_seq` | +| 0x10 | 4 | u32 | `opcode` | 1 = mail, 2 = vadd | +| 0x14 | 12 | u32[3] | `grid[3]` | launch grid X/Y/Z (0 = daemon default) | +| 0x20 | 12 | u32[3] | `block[3]` | launch block X/Y/Z (0 = daemon default) | +| 0x2c | 4 | u32 | `shared` | dynamic shared memory bytes | +| 0x30 | 4 | u32 | `n` | elements (vadd) or string length (mail) | +| 0x38 | 8 | u64 | `in_bytes` | input blob size: 0 (mail), `8*n` (vadd: a then b) | +| 0x40 | 8 | u64 | `out_bytes` | output blob size: `n` (mail), `4*n` (vadd) | +| 0x48 | 8 | u64 | `sem_va` | module-side release-semaphore VA (informational to the daemon) | +| 0x50 | 8 | u64 | `sem_payload` | release payload | +| 0x58 | 4 | u32 | `sem_flags` | bit0 redEn, bit1 oneWord, bit2 enable | +| 0x5c | 4 | u32 | `pad` | 0 | +| 0x60 | 8 | u64 | `prog_va` | guest program/function VA (informational) | +| 0x68 | 4 | u32 | `param_count` | 2 (mail) or 5 (vadd), informational | +| 0x6c | 4 | u32 | `pad2` | 0 | +| 0x70 | 144 | u8[144] | `rsv` | reserved, 0 to 0x100 | + +## Done record `CBD2` (producer: `cuda_bridge`; consumer: `vgsp.ko`) + +| Offset | Size | Type | Field | Meaning | +|---|---|---|---|---| +| 0x00 | 4 | u32 | `magic` | `0x32444243` (`'CBD2'` LE) | +| 0x04 | 4 | u32 | `version` | 2 | +| 0x08 | 8 | u64 | `seq` | echo of the job seq | +| 0x10 | 4 | u32 | `status` | 0 = ok, 1 = bad job, 2 = CUDA error, 3 = transport/RAM error | +| 0x14 | 4 | u32 | `bad` | `c[i] != a[i]+b[i]` mismatches (mail: byte mismatches) | +| 0x18 | 8 | u64 | `checksum` | IEEE-754 double bits: sum of `c[i]` (mail: 0.0) | +| 0x20 | 8 | u64 | `kernel_ns` | CUDA-event elapsed time, kernel only | +| 0x28 | 4 | u32 | `out_bytes_written` | output bytes actually written to `out` | +| 0x2c | 4 | u32 | `err_code` | CUresult, errno or opcode when `status != 0` | +| 0x30 | 64 | char[64] | `device_name` | physical GPU name, NUL padded | +| 0x70 | 8 | u64 | `host_ns` | wall time for the whole job | +| 0x78 | 136 | u8[136] | `rsv` | reserved, 0 to 0x100 | + +## Daemon loop (`--bridge --job J --in I --out O --done D`) + +1. Every `--poll-ms` (default **50 ms**) read 256 bytes from `job`, chunked, + honoring explicit offsets (`pread`, `ppos`). Absent/short file, zero or + foreign magic, or `version != 2` means **idle**; nothing is executed. +2. For a valid job, ignore it when `seq <= last_executed_seq` (the first valid + record after startup always runs). +3. Read `in_bytes` from `in` chunked at explicit offsets. +4. Execute on the physical GPU (embedded PTX, loaded with `cuModuleLoadData`): + * **opcode 1 (mail)**: no inputs. `mail_kernel<<<grid, block>>>(out, n)` + writes the mail constant `"the little gpu man says you've got mail"` into + a `n`-byte output; the daemon produces exactly `n` bytes + (`out_bytes == n` required). + * **opcode 2 (vadd)**: input is `a` (`4n` bytes) then `b` (`4n` bytes); + `vadd<<<grid, block>>>(a, b, c, n)` computes `c[i] = a[i] + b[i]`; the + daemon produces `c` (`4n` bytes). `in_bytes == 8n` and `out_bytes == 4n` + are required. + * Launch dimensions come from the job's `grid`/`block`/`shared`; zeros use + the defaults (mail: grid 1, block 64; vadd: grid `(n+255)/256`, block + 256). +5. On success write the output blob to `out` chunked at explicit offsets + (`pwrite`, `ppos`), tracking `out_bytes_written`. +6. **Always** write a 256-byte done record to `done` (success *and* failure), + at record offset 0, so the module never waits forever. On failure the + output blob is not written and `status`/`err_code` are set. +7. Log one greppable line per job: + + ``` + CUBB2 seq=42 op=2 n=1048576 gpu="NVIDIA GeForce RTX 2080 Super with Max-Q Design" \ + kernel_ns=41919 status=ok checksum=392344090.500000 bad=0 out_bytes=4194304 + ``` + +## Transport (`/sys/kernel/debug/vgsp/`) + +| File | Mode | Direction | Semantics | +|---|---|---|---| +| `cuda_bridge_job` | 0444 | module -> daemon | serves the oldest pending 256-byte `CUBB2`; short/zero read = none pending | +| `cuda_bridge_in` | 0444 | module -> daemon | that job's input blob (`in_bytes`), ppos-chunked | +| `cuda_bridge_out` | 0200 | daemon -> module | ppos-chunked output blob; offset 0 starts a new accumulation | +| `cuda_bridge_done` | 0200 | daemon -> module | 256-byte `CBD2`; a write starting on a 256-byte boundary starts a fresh record | +| `cuda_bridge_status` | 0444 | module -> all | text counters (`jobs= completed= failed= last_seq= ...`) | + +The debugfs files are `0444`/`0200` (root); an unprivileged daemon needs the +runner's `chmod 666` pass before it starts. Production paths are the debugfs +files above; tests use plain files. + +## Module-side completion + +`vgsp.ko` pops done records, validates magic/version, matches `seq` against the +pending table, and requires `out` bytes for the job before copying the output +back into the resolved guest destination. It then applies the QMD release +semaphore (`sem_va`/`sem_payload`/`sem_flags`) so the guest's synchronize +completes. `status != 0` fails the job; `bad` is data, not an execution +failure. + +## Appendix: v1 (legacy, offline only) + +The original 128-byte packed `"CUBB"`/`"CUBD"` v1 records and the +`--job J --done D --ram R` GPA/RAM-file mode remain for the offline self-test, +`--test`, `--mutation-test` and `--bench`; they are **not** the module +interface. v1 details are preserved in git history and in `cuda_bridge.c`. diff --git a/tools/cuda-bridge/README.md b/tools/cuda-bridge/README.md new file mode 100644 index 0000000..8ac0b6d --- /dev/null +++ b/tools/cuda-bridge/README.md @@ -0,0 +1,104 @@ +# CUDA Bridge (host-side) + +Host daemon that executes guest CUDA kernels on the real physical GPU for +Open-IOV. It is the execution backend for the endgame path: `vgsp.ko` detects a +guest kernel launch, writes a **CUBB2 v2** job record to the debugfs job file, +this daemon executes it with the CUDA driver API on the physical GPU and writes +a **CBD2 v2** done record, and the module wakes the guest. The v1 +`CUBB`/`CUBD` packed/GPA mode remains for the offline tests. + +Host-only development tooling: it never touches the guest, QEMU, mdev, +`vgsp.ko`, or the GPU reset path. It only maps the guest RAM file and talks to +the NVIDIA driver. + +## Files + +| File | Purpose | +|---|---| +| `cuda_bridge.c` | daemon + v1/v2 protocol structs + bridge/self-test/bench/mutation modes | +| `kernels.cu` | real kernels: `mail_kernel`, `vadd`, `vadd_mutated` | +| `embed_ptx.py` | embeds nvcc-generated PTX into a C header | +| `client_test.py` | external producer check of the v1 job/done transport | +| `bridge_selftest.py` | offline plain-file self-test of `--bridge` (mail, vadd, stale seq, bad magic, failure, bounds) | +| `check_module_layout.py` | mechanical CUBB2/CBD2 struct layout comparison vs `kernel/gsp_cuda_bridge.c` | +| `Makefile` | build, test targets | +| `PROTOCOL.md` | exact wire layout and semantics (v2 frozen; v1 appendix) | +| `INTEGRATION.md` | how `vgsp.ko` uses it (paths, seq semantics, logging) | + +## Build + +``` +make # cuda_bridge +make test # self-test on a fake 4 GiB RAM file +make bench # timings (CUDA events + CPU baseline) +make mutate-test +``` + +Kernel path: `nvcc` was available on the build host (CUDA 12.0, +`/usr/bin/nvcc`), so the Makefile compiles `kernels.cu` to PTX +(`-arch=sm_75`, PTX `.version 8.0`) and `embed_ptx.py` embeds it as a +NUL-terminated byte array; `cuda_bridge` loads it with `cuModuleLoadData`, and +the driver JITs it for Turing (CC 7.5). No PTX is loaded from disk at runtime +and no CUDA toolkit is needed at runtime. If `nvcc` is unavailable on a build +host, a pre-generated `kernels_ptx.h` can be copied in and the `cc` link step +still succeeds. Hand-written PTX was therefore **not** needed. + +Requirements: `libcuda.so` (driver), `cc`, `nvcc` (build only), Python 3 +(embedding + `client_test.py`). + +## Run + +``` +# v2 daemon: poll the debugfs job file, execute, write out + done +./cuda_bridge --bridge \ + --job /sys/kernel/debug/vgsp/cuda_bridge_job \ + --in /sys/kernel/debug/vgsp/cuda_bridge_in \ + --out /sys/kernel/debug/vgsp/cuda_bridge_out \ + --done /sys/kernel/debug/vgsp/cuda_bridge_done + +# v2 offline self-test with plain files (mail, vadd, stale seq, bad magic, +# failure path, bounds) +python3 bridge_selftest.py + +# v1 legacy daemon: poll the job file, execute, write the done file +./cuda_bridge --job /path/job.bin --done /path/done.bin --ram /dev/shm/vgpu-guest-ram + +# v1 one-shot (tests / module bring-up) +./cuda_bridge --job job.bin --done done.bin --ram ram.bin --once + +# fake-RAM self test: mail + vadd low GPA + vadd high GPA, byte-for-byte checks +./cuda_bridge --test [--dir /tmp/opencode/cuda-bridge-test] + +# falsifiability: opcode 2 vs opcode 3 (+1 mutation) must give different results +./cuda_bridge --mutation-test + +# timings and GPU utilization stress +./cuda_bridge --bench [--big-iters N] [--verbose] + +# external transport check (starts/stops the daemon itself) +python3 client_test.py +``` + +Every executed job prints one greppable line: + +``` +CUBB job seq=1000 op=2 n=1048576 gpu="NVIDIA GeForce RTX 2080 Super with Max-Q Design" kernel_ns=41567 status=ok checksum=392344090.500000 bad=0 +CUBB2 seq=42 op=2 n=1048576 gpu="NVIDIA GeForce RTX 2080 Super with Max-Q Design" kernel_ns=41919 status=ok checksum=392344090.500000 bad=0 out_bytes=4194304 +``` + +`--test`, `--mutation-test` and `--bench` use sparse fake RAM files under +`--dir` (default `/tmp/opencode/cuda-bridge-test`); they never touch +`/dev/shm/vgpu-guest-ram`. + +## Evidence (2026-10-03) + +Under `open-iov/backups/evidence-20261003-bridge/`: + +| File | Content | +|---|---| +| `01-self-test.txt` | fake-RAM test: mail exact, vadd low + high GPA windows, bad=0, checksum exact | +| `02-gpu-util.txt` | nvidia-smi @100 ms while 200 x n=2^24 vadd jobs run (0 % idle -> 27 % max, 298/308 samples > 0) | +| `03-timings.txt` | CUDA-event kernel times + CPU baseline (n=2^20: 39.6 us vs 1.84 ms; n=2^24: 620 us vs 28.6 ms) | +| `04-device.txt` | nvidia-smi identity and driver, plus the bridge's own DEVICE line | +| `05-mutation.txt` | opcode 3 proves the real kernel ran: checksum +1048576, bad=1048576, sample c[0]=1.0 | +| `06-daemon-transport.txt` | external producer, two seqs, done records parsed and verified | diff --git a/tools/cuda-bridge/bridge_selftest.py b/tools/cuda-bridge/bridge_selftest.py new file mode 100755 index 0000000..f3b92d6 --- /dev/null +++ b/tools/cuda-bridge/bridge_selftest.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# bridge_selftest.py - offline self-test of cuda_bridge --bridge (CUBB2/CBD2 v2) +# using plain files in place of the vgsp.ko debugfs files. +# +# Simulates the module side: packs 256-byte CUBB2 job records at the documented +# natural-alignment offsets, prepares the in blob, waits for the CBD2 done +# record, and checks the out blob: +# (a) mail n=39 -> done ok, out bytes exact +# (b) vadd n=1<<20 -> out c correct, bad=0, checksum 392344090.500000 +# (c) stale seq -> ignored (no re-execution) +# (d) invalid magic -> ignored (idle) +# (e) bad opcode -> done written with non-zero status +# (f) oversized n -> done written with non-zero status (bound) +# +# python3 bridge_selftest.py [--dir D] [--bin PATH] +# +# Never touches /dev/shm/vgpu-guest-ram, the guest, QEMU, mdev or vgsp.ko. + +import argparse +import array +import os +import struct +import subprocess +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +MAIL = b"the little gpu man says you've got mail" +VADD_CHECKSUM = 392344090.500000 +CHECKSUM_TOL = 1e-4 + +JOB_MAGIC = 0x32424243 # 'CBB2' LE +DONE_MAGIC = 0x32444243 # 'CBD2' LE +VERSION = 2 +JOB_SIZE = 256 +DONE_SIZE = 256 + +ST_OK, ST_BADJOB, ST_CUDA, ST_RAM = 0, 1, 2, 3 + + +def pack_job(seq, opcode, n, in_bytes=0, out_bytes=0, grid=(1, 1, 1), + block=(64, 1, 1), shared=0, sem_va=0, sem_payload=0, + sem_flags=0, prog_va=0, param_count=2, magic=JOB_MAGIC, + version=VERSION): + """Pack a CUBB2 job record field-by-field at the frozen v2 offsets.""" + b = bytearray(JOB_SIZE) + struct.pack_into("<IIQ", b, 0x00, magic, version, seq) + struct.pack_into("<I", b, 0x10, opcode) + struct.pack_into("<3I", b, 0x14, *grid) + struct.pack_into("<3I", b, 0x20, *block) + struct.pack_into("<I", b, 0x2c, shared) + struct.pack_into("<I", b, 0x30, n) + struct.pack_into("<Q", b, 0x38, in_bytes) + struct.pack_into("<Q", b, 0x40, out_bytes) + struct.pack_into("<Q", b, 0x48, sem_va) + struct.pack_into("<Q", b, 0x50, sem_payload) + struct.pack_into("<I", b, 0x58, sem_flags) + struct.pack_into("<Q", b, 0x60, prog_va) + struct.pack_into("<I", b, 0x68, param_count) + assert len(b) == JOB_SIZE + return bytes(b) + + +def parse_done(data): + """Parse a CBD2 done record at the frozen v2 offsets.""" + assert len(data) == DONE_SIZE, "done record must be 256 bytes" + magic, version, seq = struct.unpack_from("<IIQ", data, 0x00) + status, bad = struct.unpack_from("<II", data, 0x10) + (checksum_bits,) = struct.unpack_from("<Q", data, 0x18) + (kernel_ns,) = struct.unpack_from("<Q", data, 0x20) + out_bytes_written, err_code = struct.unpack_from("<II", data, 0x28) + name = data[0x30:0x70].split(b"\0", 1)[0].decode(errors="replace") + (host_ns,) = struct.unpack_from("<Q", data, 0x70) + checksum = struct.unpack("<d", struct.pack("<Q", checksum_bits))[0] + return { + "magic": magic, + "version": version, + "seq": seq, + "status": status, + "bad": bad, + "checksum": checksum, + "kernel_ns": kernel_ns, + "out_bytes_written": out_bytes_written, + "err_code": err_code, + "device": name, + "host_ns": host_ns, + } + + +class Harness: + def __init__(self, bin_path, workdir, poll_ms=10): + self.bin = bin_path + self.dir = workdir + self.job = os.path.join(workdir, "job.bin") + self.inp = os.path.join(workdir, "in.bin") + self.out = os.path.join(workdir, "out.bin") + self.done = os.path.join(workdir, "done.bin") + self.log = os.path.join(workdir, "daemon.log") + self.poll_ms = poll_ms + self.proc = None + self.fails = 0 + + def start(self): + os.makedirs(self.dir, exist_ok=True) + for p in (self.job, self.inp, self.out, self.done): + try: + os.unlink(p) + except FileNotFoundError: + pass + self.logfh = open(self.log, "wb") + self.proc = subprocess.Popen( + [self.bin, "--bridge", "--job", self.job, "--in", self.inp, + "--out", self.out, "--done", self.done, "--poll-ms", + str(self.poll_ms)], + stdout=self.logfh, stderr=subprocess.STDOUT) + deadline = time.time() + 60.0 + while time.time() < deadline: + if self.proc.poll() is not None: + print("daemon exited early rc=%s" % self.proc.returncode) + return False + try: + with open(self.log, "r", errors="replace") as fh: + text = fh.read() + except OSError: + text = "" + if "BRIDGE v2" in text and "DEVICE " in text: + return True + time.sleep(0.1) + print("daemon did not become ready in 60 s") + return False + + def stop(self): + if self.proc and self.proc.poll() is None: + self.proc.terminate() + try: + self.proc.wait(timeout=10) + except subprocess.TimeoutExpired: + self.proc.kill() + self.proc.wait() + try: + self.logfh.close() + except Exception: + pass + + def log_text(self): + try: + with open(self.log, "r", errors="replace") as fh: + return fh.read() + except OSError: + return "" + + def write_job(self, rec): + tmp = self.job + ".tmp" + with open(tmp, "wb") as fh: + fh.write(rec) + fh.flush() + os.fsync(fh.fileno()) + os.replace(tmp, self.job) + + def write_in(self, data): + tmp = self.inp + ".tmp" + with open(tmp, "wb") as fh: + fh.write(data) + fh.flush() + os.fsync(fh.fileno()) + os.replace(tmp, self.inp) + + def wait_done(self, seq, timeout=60.0): + deadline = time.time() + timeout + while time.time() < deadline: + try: + with open(self.done, "rb") as fh: + data = fh.read(DONE_SIZE) + except FileNotFoundError: + data = b"" + if len(data) == DONE_SIZE and data[:4] == struct.pack( + "<I", DONE_MAGIC) and struct.unpack_from( + "<I", data, 4)[0] == VERSION and struct.unpack_from( + "<Q", data, 8)[0] == seq: + return parse_done(data) + time.sleep(0.01) + return None + + def check(self, name, ok, detail): + if not ok: + self.fails += 1 + print("BRIDGE-SELFTEST %s %s %s" % (name, "PASS" if ok else "FAIL", + detail)) + sys.stdout.flush() + + def run(self): + if not self.start(): + print("BRIDGE-SELFTEST setup FAIL (daemon)") + return 1 + print("BRIDGE-SELFTEST device: %s" % + next((ln for ln in self.log_text().splitlines() + if ln.startswith("DEVICE ")), "?")) + try: + self.case_mail() + self.case_vadd() + self.case_stale() + self.case_badmagic() + self.case_failure() + self.case_bound() + finally: + self.stop() + print("BRIDGE-SELFTEST %s" % ("PASS" if self.fails == 0 else "FAIL")) + if self.fails: + print("--- daemon log tail ---") + print("\n".join(self.log_text().splitlines()[-25:])) + return 0 if self.fails == 0 else 1 + + # ---------------------------------------------------------------- cases + def case_mail(self): + n = 39 + self.write_job(pack_job(1, 1, n, in_bytes=0, out_bytes=n, + grid=(1, 1, 1), block=(64, 1, 1), + param_count=2)) + d = self.wait_done(1) + if d is None: + self.check("(a) mail n=39", False, "no done record") + return + try: + with open(self.out, "rb") as fh: + got = fh.read() + except OSError: + got = b"" + ok = (d["status"] == ST_OK and d["bad"] == 0 and + d["out_bytes_written"] == n and got == MAIL and + d["kernel_ns"] > 0 and d["device"]) + print('BRIDGE-SELFTEST (a) mail n=%d gpu="%s" kernel_ns=%d ' + 'status=%d bad=%d out_bytes=%d mail="%s"' + % (n, d["device"], d["kernel_ns"], d["status"], d["bad"], + d["out_bytes_written"], got.decode(errors="replace"))) + self.check("(a) mail n=39", ok, + "out_exact=%s" % ("yes" if got == MAIL else "no")) + + def case_vadd(self): + n = 1 << 20 + a = array.array("f", ((i % 1000) * 0.5 for i in range(n))) + b = array.array("f", ((i % 997) * 0.25 for i in range(n))) + self.write_in(a.tobytes() + b.tobytes()) + self.write_job(pack_job(2, 2, n, in_bytes=8 * n, out_bytes=4 * n, + grid=((n + 255) // 256, 1, 1), + block=(256, 1, 1), param_count=5)) + d = self.wait_done(2) + if d is None: + self.check("(b) vadd n=1<<20", False, "no done record") + return + with open(self.out, "rb") as fh: + c = array.array("f") + c.frombytes(fh.read(4 * n)) + bad = sum(1 for i in range(n) if c[i] != a[i] + b[i]) + checksum = sum(c) + delta = abs(checksum - VADD_CHECKSUM) + ok = (d["status"] == ST_OK and d["bad"] == 0 and bad == 0 and + delta <= CHECKSUM_TOL and d["out_bytes_written"] == 4 * n and + abs(d["checksum"] - VADD_CHECKSUM) <= CHECKSUM_TOL and + d["kernel_ns"] > 0) + print("BRIDGE-SELFTEST (b) vadd n=%d gpu=\"%s\" kernel_ns=%d " + "status=%d bad=%d checksum=%.6f delta=%.3e out_bytes=%d" + % (n, d["device"], d["kernel_ns"], d["status"], d["bad"], + checksum, delta, d["out_bytes_written"])) + self.check("(b) vadd n=1<<20", ok, "file_bad=%d" % bad) + + def case_stale(self): + with open(self.out, "rb") as fh: + before = fh.read() + self.write_job(pack_job(2, 1, 39, in_bytes=0, out_bytes=39, + grid=(1, 1, 1), block=(64, 1, 1), + param_count=2)) + time.sleep(0.6) + with open(self.out, "rb") as fh: + after = fh.read() + lines = [ln for ln in self.log_text().splitlines() + if ln.startswith("CUBB2 seq=2 ")] + d = None + try: + with open(self.done, "rb") as fh: + d = parse_done(fh.read(DONE_SIZE)) + except (OSError, AssertionError): + pass + ok = (len(lines) == 1 and before == after and d is not None and + d["seq"] == 2 and d["status"] == ST_OK) + self.check("(c) stale seq=2 ignored", ok, + "exec_lines=%d out_unchanged=%s" + % (len(lines), "yes" if before == after else "no")) + + def case_badmagic(self): + self.write_job(pack_job(100, 1, 39, in_bytes=0, out_bytes=39, + magic=0xDEADBEEF)) + time.sleep(0.4) + hits = [ln for ln in self.log_text().splitlines() + if "seq=100" in ln or "seq=100 " in ln] + self.check("(d) invalid magic seq=100 ignored", len(hits) == 0, + "exec_lines=%d" % len(hits)) + + def case_failure(self): + self.write_job(pack_job(3, 9, 8, in_bytes=0, out_bytes=8)) + d = self.wait_done(3) + ok = (d is not None and d["status"] != ST_OK and + d["err_code"] == 9) + self.check("(e) bad opcode done non-zero", + ok, + "status=%s err_code=%s" % (d["status"], d["err_code"]) + if d else "no done record") + + def case_bound(self): + n = (1 << 26) + 1 + self.write_job(pack_job(4, 2, n, in_bytes=8 * n, out_bytes=4 * n)) + d = self.wait_done(4) + ok = (d is not None and d["status"] != ST_OK) + self.check("(f) oversized n bound done non-zero", ok, + "status=%s err_code=%s" % (d["status"], d["err_code"]) + if d else "no done record") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--dir", default="/tmp/opencode/cuda-bridge-bridge-test") + ap.add_argument("--bin", default=os.path.join(HERE, "cuda_bridge")) + args = ap.parse_args() + if not os.path.exists(args.bin): + print("bridge_selftest: binary not found: %s (run make first)" + % args.bin) + return 2 + return Harness(args.bin, args.dir).run() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cuda-bridge/check_module_layout.py b/tools/cuda-bridge/check_module_layout.py new file mode 100644 index 0000000..336cc73 --- /dev/null +++ b/tools/cuda-bridge/check_module_layout.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# check_module_layout.py - mechanically compare the frozen CUBB2/CBD2 structs +# in kernel/gsp_cuda_bridge.c (module side) and cuda_bridge.c (daemon side). +# +# Parses both struct bodies, computes x86_64 natural-alignment offsets and +# sizes, and reports the first mismatch. The compile-time static asserts in +# each C file pin their own layout; this check pins the two sides to each +# other. Exit code 0 = identical. + +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +MODULE = os.path.normpath(os.path.join(HERE, "..", "..", "kernel", + "gsp_cuda_bridge.c")) +DAEMON = os.path.join(HERE, "cuda_bridge.c") + +TYPE_SIZE = { + "u8": 1, "u32": 4, "u64": 8, + "uint8_t": 1, "uint32_t": 4, "uint64_t": 8, + "char": 1, +} +CONST = {"CUBB2_NAME_MAX": 64} + + +def parse_struct(path, name): + text = open(path).read() + m = re.search(r"struct\s+%s\s*\{([^{}]*)\}\s*;" % re.escape(name), text, + re.S) + if not m: + m = re.search(r"typedef\s+struct\s*\{([^{}]*)\}\s*%s_t\s*;" + % re.escape(name), text, re.S) + if not m: + raise SystemExit("%s: struct %s not found" % (path, name)) + fields = [] + for raw in m.group(1).splitlines(): + line = raw.split("/*")[0].strip() + if not line or line.startswith("*"): + continue + m2 = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)" + r"\s*(?:\[\s*(.+?)\s*\])?\s*;$", line) + if not m2: + raise SystemExit("%s: cannot parse field %r" % (path, line)) + typ, fname, count_expr = m2.group(1), m2.group(2), m2.group(3) + count = 1 + if count_expr is not None: + count = int(eval(count_expr, {"__builtins__": {}}, CONST)) + if typ not in TYPE_SIZE: + raise SystemExit("%s: unknown type %s" % (path, typ)) + fields.append((typ, fname, count)) + return fields + + +def layout(fields): + out = [] + off = 0 + align = 1 + for typ, fname, count in fields: + sz = TYPE_SIZE[typ] + al = sz + off = (off + al - 1) // al * al + out.append((fname, typ, count, off, sz * count)) + off += sz * count + align = max(align, al) + total = (off + align - 1) // align * align + return out, total + + +def compare(name, mpath, dpath): + mod = layout(parse_struct(mpath, name)) + dae = layout(parse_struct(dpath, name)) + mfields, mtotal = mod + dfields, dtotal = dae + ok = True + print("=== struct %s (module %s | daemon %s) ===" % (name, mpath, dpath)) + if mtotal != dtotal: + print("TOTAL MISMATCH: module=%d daemon=%d" % (mtotal, dtotal)) + ok = False + if len(mfields) != len(dfields): + print("FIELD COUNT MISMATCH: module=%d daemon=%d" + % (len(mfields), len(dfields))) + return False + print("%-20s %-10s %-18s %-18s" % ("field", "type", "module off/size", + "daemon off/size")) + for (mn, mt, mc, mo, ms), (dn, dt, dc, do, ds) in zip(mfields, dfields): + same = (mn == dn and mc == dc and mo == do and ms == ds) + print("%-20s %-10s 0x%02x/%-12d 0x%02x/%-12d%s" + % (mn, "%s|%s" % (mt, dt), mo, ms, do, ds, + "" if same else " <-- MISMATCH")) + if not same: + ok = False + print("total: module=0x%x daemon=0x%x %s" + % (mtotal, dtotal, "MATCH" if mtotal == dtotal else "MISMATCH")) + return ok + + +def main(): + ok = True + for name in ("cubb2_job", "cubb2_done"): + ok = compare(name, MODULE, DAEMON) and ok + print("LAYOUT-CHECK %s" % ("PASS" if ok else "FAIL")) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cuda-bridge/client_test.py b/tools/cuda-bridge/client_test.py new file mode 100644 index 0000000..da0dd4d --- /dev/null +++ b/tools/cuda-bridge/client_test.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# client_test.py - external producer/consumer check for the CUBB v1 transport. +# +# Simulates what vgsp.ko will do: write a CUBB job record into the job file, +# wait for the daemon to update the done file with the matching seq, then +# verify the fields. Uses a fake guest-RAM file (never /dev/shm/vgpu-guest-ram). +# +# python3 client_test.py + +import array +import os +import struct +import subprocess +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +BIN = os.path.join(HERE, "cuda_bridge") +DIR = os.environ.get("CUBB_TEST_DIR", "/tmp/opencode/cuda-bridge-test") +RAM = os.path.join(DIR, "transport-ram.bin") +JOB = os.path.join(DIR, "transport-job.bin") +DONE = os.path.join(DIR, "transport-done.bin") +LOG = os.path.join(DIR, "transport-daemon.log") + +N = 1 << 20 +MASK = 0xFFFFFFFF + +JOB_FMT = "<4sIQ10I9Q" +DONE_FMT = "<4sIQIIQQII64sQII" +assert struct.calcsize(JOB_FMT) == 128 +assert struct.calcsize(DONE_FMT) == 128 + + +def job_record(seq, opcode, n, mail_out_gpa=0, a_gpa=0, b_gpa=0, c_gpa=0): + buffer_bytes = n * 4 + mail_bytes = n if opcode == 1 else 0 + return struct.pack( + JOB_FMT, + b"CUBB", + 1, + seq, + opcode, + (n + 255) // 256 if opcode != 1 else 1, + 1, + 1, + 256, + 1, + 1, + 0, + n, + 0, + mail_out_gpa, + mail_bytes, + a_gpa, + buffer_bytes, + b_gpa, + buffer_bytes, + c_gpa, + buffer_bytes, + 0, + ) + + +def parse_done(data): + ( + magic, + version, + seq, + status, + bad_count, + checksum_bits, + kernel_ns, + name_len, + opcode, + name, + host_ns, + err_code, + _reserved, + ) = struct.unpack(DONE_FMT, data) + checksum = struct.unpack("<d", struct.pack("<Q", checksum_bits))[0] + return { + "magic": magic, + "version": version, + "seq": seq, + "status": status, + "bad_count": bad_count, + "checksum": checksum, + "kernel_ns": kernel_ns, + "device": name.split(b"\0", 1)[0].decode(), + "opcode": opcode, + "host_ns": host_ns, + "err_code": err_code, + } + + +def fill_ram(): + fd = os.open(RAM, os.O_RDWR | os.O_CREAT) + os.ftruncate(fd, 4 << 30) + a = array.array("f", [(i % 1000) * 0.5 for i in range(N)]) + b = array.array("f", [(i % 997) * 0.25 for i in range(N)]) + os.pwrite(fd, a.tobytes(), 0x10000000) + os.pwrite(fd, b.tobytes(), 0x20000000) + os.close(fd) + + +def write_job(rec): + tmp = JOB + ".tmp" + with open(tmp, "wb") as fh: + fh.write(rec) + fh.flush() + os.fsync(fh.fileno()) + os.replace(tmp, JOB) + + +def wait_done(seq, timeout=60.0): + t0 = time.time() + while time.time() - t0 < timeout: + try: + with open(DONE, "rb") as fh: + data = fh.read(128) + except FileNotFoundError: + data = b"" + if len(data) == 128 and data[:4] == b"CUBD": + if struct.unpack_from("<Q", data, 8)[0] == seq: + return data + time.sleep(0.02) + return None + + +def main(): + os.makedirs(DIR, exist_ok=True) + fill_ram() + for path in (JOB, DONE): + try: + os.unlink(path) + except FileNotFoundError: + pass + + with open(LOG, "wb") as log: + daemon = subprocess.Popen( + [BIN, "--job", JOB, "--done", DONE, "--ram", RAM, "--poll-ms", "5"], + stdout=log, + stderr=subprocess.STDOUT, + ) + fails = 0 + try: + time.sleep(0.5) + + write_job(job_record(1000, 2, N, a_gpa=0x10000000, b_gpa=0x20000000, + c_gpa=0x30000000)) + data = wait_done(1000) + if data is None: + print("TRANSPORT seq=1000 vadd: no done record") + fails += 1 + else: + d = parse_done(data) + with open(RAM, "rb") as fh: + fh.seek(0x30000000) + c = array.array("f") + c.frombytes(fh.read(N * 4)) + bad = sum( + 1 + for i in range(N) + if c[i] != (i % 1000) * 0.5 + (i % 997) * 0.25 + ) + checksum = sum(c) + ok = ( + d["status"] == 0 + and d["bad_count"] == 0 + and bad == 0 + and abs(checksum - 392344090.5) < 1e-4 + and d["kernel_ns"] > 0 + ) + if not ok: + fails += 1 + print( + "TRANSPORT seq=%d op=%d status=%d bad=%d kernel_ns=%d " + "checksum=%.6f device=\"%s\" file_bad=%d %s" + % ( + d["seq"], + d["opcode"], + d["status"], + d["bad_count"], + d["kernel_ns"], + d["checksum"], + d["device"], + bad, + "PASS" if ok else "FAIL", + ) + ) + + write_job(job_record(1001, 1, len(b"the little gpu man says you've got mail"), + mail_out_gpa=0x40000000)) + data = wait_done(1001) + want = b"the little gpu man says you've got mail" + if data is None: + print("TRANSPORT seq=1001 mail: no done record") + fails += 1 + else: + d = parse_done(data) + with open(RAM, "rb") as fh: + fh.seek(0x40000000) + got = fh.read(len(want)) + ok = d["status"] == 0 and d["bad_count"] == 0 and got == want + if not ok: + fails += 1 + print( + "TRANSPORT seq=%d op=%d status=%d bad=%d mail=\"%s\" %s" + % ( + d["seq"], + d["opcode"], + d["status"], + d["bad_count"], + got.decode(), + "PASS" if ok else "FAIL", + ) + ) + finally: + daemon.terminate() + try: + daemon.wait(timeout=10) + except subprocess.TimeoutExpired: + daemon.kill() + daemon.wait() + + print("TRANSPORT %s" % ("PASS" if fails == 0 else "FAIL")) + return 0 if fails == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cuda-bridge/cuda_bridge b/tools/cuda-bridge/cuda_bridge Binary files differnew file mode 100755 index 0000000..21fa295 --- /dev/null +++ b/tools/cuda-bridge/cuda_bridge diff --git a/tools/cuda-bridge/cuda_bridge.c b/tools/cuda-bridge/cuda_bridge.c new file mode 100644 index 0000000..86823e6 --- /dev/null +++ b/tools/cuda-bridge/cuda_bridge.c @@ -0,0 +1,1598 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// cuda_bridge.c - host-side CUDA bridge daemon for Open-IOV vGPU. +// +// Reads a fixed-size job record written by the host kernel module (vgsp.ko, +// future integration), executes the requested kernel on the physical GPU with +// the CUDA driver API, copies results back into the guest RAM mapping, and +// writes a fixed-size done record. Host-only development tool: it never +// touches the guest, QEMU, mdev, vgsp.ko or the GPU reset path. +// +// Job record (little-endian, 128 bytes, C struct below): +// +// off size field +// 0x00 4 magic "CUBB" +// 0x04 4 version = 1 +// 0x08 8 seq (u64) +// 0x10 4 opcode (1=mail, 2=vadd, 3=vadd+1 mutation) +// 0x14 4 grid_x +// 0x18 4 grid_y +// 0x1c 4 grid_z +// 0x20 4 block_x +// 0x24 4 block_y +// 0x28 4 block_z +// 0x2c 4 shared_mem +// 0x30 4 n (elements for vadd, length for mail) +// 0x34 4 flags (bit0 = mutate vadd) +// 0x38 8 mail_out.guest_gpa +// 0x40 8 mail_out.bytes +// 0x48 8 a.guest_gpa +// 0x50 8 a.bytes +// 0x58 8 b.guest_gpa +// 0x60 8 b.bytes +// 0x68 8 c.guest_gpa +// 0x70 8 c.bytes +// 0x78 8 reserved0 +// = 0x80 bytes +// +// Done record (little-endian, 128 bytes): +// +// off size field +// 0x00 4 magic "CUBD" +// 0x04 4 version = 1 +// 0x08 8 seq (u64) +// 0x10 4 status (0=ok, 1=bad job, 2=cuda error, 3=ram/mapping error) +// 0x14 4 bad_count (naive c==a+b mismatches; mail byte mismatches) +// 0x18 8 checksum (IEEE-754 double bit pattern, sum of c) +// 0x20 8 kernel_ns (CUDA event elapsed, kernel only) +// 0x28 4 device_name_len +// 0x2c 4 opcode +// 0x30 64 device_name (NUL padded) +// 0x70 8 host_ns (wall time for the whole job) +// 0x78 4 err_code (CUresult / internal code) +// 0x7c 4 reserved0 +// = 0x80 bytes +// +// GPA mapping of /dev/shm/vgpu-guest-ram (4 GiB file): +// [0x0000000000, 0x80000000) -> file offset = GPA +// [0x100000000, 0x180000000) -> file offset = GPA - 0x100000000 + 0x80000000 + +#define CUBB_JOB_SIZE 128 +#define CUBD_DONE_SIZE 128 +#define CUBB_VERSION 1u +#define CUBB_MAGIC "CUBB" +#define CUBD_MAGIC "CUBD" + +#define ST_OK 0u +#define ST_BADJOB 1u +#define ST_CUDA 2u +#define ST_RAM 3u + +#define MAIL "the little gpu man says you've got mail" + +#define GPA_LOW_LIMIT 0x0000000080000000ULL +#define GPA_HIGH_BASE 0x0000000100000000ULL +#define GPA_HIGH_LIMIT 0x0000000180000000ULL +#define RAM_MAP_HIGH_OFF 0x0000000080000000ULL +#define RAM_FILE_SIZE 0x0000000100000000ULL + +#define VADD_EXPECTED_CHECKSUM 392344090.500000 +#define CHECKSUM_TOL 1e-4 + +#include <cuda.h> +#include <errno.h> +#include <fcntl.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +#include "kernels_ptx.h" + +#pragma pack(push, 1) +typedef struct { + char magic[4]; + uint32_t version; + uint64_t seq; + uint32_t opcode; + uint32_t grid_x, grid_y, grid_z; + uint32_t block_x, block_y, block_z; + uint32_t shared_mem; + uint32_t n; + uint32_t flags; + uint64_t mail_out_gpa, mail_out_bytes; + uint64_t a_gpa, a_bytes; + uint64_t b_gpa, b_bytes; + uint64_t c_gpa, c_bytes; + uint64_t reserved0; +} cubb_job_t; + +typedef struct { + char magic[4]; + uint32_t version; + uint64_t seq; + uint32_t status; + uint32_t bad_count; + uint64_t checksum_bits; + uint64_t kernel_ns; + uint32_t device_name_len; + uint32_t opcode; + char device_name[64]; + uint64_t host_ns; + uint32_t err_code; + uint32_t reserved0; +} cubb_done_t; +#pragma pack(pop) + +_Static_assert(sizeof(cubb_job_t) == CUBB_JOB_SIZE, "CUBB job layout"); +_Static_assert(offsetof(cubb_job_t, magic) == 0x00, "job magic"); +_Static_assert(offsetof(cubb_job_t, version) == 0x04, "job version"); +_Static_assert(offsetof(cubb_job_t, seq) == 0x08, "job seq"); +_Static_assert(offsetof(cubb_job_t, opcode) == 0x10, "job opcode"); +_Static_assert(offsetof(cubb_job_t, grid_x) == 0x14, "job grid_x"); +_Static_assert(offsetof(cubb_job_t, grid_y) == 0x18, "job grid_y"); +_Static_assert(offsetof(cubb_job_t, grid_z) == 0x1c, "job grid_z"); +_Static_assert(offsetof(cubb_job_t, block_x) == 0x20, "job block_x"); +_Static_assert(offsetof(cubb_job_t, block_y) == 0x24, "job block_y"); +_Static_assert(offsetof(cubb_job_t, block_z) == 0x28, "job block_z"); +_Static_assert(offsetof(cubb_job_t, shared_mem) == 0x2c, "job shared_mem"); +_Static_assert(offsetof(cubb_job_t, n) == 0x30, "job n"); +_Static_assert(offsetof(cubb_job_t, flags) == 0x34, "job flags"); +_Static_assert(offsetof(cubb_job_t, mail_out_gpa) == 0x38, "job mail_out_gpa"); +_Static_assert(offsetof(cubb_job_t, mail_out_bytes) == 0x40, "job mail_out_bytes"); +_Static_assert(offsetof(cubb_job_t, a_gpa) == 0x48, "job a_gpa"); +_Static_assert(offsetof(cubb_job_t, a_bytes) == 0x50, "job a_bytes"); +_Static_assert(offsetof(cubb_job_t, b_gpa) == 0x58, "job b_gpa"); +_Static_assert(offsetof(cubb_job_t, b_bytes) == 0x60, "job b_bytes"); +_Static_assert(offsetof(cubb_job_t, c_gpa) == 0x68, "job c_gpa"); +_Static_assert(offsetof(cubb_job_t, c_bytes) == 0x70, "job c_bytes"); +_Static_assert(offsetof(cubb_job_t, reserved0) == 0x78, "job reserved0"); + +_Static_assert(sizeof(cubb_done_t) == CUBD_DONE_SIZE, "CUBD done layout"); +_Static_assert(offsetof(cubb_done_t, magic) == 0x00, "done magic"); +_Static_assert(offsetof(cubb_done_t, version) == 0x04, "done version"); +_Static_assert(offsetof(cubb_done_t, seq) == 0x08, "done seq"); +_Static_assert(offsetof(cubb_done_t, status) == 0x10, "done status"); +_Static_assert(offsetof(cubb_done_t, bad_count) == 0x14, "done bad_count"); +_Static_assert(offsetof(cubb_done_t, checksum_bits) == 0x18, "done checksum"); +_Static_assert(offsetof(cubb_done_t, kernel_ns) == 0x20, "done kernel_ns"); +_Static_assert(offsetof(cubb_done_t, device_name_len) == 0x28, "done name_len"); +_Static_assert(offsetof(cubb_done_t, opcode) == 0x2c, "done opcode"); +_Static_assert(offsetof(cubb_done_t, device_name) == 0x30, "done device_name"); +_Static_assert(offsetof(cubb_done_t, host_ns) == 0x70, "done host_ns"); +_Static_assert(offsetof(cubb_done_t, err_code) == 0x78, "done err_code"); +_Static_assert(offsetof(cubb_done_t, reserved0) == 0x7c, "done reserved0"); + +/* + * v2 bridge protocol (frozen interface shared with kernel/gsp_cuda_bridge.c). + * 256-byte records, NATURAL alignment (no packing), little-endian. The kernel + * module and this daemon both compile for x86_64; the static asserts below + * pin every documented offset so a layout drift fails the build on either + * side. + */ +#define CUBB2_JOB_SIZE 256 +#define CUBB2_DONE_SIZE 256 +#define CUBB2_VERSION 2u +#define CUBB2_JOB_MAGIC 0x32424243u /* 'CBB2' LE */ +#define CUBB2_DONE_MAGIC 0x32444243u /* 'CBD2' LE */ +#define CUBB2_N_MAX (1u << 26) +#define CUBB2_BYTES_MAX (32u * 1024u * 1024u) +#define CUBB2_MAIL_MAX 4096u /* module-side param-length limit */ +#define CUBB2_CHUNK (1u << 20) /* file read/write chunk */ + +typedef struct { + uint32_t magic; + uint32_t version; + uint64_t seq; + uint32_t opcode; + uint32_t grid[3]; + uint32_t block[3]; + uint32_t shared; + uint32_t n; + uint64_t in_bytes; + uint64_t out_bytes; + uint64_t sem_va; + uint64_t sem_payload; + uint32_t sem_flags; + uint32_t pad; + uint64_t prog_va; + uint32_t param_count; + uint32_t pad2; + uint8_t rsv[0x100 - 0x70]; +} cubb2_job_t; + +typedef struct { + uint32_t magic; + uint32_t version; + uint64_t seq; + uint32_t status; + uint32_t bad; + uint64_t checksum; + uint64_t kernel_ns; + uint32_t out_bytes_written; + uint32_t err_code; + char device_name[64]; + uint64_t host_ns; + uint8_t rsv[0x100 - 0x78]; +} cubb2_done_t; + +_Static_assert(sizeof(cubb2_job_t) == CUBB2_JOB_SIZE, "cubb2_job size"); +_Static_assert(offsetof(cubb2_job_t, magic) == 0x00, "cubb2_job magic"); +_Static_assert(offsetof(cubb2_job_t, version) == 0x04, "cubb2_job version"); +_Static_assert(offsetof(cubb2_job_t, seq) == 0x08, "cubb2_job seq"); +_Static_assert(offsetof(cubb2_job_t, opcode) == 0x10, "cubb2_job opcode"); +_Static_assert(offsetof(cubb2_job_t, grid) == 0x14, "cubb2_job grid"); +_Static_assert(offsetof(cubb2_job_t, block) == 0x20, "cubb2_job block"); +_Static_assert(offsetof(cubb2_job_t, shared) == 0x2c, "cubb2_job shared"); +_Static_assert(offsetof(cubb2_job_t, n) == 0x30, "cubb2_job n"); +_Static_assert(offsetof(cubb2_job_t, in_bytes) == 0x38, "cubb2_job in_bytes"); +_Static_assert(offsetof(cubb2_job_t, out_bytes) == 0x40, "cubb2_job out_bytes"); +_Static_assert(offsetof(cubb2_job_t, sem_va) == 0x48, "cubb2_job sem_va"); +_Static_assert(offsetof(cubb2_job_t, sem_payload) == 0x50, "cubb2_job sem_payload"); +_Static_assert(offsetof(cubb2_job_t, sem_flags) == 0x58, "cubb2_job sem_flags"); +_Static_assert(offsetof(cubb2_job_t, pad) == 0x5c, "cubb2_job pad"); +_Static_assert(offsetof(cubb2_job_t, prog_va) == 0x60, "cubb2_job prog_va"); +_Static_assert(offsetof(cubb2_job_t, param_count) == 0x68, "cubb2_job param_count"); +_Static_assert(offsetof(cubb2_job_t, pad2) == 0x6c, "cubb2_job pad2"); +_Static_assert(offsetof(cubb2_job_t, rsv) == 0x70, "cubb2_job rsv"); + +_Static_assert(sizeof(cubb2_done_t) == CUBB2_DONE_SIZE, "cubb2_done size"); +_Static_assert(offsetof(cubb2_done_t, magic) == 0x00, "cubb2_done magic"); +_Static_assert(offsetof(cubb2_done_t, version) == 0x04, "cubb2_done version"); +_Static_assert(offsetof(cubb2_done_t, seq) == 0x08, "cubb2_done seq"); +_Static_assert(offsetof(cubb2_done_t, status) == 0x10, "cubb2_done status"); +_Static_assert(offsetof(cubb2_done_t, bad) == 0x14, "cubb2_done bad"); +_Static_assert(offsetof(cubb2_done_t, checksum) == 0x18, "cubb2_done checksum"); +_Static_assert(offsetof(cubb2_done_t, kernel_ns) == 0x20, "cubb2_done kernel_ns"); +_Static_assert(offsetof(cubb2_done_t, out_bytes_written) == 0x28, + "cubb2_done out_bytes_written"); +_Static_assert(offsetof(cubb2_done_t, err_code) == 0x2c, "cubb2_done err_code"); +_Static_assert(offsetof(cubb2_done_t, device_name) == 0x30, "cubb2_done name"); +_Static_assert(offsetof(cubb2_done_t, host_ns) == 0x70, "cubb2_done host_ns"); +_Static_assert(offsetof(cubb2_done_t, rsv) == 0x78, "cubb2_done rsv"); + +typedef struct { + uint64_t kernel_ns; + uint64_t htod_ns; + uint64_t dtoh_ns; + uint64_t total_ns; +} job_timing_t; + +static CUdevice g_dev; +static CUcontext g_ctx; +static CUmodule g_mod; +static CUfunction g_fn_mail, g_fn_vadd, g_fn_vadd_mut; +static char g_dev_name[128]; +static uint8_t *g_ram; +static uint64_t g_ram_len = RAM_FILE_SIZE; + +static uint64_t now_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +static double bits_to_double(uint64_t bits) +{ + double d; + + memcpy(&d, &bits, sizeof d); + return d; +} + +static uint64_t double_to_bits(double d) +{ + uint64_t bits; + + memcpy(&bits, &d, sizeof bits); + return bits; +} + +static void uuid_str(const CUuuid *u, char *out, size_t outlen) +{ + snprintf(out, outlen, + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-" + "%02x%02x%02x%02x%02x%02x", + (unsigned char)u->bytes[0], (unsigned char)u->bytes[1], + (unsigned char)u->bytes[2], (unsigned char)u->bytes[3], + (unsigned char)u->bytes[4], (unsigned char)u->bytes[5], + (unsigned char)u->bytes[6], (unsigned char)u->bytes[7], + (unsigned char)u->bytes[8], (unsigned char)u->bytes[9], + (unsigned char)u->bytes[10], (unsigned char)u->bytes[11], + (unsigned char)u->bytes[12], (unsigned char)u->bytes[13], + (unsigned char)u->bytes[14], (unsigned char)u->bytes[15]); +} + +static void cuda_init(void) +{ + CUuuid uuid; + char uuidbuf[64]; + CUresult r; + int major = 0, minor = 0, sm = 0, drv = 0; + size_t mem = 0; + + r = cuInit(0); + if (r != CUDA_SUCCESS) { + fprintf(stderr, "bridge: cuInit failed (%d)\n", (int)r); + exit(1); + } + r = cuDeviceGet(&g_dev, 0); + if (r != CUDA_SUCCESS) { + fprintf(stderr, "bridge: cuDeviceGet failed (%d)\n", (int)r); + exit(1); + } + cuDeviceGetName(g_dev_name, sizeof g_dev_name, g_dev); + cuDeviceGetUuid(&uuid, g_dev); + uuid_str(&uuid, uuidbuf, sizeof uuidbuf); + cuDeviceGetAttribute(&major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, g_dev); + cuDeviceGetAttribute(&minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, g_dev); + cuDeviceGetAttribute(&sm, CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT, g_dev); + cuDeviceTotalMem(&mem, g_dev); + cuDriverGetVersion(&drv); + + printf("DEVICE name=\"%s\" uuid=GPU-%s cc=%d.%d sm=%d mem=%zu driver_api=%d.%d\n", + g_dev_name, uuidbuf, major, minor, sm, mem, drv / 1000, (drv % 1000) / 10); + fflush(stdout); + + r = cuCtxCreate(&g_ctx, 0, g_dev); + if (r != CUDA_SUCCESS) { + fprintf(stderr, "bridge: cuCtxCreate failed (%d)\n", (int)r); + exit(1); + } + r = cuModuleLoadData(&g_mod, kernels_ptx); + if (r != CUDA_SUCCESS) { + fprintf(stderr, "bridge: cuModuleLoadData failed (%d)\n", (int)r); + exit(1); + } + r = cuModuleGetFunction(&g_fn_mail, g_mod, "mail_kernel"); + if (r == CUDA_SUCCESS) + r = cuModuleGetFunction(&g_fn_vadd, g_mod, "vadd"); + if (r == CUDA_SUCCESS) + r = cuModuleGetFunction(&g_fn_vadd_mut, g_mod, "vadd_mutated"); + if (r != CUDA_SUCCESS) { + fprintf(stderr, "bridge: cuModuleGetFunction failed (%d)\n", (int)r); + exit(1); + } + printf("BRIDGE ptx=embedded bytes=%u kernels=mail_kernel,vadd,vadd_mutated\n", + kernels_ptx_len); + fflush(stdout); +} + +static int map_ram_file(const char *path, int create) +{ + int fd, flags = O_RDWR; + struct stat st; + void *p; + + if (create) + flags |= O_CREAT; + fd = open(path, flags, 0644); + if (fd < 0) { + fprintf(stderr, "bridge: open %s: %s\n", path, strerror(errno)); + return -1; + } + if (fstat(fd, &st) != 0) { + close(fd); + return -1; + } + if ((uint64_t)st.st_size < RAM_FILE_SIZE) { + if (ftruncate(fd, (off_t)RAM_FILE_SIZE) != 0) { + fprintf(stderr, "bridge: ftruncate %s: %s\n", path, strerror(errno)); + close(fd); + return -1; + } + } + p = mmap(NULL, RAM_FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + if (p == MAP_FAILED) { + fprintf(stderr, "bridge: mmap %s: %s\n", path, strerror(errno)); + return -1; + } + g_ram = p; + g_ram_len = RAM_FILE_SIZE; + return 0; +} + +static int gpa_to_off(uint64_t gpa, uint64_t bytes, uint64_t *off) +{ + if (bytes == 0 || gpa + bytes < gpa) + return -1; + if (gpa < GPA_LOW_LIMIT && bytes <= GPA_LOW_LIMIT - gpa) { + *off = gpa; + } else if (gpa >= GPA_HIGH_BASE && gpa < GPA_HIGH_LIMIT && + bytes <= GPA_HIGH_LIMIT - gpa) { + *off = gpa - GPA_HIGH_BASE + RAM_MAP_HIGH_OFF; + } else { + return -1; + } + if (*off + bytes > g_ram_len) + return -1; + return 0; +} + +static int read_job_file(const char *path, cubb_job_t *j) +{ + int fd; + ssize_t r; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + r = pread(fd, j, sizeof *j, 0); + close(fd); + if (r != (ssize_t)sizeof *j) + return -1; + if (memcmp(j->magic, CUBB_MAGIC, 4) != 0 || j->version != CUBB_VERSION) + return -1; + return 0; +} + +static int atomic_write(const char *path, const void *buf, size_t len) +{ + char tmp[4096]; + ssize_t w; + int fd; + + if (snprintf(tmp, sizeof tmp, "%s.tmp.%d", path, (int)getpid()) >= (int)sizeof tmp) + return -1; + fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + return -1; + w = write(fd, buf, len); + if (w == (ssize_t)len) + fsync(fd); + close(fd); + if (w != (ssize_t)len) { + unlink(tmp); + return -1; + } + if (rename(tmp, path) != 0) { + unlink(tmp); + return -1; + } + return 0; +} + +static void print_job_line(const cubb_job_t *j, const cubb_done_t *d) +{ + double cs = bits_to_double(d->checksum_bits); + + printf("CUBB job seq=%llu op=%u n=%u gpu=\"%s\" kernel_ns=%llu " + "status=%s checksum=%.6f bad=%u\n", + (unsigned long long)j->seq, j->opcode, j->n, d->device_name, + (unsigned long long)d->kernel_ns, + d->status == ST_OK ? "ok" : "err", cs, d->bad_count); + fflush(stdout); +} + +static int run_mail(const cubb_job_t *j, cubb_done_t *d, job_timing_t *t) +{ + uint32_t len = j->n; + uint64_t off; + CUdeviceptr dp = 0; + CUevent e0 = NULL, e1 = NULL; + CUresult r; + char *out = NULL; + unsigned gx, gy, gz, bx, by, bz; + int have_ev = 0; + int n; + + if (len == 0 || len > 4096 || j->mail_out_bytes < len || + gpa_to_off(j->mail_out_gpa, len, &off) != 0) { + d->status = ST_BADJOB; + return -1; + } + out = malloc(len); + if (!out) { + d->status = ST_RAM; + return -1; + } + r = cuMemAlloc(&dp, len); + if (r != CUDA_SUCCESS) { + d->status = ST_CUDA; + d->err_code = (uint32_t)r; + goto done; + } + gx = j->grid_x ? j->grid_x : 1; + gy = j->grid_y ? j->grid_y : 1; + gz = j->grid_z ? j->grid_z : 1; + bx = j->block_x ? j->block_x : 64; + by = j->block_y ? j->block_y : 1; + bz = j->block_z ? j->block_z : 1; + n = (int)len; + + if (cuEventCreate(&e0, CU_EVENT_DEFAULT) == CUDA_SUCCESS && + cuEventCreate(&e1, CU_EVENT_DEFAULT) == CUDA_SUCCESS) + have_ev = 1; + if (have_ev) + cuEventRecord(e0, 0); + r = cuLaunchKernel(g_fn_mail, gx, gy, gz, bx, by, bz, j->shared_mem, 0, + (void *[]){ &dp, &n }, NULL); + if (have_ev) + cuEventRecord(e1, 0); + if (r == CUDA_SUCCESS) + r = cuCtxSynchronize(); + if (have_ev) { + float ms = 0.0f; + + if (cuEventElapsedTime(&ms, e0, e1) == CUDA_SUCCESS) + t->kernel_ns = (uint64_t)((double)ms * 1e6); + cuEventDestroy(e0); + cuEventDestroy(e1); + } + if (r == CUDA_SUCCESS) + r = cuMemcpyDtoH(out, dp, len); + if (r == CUDA_SUCCESS) { + const char *mail = MAIL; + uint32_t bad = 0, i; + + memcpy(g_ram + off, out, len); + for (i = 0; i < len; i++) + if (out[i] != (i < (uint32_t)strlen(mail) ? mail[i] : '\0')) + bad++; + d->bad_count = bad; + d->checksum_bits = double_to_bits(0.0); + } else { + d->status = ST_CUDA; + d->err_code = (uint32_t)r; + } + +done: + if (dp) + cuMemFree(dp); + free(out); + return d->status == ST_OK ? 0 : -1; +} + +static int run_vadd(const cubb_job_t *j, cubb_done_t *d, job_timing_t *t, int mutated) +{ + uint32_t n = j->n; + uint64_t bytes = (uint64_t)n * 4; + uint64_t oa, ob, oc; + float *ha = NULL, *hb = NULL, *hc = NULL; + CUdeviceptr da = 0, db = 0, dc = 0; + CUevent e0 = NULL, e1 = NULL; + CUresult r; + unsigned gx, gy, gz, bx, by, bz; + int have_ev = 0; + uint64_t t0; + double sum = 0.0; + uint32_t bad = 0, i; + int in; + + if (n == 0 || bytes / 4 != n || j->a_bytes < bytes || + j->b_bytes < bytes || j->c_bytes < bytes || + gpa_to_off(j->a_gpa, bytes, &oa) != 0 || + gpa_to_off(j->b_gpa, bytes, &ob) != 0 || + gpa_to_off(j->c_gpa, bytes, &oc) != 0) { + d->status = ST_BADJOB; + return -1; + } + ha = malloc(bytes); + hb = malloc(bytes); + hc = malloc(bytes); + if (!ha || !hb || !hc) { + d->status = ST_RAM; + goto done; + } + memcpy(ha, g_ram + oa, bytes); + memcpy(hb, g_ram + ob, bytes); + + r = cuMemAlloc(&da, bytes); + if (r == CUDA_SUCCESS) + r = cuMemAlloc(&db, bytes); + if (r == CUDA_SUCCESS) + r = cuMemAlloc(&dc, bytes); + if (r != CUDA_SUCCESS) { + d->status = ST_CUDA; + d->err_code = (uint32_t)r; + goto done; + } + + t0 = now_ns(); + r = cuMemcpyHtoD(da, ha, bytes); + if (r == CUDA_SUCCESS) + r = cuMemcpyHtoD(db, hb, bytes); + t->htod_ns = now_ns() - t0; + if (r != CUDA_SUCCESS) { + d->status = ST_CUDA; + d->err_code = (uint32_t)r; + goto done; + } + + gx = j->grid_x ? j->grid_x : (n + 255) / 256; + gy = j->grid_y ? j->grid_y : 1; + gz = j->grid_z ? j->grid_z : 1; + bx = j->block_x ? j->block_x : 256; + by = j->block_y ? j->block_y : 1; + bz = j->block_z ? j->block_z : 1; + in = (int)n; + + if (cuEventCreate(&e0, CU_EVENT_DEFAULT) == CUDA_SUCCESS && + cuEventCreate(&e1, CU_EVENT_DEFAULT) == CUDA_SUCCESS) + have_ev = 1; + if (have_ev) + cuEventRecord(e0, 0); + r = cuLaunchKernel(mutated ? g_fn_vadd_mut : g_fn_vadd, gx, gy, gz, bx, by, + bz, j->shared_mem, 0, + (void *[]){ &da, &db, &dc, &in }, NULL); + if (have_ev) + cuEventRecord(e1, 0); + if (r == CUDA_SUCCESS) + r = cuCtxSynchronize(); + if (have_ev) { + float ms = 0.0f; + + if (cuEventElapsedTime(&ms, e0, e1) == CUDA_SUCCESS) + t->kernel_ns = (uint64_t)((double)ms * 1e6); + cuEventDestroy(e0); + cuEventDestroy(e1); + } + if (r != CUDA_SUCCESS) { + d->status = ST_CUDA; + d->err_code = (uint32_t)r; + goto done; + } + + t0 = now_ns(); + r = cuMemcpyDtoH(hc, dc, bytes); + t->dtoh_ns = now_ns() - t0; + if (r != CUDA_SUCCESS) { + d->status = ST_CUDA; + d->err_code = (uint32_t)r; + goto done; + } + memcpy(g_ram + oc, hc, bytes); + + for (i = 0; i < n; i++) { + float expect = ha[i] + hb[i]; + + if (hc[i] != expect) + bad++; + sum += (double)hc[i]; + } + d->bad_count = bad; + d->checksum_bits = double_to_bits(sum); + +done: + if (da) + cuMemFree(da); + if (db) + cuMemFree(db); + if (dc) + cuMemFree(dc); + free(ha); + free(hb); + free(hc); + return d->status == ST_OK ? 0 : -1; +} + +static int run_job(const cubb_job_t *j, cubb_done_t *d, job_timing_t *t) +{ + uint64_t t0 = now_ns(); + + memset(t, 0, sizeof *t); + memset(d, 0, sizeof *d); + memcpy(d->magic, CUBD_MAGIC, 4); + d->version = CUBB_VERSION; + d->seq = j->seq; + d->opcode = j->opcode; + d->device_name_len = (uint32_t)strlen(g_dev_name); + snprintf(d->device_name, sizeof d->device_name, "%.63s", g_dev_name); + + if (j->opcode == 1) { + run_mail(j, d, t); + } else if (j->opcode == 2 || j->opcode == 3 || (j->flags & 1u)) { + int mut = (j->opcode == 3) || (j->flags & 1u); + + run_vadd(j, d, t, mut); + } else { + d->status = ST_BADJOB; + d->err_code = j->opcode; + } + d->host_ns = now_ns() - t0; + d->kernel_ns = t->kernel_ns; + t->total_ns = d->host_ns; + return d->status == ST_OK ? 0 : -1; +} + +static int execute_job_file(const char *job_path, const char *done_path, + cubb_job_t *jout, cubb_done_t *dout, job_timing_t *tout) +{ + cubb_job_t j; + cubb_done_t d; + job_timing_t t; + int rc; + + if (read_job_file(job_path, &j) != 0) + return -1; + rc = run_job(&j, &d, &t); + if (atomic_write(done_path, &d, sizeof d) != 0) + fprintf(stderr, "bridge: cannot write done file %s: %s\n", + done_path, strerror(errno)); + print_job_line(&j, &d); + if (jout) + *jout = j; + if (dout) + *dout = d; + if (tout) + *tout = t; + return rc; +} + +static int poll_loop(const char *job_path, const char *done_path, int once, int poll_ms) +{ + uint64_t last = 0; + int have = 0; + + printf("BRIDGE poll job=%s done=%s ram-mapped=4GiB once=%d\n", + job_path, done_path, once); + fflush(stdout); + for (;;) { + cubb_job_t j; + + if (read_job_file(job_path, &j) == 0 && (!have || j.seq != last)) { + execute_job_file(job_path, done_path, NULL, NULL, NULL); + last = j.seq; + have = 1; + if (once) + return 0; + } + usleep((useconds_t)poll_ms * 1000); + } +} + +static void fill_ab_gpa(uint64_t a_gpa, uint64_t b_gpa, uint32_t n) +{ + uint64_t oa, ob; + uint32_t i; + float *a, *b; + + if (gpa_to_off(a_gpa, (uint64_t)n * 4, &oa) != 0 || + gpa_to_off(b_gpa, (uint64_t)n * 4, &ob) != 0) { + fprintf(stderr, "bridge: test GPA translation failed\n"); + exit(1); + } + a = (float *)(g_ram + oa); + b = (float *)(g_ram + ob); + for (i = 0; i < n; i++) { + a[i] = (float)(i % 1000) * 0.5f; + b[i] = (float)(i % 997) * 0.25f; + } +} + +static int verify_vadd_file(const cubb_job_t *j, double *sum_out, uint32_t *bad_out, + int *bytes_match) +{ + uint64_t oa, ob, oc; + uint32_t i; + float *a, *b, *c; + double sum = 0.0; + uint32_t bad = 0; + int match = 1; + + if (gpa_to_off(j->a_gpa, j->a_bytes, &oa) != 0 || + gpa_to_off(j->b_gpa, j->b_bytes, &ob) != 0 || + gpa_to_off(j->c_gpa, j->c_bytes, &oc) != 0) + return -1; + a = (float *)(g_ram + oa); + b = (float *)(g_ram + ob); + c = (float *)(g_ram + oc); + for (i = 0; i < j->n; i++) { + float expect = a[i] + b[i]; + + if (c[i] != expect) + bad++; + if (memcmp(&c[i], &expect, sizeof expect) != 0) + match = 0; + sum += (double)c[i]; + } + *sum_out = sum; + *bad_out = bad; + *bytes_match = match; + return 0; +} + +static void make_vadd_job(cubb_job_t *j, uint64_t seq, uint32_t opcode, + uint32_t n, uint64_t a_gpa, uint64_t b_gpa, uint64_t c_gpa) +{ + memset(j, 0, sizeof *j); + memcpy(j->magic, CUBB_MAGIC, 4); + j->version = CUBB_VERSION; + j->seq = seq; + j->opcode = opcode; + j->grid_x = (n + 255) / 256; + j->block_x = 256; + j->n = n; + j->a_gpa = a_gpa; + j->a_bytes = (uint64_t)n * 4; + j->b_gpa = b_gpa; + j->b_bytes = (uint64_t)n * 4; + j->c_gpa = c_gpa; + j->c_bytes = (uint64_t)n * 4; +} + +static void make_mail_job(cubb_job_t *j, uint64_t seq, uint64_t out_gpa) +{ + memset(j, 0, sizeof *j); + memcpy(j->magic, CUBB_MAGIC, 4); + j->version = CUBB_VERSION; + j->seq = seq; + j->opcode = 1; + j->grid_x = 1; + j->block_x = 64; + j->n = (uint32_t)strlen(MAIL); + j->mail_out_gpa = out_gpa; + j->mail_out_bytes = (uint64_t)strlen(MAIL); +} + +/* ------------------------------------------------------------------ */ +/* v2 bridge: debugfs job/in -> real GPU -> out/done (see PROTOCOL.md) */ +/* ------------------------------------------------------------------ */ + +/* + * Read exactly len bytes from path at increasing offsets (ppos), in <=chunk + * slices. Returns 0 on success, -1 on open failure or any short/zero read + * (which for the debugfs job file means "no pending job"). + */ +static int bridge_read_blob(const char *path, uint8_t *buf, uint64_t len, + uint64_t chunk) +{ + int fd; + uint64_t off = 0; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + while (off < len) { + size_t want = (len - off < chunk) ? (size_t)(len - off) + : (size_t)chunk; + ssize_t r = pread(fd, buf + off, want, (off_t)off); + + if (r <= 0) { + close(fd); + return -1; + } + off += (uint64_t)r; + } + close(fd); + return 0; +} + +/* Write exactly len bytes at increasing offsets (ppos), in <=chunk slices. */ +static int bridge_write_blob(const char *path, const uint8_t *buf, uint64_t len, + uint64_t chunk) +{ + int fd; + uint64_t off = 0; + + fd = open(path, O_WRONLY | O_CREAT, 0644); + if (fd < 0) + return -1; + while (off < len) { + size_t want = (len - off < chunk) ? (size_t)(len - off) + : (size_t)chunk; + ssize_t w = pwrite(fd, buf + off, want, (off_t)off); + + if (w <= 0) { + close(fd); + return -1; + } + off += (uint64_t)w; + } + close(fd); + return 0; +} + +/* + * Read a 256-byte CUBB2 job. Returns 1 for a new valid job, 0 for idle + * (absent file, short read, zero/foreign magic, wrong version). + */ +static int bridge_read_job(const char *path, cubb2_job_t *j) +{ + if (bridge_read_blob(path, (uint8_t *)j, sizeof *j, + CUBB2_JOB_SIZE) != 0) + return 0; + if (j->magic != CUBB2_JOB_MAGIC || j->version != CUBB2_VERSION) + return 0; + return 1; +} + +/* Launch + synchronize with CUDA-event kernel timing (v2 path). */ +static CUresult launch_wait(CUfunction fn, unsigned gx, unsigned gy, unsigned gz, + unsigned bx, unsigned by, unsigned bz, + unsigned shared, void **args, uint64_t *kernel_ns) +{ + CUevent e0 = NULL, e1 = NULL; + CUresult r; + int have_ev = 0; + + if (cuEventCreate(&e0, CU_EVENT_DEFAULT) == CUDA_SUCCESS && + cuEventCreate(&e1, CU_EVENT_DEFAULT) == CUDA_SUCCESS) + have_ev = 1; + if (have_ev) + cuEventRecord(e0, 0); + r = cuLaunchKernel(fn, gx, gy, gz, bx, by, bz, shared, 0, args, NULL); + if (have_ev) + cuEventRecord(e1, 0); + if (r == CUDA_SUCCESS) + r = cuCtxSynchronize(); + if (have_ev) { + float ms = 0.0f; + + if (cuEventElapsedTime(&ms, e0, e1) == CUDA_SUCCESS) + *kernel_ns = (uint64_t)((double)ms * 1e6); + cuEventDestroy(e0); + cuEventDestroy(e1); + } + return r; +} + +static void bridge_cuda_err(cubb2_done_t *d, CUresult r) +{ + d->status = ST_CUDA; + d->err_code = (uint32_t)r; +} + +/* + * opcode 1 (mail): no inputs, output = n bytes of the mail constant (the + * kernel writes min(n, strlen) chars; a zeroed device buffer makes any + * longer requested output deterministic NULs, matching the v1 semantics). + */ +static int run_bridge_mail(const cubb2_job_t *j, cubb2_done_t *d, + uint8_t **outp, uint64_t *out_len) +{ + const char *mail = MAIL; + uint32_t mlen = (uint32_t)strlen(mail); + uint32_t len = j->n; + uint32_t launch_len = len < mlen ? len : mlen; + uint8_t *out = NULL; + CUdeviceptr dp = 0; + CUresult r; + unsigned gx, gy, gz, bx, by, bz; + uint32_t bad = 0, i; + int n; + + if (len == 0 || len > CUBB2_MAIL_MAX || j->out_bytes != len) { + d->status = ST_BADJOB; + d->err_code = EINVAL; + return -1; + } + out = malloc(len); + if (!out) { + d->status = ST_RAM; + d->err_code = ENOMEM; + return -1; + } + r = cuMemAlloc(&dp, len); + if (r == CUDA_SUCCESS) + r = cuMemsetD8(dp, 0, len); + if (r != CUDA_SUCCESS) { + bridge_cuda_err(d, r); + goto done; + } + gx = j->grid[0] ? j->grid[0] : 1; + gy = j->grid[1] ? j->grid[1] : 1; + gz = j->grid[2] ? j->grid[2] : 1; + bx = j->block[0] ? j->block[0] : 64; + by = j->block[1] ? j->block[1] : 1; + bz = j->block[2] ? j->block[2] : 1; + n = (int)launch_len; + r = launch_wait(g_fn_mail, gx, gy, gz, bx, by, bz, j->shared, + (void *[]){ &dp, &n }, &d->kernel_ns); + if (r == CUDA_SUCCESS) + r = cuMemcpyDtoH(out, dp, len); + if (r != CUDA_SUCCESS) { + bridge_cuda_err(d, r); + goto done; + } + for (i = 0; i < len; i++) + if (out[i] != (i < mlen ? (uint8_t)mail[i] : 0)) + bad++; + d->bad = bad; + d->checksum = double_to_bits(0.0); + *outp = out; + *out_len = len; + out = NULL; +done: + if (dp) + cuMemFree(dp); + free(out); + return d->status == ST_OK ? 0 : -1; +} + +/* opcode 2 (vadd): in = a (4n) then b (4n); out = c (4n), c[i]=a[i]+b[i]. */ +static int run_bridge_vadd(const cubb2_job_t *j, const uint8_t *in, + cubb2_done_t *d, uint8_t **outp, uint64_t *out_len) +{ + uint32_t n = j->n; + uint64_t bytes = (uint64_t)n * 4; + const float *ha = (const float *)in; + const float *hb = (const float *)(in + bytes); + uint8_t *hc = NULL; + float *hf; + CUdeviceptr da = 0, db = 0, dc = 0; + CUresult r; + unsigned gx, gy, gz, bx, by, bz; + double sum = 0.0; + uint32_t bad = 0, i; + int ni; + + hc = malloc(bytes); + if (!hc) { + d->status = ST_RAM; + d->err_code = ENOMEM; + return -1; + } + hf = (float *)hc; + r = cuMemAlloc(&da, bytes); + if (r == CUDA_SUCCESS) + r = cuMemAlloc(&db, bytes); + if (r == CUDA_SUCCESS) + r = cuMemAlloc(&dc, bytes); + if (r == CUDA_SUCCESS) + r = cuMemcpyHtoD(da, ha, bytes); + if (r == CUDA_SUCCESS) + r = cuMemcpyHtoD(db, hb, bytes); + if (r != CUDA_SUCCESS) { + bridge_cuda_err(d, r); + goto done; + } + gx = j->grid[0] ? j->grid[0] : (n + 255) / 256; + gy = j->grid[1] ? j->grid[1] : 1; + gz = j->grid[2] ? j->grid[2] : 1; + bx = j->block[0] ? j->block[0] : 256; + by = j->block[1] ? j->block[1] : 1; + bz = j->block[2] ? j->block[2] : 1; + ni = (int)n; + r = launch_wait(g_fn_vadd, gx, gy, gz, bx, by, bz, j->shared, + (void *[]){ &da, &db, &dc, &ni }, &d->kernel_ns); + if (r == CUDA_SUCCESS) + r = cuMemcpyDtoH(hc, dc, bytes); + if (r != CUDA_SUCCESS) { + bridge_cuda_err(d, r); + goto done; + } + for (i = 0; i < n; i++) { + float expect = ha[i] + hb[i]; + + if (hf[i] != expect) + bad++; + sum += (double)hf[i]; + } + d->bad = bad; + d->checksum = double_to_bits(sum); + *outp = hc; + *out_len = bytes; + hc = NULL; +done: + if (da) + cuMemFree(da); + if (db) + cuMemFree(db); + if (dc) + cuMemFree(dc); + free(hc); + return d->status == ST_OK ? 0 : -1; +} + +/* + * Execute one CUBB2 job. On failure d->status is always non-zero. The caller + * owns *outp (malloc'd) on success. + */ +static int bridge_execute(const char *in_path, const cubb2_job_t *j, + cubb2_done_t *d, uint8_t **outp, uint64_t *out_len) +{ + uint8_t *in = NULL; + uint64_t t0 = now_ns(); + int rc = -1; + + *outp = NULL; + *out_len = 0; + memset(d, 0, sizeof *d); + d->magic = CUBB2_DONE_MAGIC; + d->version = CUBB2_VERSION; + d->seq = j->seq; + snprintf(d->device_name, sizeof d->device_name, "%.63s", g_dev_name); + + if (j->n == 0 || j->n > CUBB2_N_MAX || + j->in_bytes > CUBB2_BYTES_MAX || j->out_bytes > CUBB2_BYTES_MAX) { + d->status = ST_BADJOB; + d->err_code = EINVAL; + fprintf(stderr, "bridge: job seq=%llu rejected: out of bounds " + "n=%u in_bytes=%llu out_bytes=%llu (n<=%u bytes<=%u)\n", + (unsigned long long)j->seq, j->n, + (unsigned long long)j->in_bytes, + (unsigned long long)j->out_bytes, CUBB2_N_MAX, + CUBB2_BYTES_MAX); + goto done; + } + if (j->in_bytes) { + in = malloc((size_t)j->in_bytes); + if (!in) { + d->status = ST_RAM; + d->err_code = ENOMEM; + goto done; + } + if (bridge_read_blob(in_path, in, j->in_bytes, CUBB2_CHUNK) != 0) { + d->status = ST_RAM; + d->err_code = EIO; + fprintf(stderr, "bridge: job seq=%llu failed: cannot " + "read %llu input bytes from %s\n", + (unsigned long long)j->seq, + (unsigned long long)j->in_bytes, in_path); + goto done; + } + } + if (j->opcode == 1) { + rc = run_bridge_mail(j, d, outp, out_len); + } else if (j->opcode == 2) { + if (j->in_bytes != 8ULL * j->n || j->out_bytes != 4ULL * j->n) { + d->status = ST_BADJOB; + d->err_code = EINVAL; + fprintf(stderr, "bridge: job seq=%llu rejected: vadd " + "needs in=%llu out=%llu (got in=%llu out=%llu)\n", + (unsigned long long)j->seq, + (unsigned long long)(8ULL * j->n), + (unsigned long long)(4ULL * j->n), + (unsigned long long)j->in_bytes, + (unsigned long long)j->out_bytes); + goto done; + } + rc = run_bridge_vadd(j, in, d, outp, out_len); + } else { + d->status = ST_BADJOB; + d->err_code = j->opcode; + fprintf(stderr, "bridge: job seq=%llu rejected: unknown opcode %u\n", + (unsigned long long)j->seq, j->opcode); + } +done: + free(in); + d->host_ns = now_ns() - t0; + return rc; +} + +static void print_bridge_line(const cubb2_job_t *j, const cubb2_done_t *d) +{ + double cs = bits_to_double(d->checksum); + + printf("CUBB2 seq=%llu op=%u n=%u gpu=\"%s\" kernel_ns=%llu " + "status=%s checksum=%.6f bad=%u out_bytes=%u\n", + (unsigned long long)j->seq, j->opcode, j->n, d->device_name, + (unsigned long long)d->kernel_ns, + d->status == ST_OK ? "ok" : "err", cs, d->bad, + d->out_bytes_written); + fflush(stdout); +} + +/* Execute, write out (success only), then always write a done record. */ +static int bridge_process_job(const cubb2_job_t *j, const char *in_path, + const char *out_path, const char *done_path) +{ + cubb2_done_t d; + uint8_t *out = NULL; + uint64_t out_len = 0; + + bridge_execute(in_path, j, &d, &out, &out_len); + if (d.status == ST_OK) { + if (!out || out_len == 0 || + bridge_write_blob(out_path, out, out_len, + CUBB2_CHUNK) != 0) { + fprintf(stderr, "bridge: job seq=%llu failed: cannot " + "write %llu output bytes to %s: %s\n", + (unsigned long long)j->seq, + (unsigned long long)out_len, out_path, + strerror(errno)); + d.status = ST_RAM; + d.err_code = (uint32_t)errno; + } else { + d.out_bytes_written = (uint32_t)out_len; + } + } + if (bridge_write_blob(done_path, (const uint8_t *)&d, sizeof d, + CUBB2_DONE_SIZE) != 0) { + fprintf(stderr, "bridge: job seq=%llu failed: cannot write done " + "record to %s: %s\n", (unsigned long long)j->seq, + done_path, strerror(errno)); + } + print_bridge_line(j, &d); + free(out); + return d.status == ST_OK ? 0 : -1; +} + +static int run_bridge(const char *job_path, const char *in_path, + const char *out_path, const char *done_path, int once, + int poll_ms) +{ + uint64_t last = 0; + int have = 0; + + if (poll_ms < 1) + poll_ms = 1; + printf("BRIDGE v2 job=%s in=%s out=%s done=%s gpu=\"%s\" poll_ms=%d " + "once=%d\n", job_path, in_path, out_path, done_path, g_dev_name, + poll_ms, once); + fflush(stdout); + for (;;) { + cubb2_job_t j; + + if (bridge_read_job(job_path, &j) == 1) { + if (have && j.seq <= last) { + /* stale/duplicate seq: ignored */ + } else { + bridge_process_job(&j, in_path, out_path, + done_path); + last = j.seq; + have = 1; + if (once) + return 0; + } + } + usleep((useconds_t)poll_ms * 1000); + } +} + +static int run_selftest(const char *dir) +{ + char ram_path[4096], job_path[4096], done_path[4096]; + const uint32_t n = 1u << 20; + cubb_job_t j; + cubb_done_t d; + job_timing_t t; + double sum; + uint32_t bad; + int match, fails = 0; + + snprintf(ram_path, sizeof ram_path, "%s/ram.bin", dir); + snprintf(job_path, sizeof job_path, "%s/job.bin", dir); + snprintf(done_path, sizeof done_path, "%s/done.bin", dir); + if (map_ram_file(ram_path, 1) != 0) + return 1; + printf("SELFTEST ram=%s size=%llu\n", ram_path, + (unsigned long long)g_ram_len); + fflush(stdout); + + make_mail_job(&j, 1, 0x40000000ULL); + if (atomic_write(job_path, &j, sizeof j) != 0 || + execute_job_file(job_path, done_path, &j, &d, &t) != 0) { + printf("SELFTEST mail status=err\n"); + fails++; + } else { + char mail[256]; + uint64_t off = 0x40000000ULL; + + memset(mail, 0, sizeof mail); + memcpy(mail, g_ram + off, j.n); + match = memcmp(mail, MAIL, j.n) == 0; + if (!match || d.bad_count != 0) + fails++; + printf("SELFTEST mail gpa=0x%llx len=%u filecheck=%s bad=%u status=%s\n", + (unsigned long long)j.mail_out_gpa, j.n, + match ? "yes" : "no", d.bad_count, + d.status == ST_OK ? "ok" : "err"); + } + fflush(stdout); + + make_vadd_job(&j, 2, 2, n, 0x10000000ULL, 0x20000000ULL, 0x30000000ULL); + fill_ab_gpa(j.a_gpa, j.b_gpa, n); + if (atomic_write(job_path, &j, sizeof j) != 0 || + execute_job_file(job_path, done_path, &j, &d, &t) != 0 || + verify_vadd_file(&j, &sum, &bad, &match) != 0) { + printf("SELFTEST vadd window=low status=err\n"); + fails++; + } else { + double delta = bits_to_double(d.checksum_bits) - VADD_EXPECTED_CHECKSUM; + + if (delta < 0) + delta = -delta; + if (d.bad_count != 0 || bad != 0 || !match || delta > CHECKSUM_TOL) + fails++; + printf("SELFTEST vadd window=low n=%u a=0x%llx b=0x%llx c=0x%llx " + "filecheck=%s bad=%u checksum=%.6f delta=%.3e status=%s\n", + n, (unsigned long long)j.a_gpa, (unsigned long long)j.b_gpa, + (unsigned long long)j.c_gpa, match ? "yes" : "no", bad, + sum, delta, d.status == ST_OK ? "ok" : "err"); + } + fflush(stdout); + + make_vadd_job(&j, 3, 2, n, 0x100100000ULL, 0x100800000ULL, 0x101000000ULL); + fill_ab_gpa(j.a_gpa, j.b_gpa, n); + if (atomic_write(job_path, &j, sizeof j) != 0 || + execute_job_file(job_path, done_path, &j, &d, &t) != 0 || + verify_vadd_file(&j, &sum, &bad, &match) != 0) { + printf("SELFTEST vadd window=high status=err\n"); + fails++; + } else { + double delta = bits_to_double(d.checksum_bits) - VADD_EXPECTED_CHECKSUM; + + if (delta < 0) + delta = -delta; + if (d.bad_count != 0 || bad != 0 || !match || delta > CHECKSUM_TOL) + fails++; + printf("SELFTEST vadd window=high n=%u a=0x%llx b=0x%llx c=0x%llx " + "filecheck=%s bad=%u checksum=%.6f delta=%.3e status=%s\n", + n, (unsigned long long)j.a_gpa, (unsigned long long)j.b_gpa, + (unsigned long long)j.c_gpa, match ? "yes" : "no", bad, + sum, delta, d.status == ST_OK ? "ok" : "err"); + } + fflush(stdout); + + printf("SELFTEST %s\n", fails == 0 ? "PASS" : "FAIL"); + fflush(stdout); + return fails == 0 ? 0 : 1; +} + +static int run_mutation(const char *dir) +{ + char ram_path[4096], job_path[4096], done_path[4096]; + const uint32_t n = 1u << 20; + cubb_job_t j; + cubb_done_t d; + job_timing_t t; + double sum_base = 0.0, sum_mut = 0.0, delta, exp_delta; + uint32_t bad_base = 0, bad_mut = 0; + int match_base = 0, match_mut = 0, fails = 0; + uint64_t off_c; + float c0 = 0.0f, a0 = 0.0f, b0 = 0.0f; + + snprintf(ram_path, sizeof ram_path, "%s/mutation-ram.bin", dir); + snprintf(job_path, sizeof job_path, "%s/mutation-job.bin", dir); + snprintf(done_path, sizeof done_path, "%s/mutation-done.bin", dir); + if (map_ram_file(ram_path, 1) != 0) + return 1; + + make_vadd_job(&j, 1, 2, n, 0x10000000ULL, 0x20000000ULL, 0x30000000ULL); + fill_ab_gpa(j.a_gpa, j.b_gpa, n); + if (atomic_write(job_path, &j, sizeof j) != 0 || + execute_job_file(job_path, done_path, &j, &d, &t) != 0 || + verify_vadd_file(&j, &sum_base, &bad_base, &match_base) != 0) { + printf("MUTATION baseline status=err\n"); + return 1; + } + printf("MUTATION baseline op=2 n=%u checksum=%.6f bad=%u filecheck=%s\n", + n, sum_base, bad_base, match_base ? "yes" : "no"); + + j.opcode = 3; + j.seq = 2; + if (atomic_write(job_path, &j, sizeof j) != 0 || + execute_job_file(job_path, done_path, &j, &d, &t) != 0 || + verify_vadd_file(&j, &sum_mut, &bad_mut, &match_mut) != 0) { + printf("MUTATION variant status=err\n"); + return 1; + } + printf("MUTATION variant op=3 n=%u checksum=%.6f bad=%u filecheck=%s\n", + n, sum_mut, bad_mut, match_mut ? "yes" : "no"); + + if (gpa_to_off(j.c_gpa, j.c_bytes, &off_c) == 0) + c0 = ((float *)(g_ram + off_c))[0]; + a0 = 0.0f * 0.5f; + b0 = 0.0f * 0.25f; + printf("MUTATION sample[0] a+b=%.6f mutated_c=%.6f naive_check=%s\n", + (double)(a0 + b0), (double)c0, + (c0 == a0 + b0) ? "PASS" : "FAIL"); + + delta = sum_mut - sum_base; + exp_delta = (double)n; + printf("MUTATION checksum_delta=%.6f expected_delta=%.6f\n", delta, exp_delta); + if (delta < 0) + delta = -delta; + if (bad_base != 0 || bad_mut != n || !match_base || match_mut || + (delta - exp_delta > 1e-3 || delta - exp_delta < -1e-3)) + fails++; + + printf("MUTATION %s\n", fails == 0 ? "PASS" : "FAIL"); + fflush(stdout); + return fails == 0 ? 0 : 1; +} + +static void cpu_baseline(uint32_t n, uint64_t *ns_out, double *sum_out) +{ + float *a, *b, *c; + uint32_t i; + uint64_t t0; + double sum = 0.0; + + a = malloc((size_t)n * 4); + b = malloc((size_t)n * 4); + c = malloc((size_t)n * 4); + if (!a || !b || !c) { + free(a); + free(b); + free(c); + *ns_out = 0; + *sum_out = 0.0; + return; + } + for (i = 0; i < n; i++) { + a[i] = (float)(i % 1000) * 0.5f; + b[i] = (float)(i % 997) * 0.25f; + } + t0 = now_ns(); + for (i = 0; i < n; i++) + c[i] = a[i] + b[i]; + *ns_out = now_ns() - t0; + for (i = 0; i < n; i++) + sum += (double)c[i]; + *sum_out = sum; + free(a); + free(b); + free(c); +} + +static int run_bench(const char *dir, int big_iters, int verbose) +{ + char ram_path[4096], job_path[4096], done_path[4096]; + const uint32_t sizes[2] = { 1u << 20, 1u << 24 }; + cubb_job_t j; + cubb_done_t d; + job_timing_t t; + int fails = 0, s; + + snprintf(ram_path, sizeof ram_path, "%s/bench-ram.bin", dir); + snprintf(job_path, sizeof job_path, "%s/bench-job.bin", dir); + snprintf(done_path, sizeof done_path, "%s/bench-done.bin", dir); + if (map_ram_file(ram_path, 1) != 0) + return 1; + + for (s = 0; s < 2; s++) { + uint32_t n = sizes[s]; + double sum = 0.0; + uint32_t bad = 0; + int match = 0; + uint64_t cpu_ns = 0; + double cpu_sum = 0.0; + + make_vadd_job(&j, (uint64_t)(s + 1), 2, n, + 0x10000000ULL, 0x20000000ULL, 0x30000000ULL); + fill_ab_gpa(j.a_gpa, j.b_gpa, n); + if (atomic_write(job_path, &j, sizeof j) != 0 || + execute_job_file(job_path, done_path, &j, &d, &t) != 0 || + verify_vadd_file(&j, &sum, &bad, &match) != 0 || d.bad_count != 0) { + printf("TIMING op=vadd n=%u status=err\n", n); + fails++; + continue; + } + printf("TIMING op=vadd n=%u kernel_ns=%llu htod_ns=%llu dtoh_ns=%llu " + "job_ns=%llu bad=%u checksum=%.6f\n", + n, (unsigned long long)t.kernel_ns, + (unsigned long long)t.htod_ns, + (unsigned long long)t.dtoh_ns, + (unsigned long long)t.total_ns, bad, sum); + cpu_baseline(n, &cpu_ns, &cpu_sum); + printf("TIMING op=cpu-vadd n=%u cpu_ns=%llu cpu_checksum=%.6f " + "gpu_kernel_ns=%llu speedup_vs_cpu=%.1fx\n", + n, (unsigned long long)cpu_ns, cpu_sum, + (unsigned long long)t.kernel_ns, + t.kernel_ns ? (double)cpu_ns / (double)t.kernel_ns : 0.0); + fflush(stdout); + } + + if (big_iters > 0) { + uint32_t n = sizes[1]; + int i; + + make_vadd_job(&j, 100, 2, n, 0x10000000ULL, 0x20000000ULL, 0x30000000ULL); + fill_ab_gpa(j.a_gpa, j.b_gpa, n); + for (i = 0; i < big_iters; i++) { + j.seq = (uint64_t)(100 + i); + if (atomic_write(job_path, &j, sizeof j) != 0 || + execute_job_file(job_path, done_path, &j, &d, &t) != 0) { + printf("STRESS iter=%d status=err\n", i); + fails++; + break; + } + if (verbose || i % 25 == 0 || i == big_iters - 1) + printf("STRESS iter=%d seq=%llu n=%u kernel_ns=%llu " + "job_ns=%llu status=ok bad=%u\n", + i, (unsigned long long)j.seq, n, + (unsigned long long)t.kernel_ns, + (unsigned long long)t.total_ns, d.bad_count); + } + } + fflush(stdout); + printf("BENCH %s\n", fails == 0 ? "PASS" : "FAIL"); + fflush(stdout); + return fails == 0 ? 0 : 1; +} + +static void usage(void) +{ + fprintf(stderr, + "usage:\n" + " cuda_bridge --bridge --job J --in I --out O --done D " + "[--once] [--poll-ms N]\n" + " cuda_bridge --job J --done D --ram R [--once] [--poll-ms N]\n" + " cuda_bridge --test [--dir D]\n" + " cuda_bridge --mutation-test [--dir D]\n" + " cuda_bridge --bench [--dir D] [--big-iters N] [--verbose]\n"); + exit(2); +} + +int main(int argc, char **argv) +{ + const char *job_path = NULL, *done_path = NULL, *ram_path = NULL; + const char *in_path = NULL, *out_path = NULL; + const char *dir = "/tmp/opencode/cuda-bridge-test"; + int mode_job = 0, mode_test = 0, mode_bench = 0, mode_mut = 0; + int mode_bridge = 0; + int once = 0, poll_ms = 10, poll_ms_set = 0, big_iters = 0, verbose = 0; + int i; + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--job") && i + 1 < argc) + job_path = argv[++i]; + else if (!strcmp(argv[i], "--done") && i + 1 < argc) + done_path = argv[++i]; + else if (!strcmp(argv[i], "--ram") && i + 1 < argc) + ram_path = argv[++i]; + else if (!strcmp(argv[i], "--in") && i + 1 < argc) + in_path = argv[++i]; + else if (!strcmp(argv[i], "--out") && i + 1 < argc) + out_path = argv[++i]; + else if (!strcmp(argv[i], "--dir") && i + 1 < argc) + dir = argv[++i]; + else if (!strcmp(argv[i], "--poll-ms") && i + 1 < argc) { + poll_ms = atoi(argv[++i]); + poll_ms_set = 1; + } else if (!strcmp(argv[i], "--big-iters") && i + 1 < argc) + big_iters = atoi(argv[++i]); + else if (!strcmp(argv[i], "--once")) + once = 1; + else if (!strcmp(argv[i], "--verbose")) + verbose = 1; + else if (!strcmp(argv[i], "--job-mode")) + mode_job = 1; + else if (!strcmp(argv[i], "--bridge")) + mode_bridge = 1; + else if (!strcmp(argv[i], "--test")) + mode_test = 1; + else if (!strcmp(argv[i], "--bench")) + mode_bench = 1; + else if (!strcmp(argv[i], "--mutation-test")) + mode_mut = 1; + else if (!strcmp(argv[i], "--help")) + usage(); + else + usage(); + } + if ((job_path || done_path || ram_path) && !mode_bridge) + mode_job = 1; + if (mode_test + mode_bench + mode_mut + mode_job + mode_bridge != 1) + usage(); + + cuda_init(); + + if (mode_test) + return run_selftest(dir); + if (mode_mut) + return run_mutation(dir); + if (mode_bench) + return run_bench(dir, big_iters, verbose); + if (mode_bridge) { + /* Frozen v2 CLI: --bridge --job J --in I --out O --done D. */ + if (!job_path || !in_path || !out_path || !done_path) { + fprintf(stderr, "bridge: --bridge requires --job, --in, " + "--out and --done\n"); + return 2; + } + if (!poll_ms_set) + poll_ms = 50; + return run_bridge(job_path, in_path, out_path, done_path, once, + poll_ms); + } + if (!job_path || !done_path || !ram_path) { + fprintf(stderr, "bridge: --job, --done and --ram are required\n"); + return 2; + } + if (map_ram_file(ram_path, 0) != 0) + return 1; + return poll_loop(job_path, done_path, once, poll_ms); +} diff --git a/tools/cuda-bridge/embed_ptx.py b/tools/cuda-bridge/embed_ptx.py new file mode 100644 index 0000000..71b0e5f --- /dev/null +++ b/tools/cuda-bridge/embed_ptx.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# embed_ptx.py - turn a PTX text file into a NUL-terminated C byte array. +# Used by the Makefile so cuda_bridge carries its kernels without any runtime +# dependency on files or on a CUDA toolkit. + +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print("usage: embed_ptx.py <in.ptx> <out.h>", file=sys.stderr) + return 2 + data = open(sys.argv[1], "rb").read() + if b"\x00" in data: + print("embed_ptx: input contains NUL", file=sys.stderr) + return 1 + rows = [] + for off in range(0, len(data), 12): + chunk = data[off:off + 12] + rows.append("\t" + " ".join(f"0x{b:02x}," for b in chunk)) + body = "\n".join(rows) + text = ( + "/* SPDX-License-Identifier: BSD-3-Clause */\n" + "/* generated by embed_ptx.py from %s - do not edit */\n" + "static const unsigned char kernels_ptx[] = {\n" + "%s\n" + "\t0x00\n" + "};\n" + "static const unsigned int kernels_ptx_len = %d;\n" + ) % (sys.argv[1], body, len(data)) + with open(sys.argv[2], "w") as fh: + fh.write(text) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cuda-bridge/kernels.cu b/tools/cuda-bridge/kernels.cu new file mode 100644 index 0000000..508c94c --- /dev/null +++ b/tools/cuda-bridge/kernels.cu @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// kernels.cu - CUDA kernels for the host-side CUDA bridge. +// +// Exactly mirrors the victory_mail semantics: mail_kernel writes every +// character of the message from device memory, vadd computes c = a + b. +// vadd_mutated is the falsifiability variant (adds 1.0f); it exists only to +// prove the bridge executes the real requested kernel, never a canned answer. +// +// Compiled to PTX at build time by nvcc (see Makefile); the PTX is embedded +// into cuda_bridge and loaded with cuModuleLoadData. + +#define MAIL "the little gpu man says you've got mail" + +__device__ const char kMail[] = MAIL; + +extern "C" __global__ void mail_kernel(char *out, int len) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + + if (i < len) + out[i] = kMail[i]; +} + +extern "C" __global__ void vadd(const float *a, const float *b, float *c, int n) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + + if (i < n) + c[i] = a[i] + b[i]; +} + +extern "C" __global__ void vadd_mutated(const float *a, const float *b, + float *c, int n) +{ + int i = blockIdx.x * blockDim.x + threadIdx.x; + + if (i < n) + c[i] = a[i] + b[i] + 1.0f; +} diff --git a/tools/cuda-bridge/kernels.ptx b/tools/cuda-bridge/kernels.ptx new file mode 100644 index 0000000..4d1b3ad --- /dev/null +++ b/tools/cuda-bridge/kernels.ptx @@ -0,0 +1,132 @@ +// +// Generated by NVIDIA NVVM Compiler +// +// Compiler Build ID: CL-32267302 +// Cuda compilation tools, release 12.0, V12.0.140 +// Based on NVVM 7.0.1 +// + +.version 8.0 +.target sm_75 +.address_size 64 + + // .globl mail_kernel +.global .align 1 .b8 kMail[40] = {116, 104, 101, 32, 108, 105, 116, 116, 108, 101, 32, 103, 112, 117, 32, 109, 97, 110, 32, 115, 97, 121, 115, 32, 121, 111, 117, 39, 118, 101, 32, 103, 111, 116, 32, 109, 97, 105, 108, 0}; + +.visible .entry mail_kernel( + .param .u64 mail_kernel_param_0, + .param .u32 mail_kernel_param_1 +) +{ + .reg .pred %p<2>; + .reg .b16 %rs<3>; + .reg .b32 %r<6>; + .reg .b64 %rd<7>; + + + ld.param.u64 %rd1, [mail_kernel_param_0]; + ld.param.u32 %r2, [mail_kernel_param_1]; + mov.u32 %r3, %ctaid.x; + mov.u32 %r4, %ntid.x; + mov.u32 %r5, %tid.x; + mad.lo.s32 %r1, %r3, %r4, %r5; + setp.ge.s32 %p1, %r1, %r2; + @%p1 bra $L__BB0_2; + + cvta.to.global.u64 %rd2, %rd1; + cvt.s64.s32 %rd3, %r1; + mov.u64 %rd4, kMail; + add.s64 %rd5, %rd4, %rd3; + ld.global.nc.u8 %rs1, [%rd5]; + cvt.u16.u8 %rs2, %rs1; + add.s64 %rd6, %rd2, %rd3; + st.global.u8 [%rd6], %rs2; + +$L__BB0_2: + ret; + +} + // .globl vadd +.visible .entry vadd( + .param .u64 vadd_param_0, + .param .u64 vadd_param_1, + .param .u64 vadd_param_2, + .param .u32 vadd_param_3 +) +{ + .reg .pred %p<2>; + .reg .f32 %f<4>; + .reg .b32 %r<6>; + .reg .b64 %rd<11>; + + + ld.param.u64 %rd1, [vadd_param_0]; + ld.param.u64 %rd2, [vadd_param_1]; + ld.param.u64 %rd3, [vadd_param_2]; + ld.param.u32 %r2, [vadd_param_3]; + mov.u32 %r3, %ctaid.x; + mov.u32 %r4, %ntid.x; + mov.u32 %r5, %tid.x; + mad.lo.s32 %r1, %r3, %r4, %r5; + setp.ge.s32 %p1, %r1, %r2; + @%p1 bra $L__BB1_2; + + cvta.to.global.u64 %rd4, %rd1; + mul.wide.s32 %rd5, %r1, 4; + add.s64 %rd6, %rd4, %rd5; + cvta.to.global.u64 %rd7, %rd2; + add.s64 %rd8, %rd7, %rd5; + ld.global.f32 %f1, [%rd8]; + ld.global.f32 %f2, [%rd6]; + add.f32 %f3, %f2, %f1; + cvta.to.global.u64 %rd9, %rd3; + add.s64 %rd10, %rd9, %rd5; + st.global.f32 [%rd10], %f3; + +$L__BB1_2: + ret; + +} + // .globl vadd_mutated +.visible .entry vadd_mutated( + .param .u64 vadd_mutated_param_0, + .param .u64 vadd_mutated_param_1, + .param .u64 vadd_mutated_param_2, + .param .u32 vadd_mutated_param_3 +) +{ + .reg .pred %p<2>; + .reg .f32 %f<5>; + .reg .b32 %r<6>; + .reg .b64 %rd<11>; + + + ld.param.u64 %rd1, [vadd_mutated_param_0]; + ld.param.u64 %rd2, [vadd_mutated_param_1]; + ld.param.u64 %rd3, [vadd_mutated_param_2]; + ld.param.u32 %r2, [vadd_mutated_param_3]; + mov.u32 %r3, %ctaid.x; + mov.u32 %r4, %ntid.x; + mov.u32 %r5, %tid.x; + mad.lo.s32 %r1, %r3, %r4, %r5; + setp.ge.s32 %p1, %r1, %r2; + @%p1 bra $L__BB2_2; + + cvta.to.global.u64 %rd4, %rd1; + mul.wide.s32 %rd5, %r1, 4; + add.s64 %rd6, %rd4, %rd5; + cvta.to.global.u64 %rd7, %rd2; + add.s64 %rd8, %rd7, %rd5; + ld.global.f32 %f1, [%rd8]; + ld.global.f32 %f2, [%rd6]; + add.f32 %f3, %f2, %f1; + add.f32 %f4, %f3, 0f3F800000; + cvta.to.global.u64 %rd9, %rd3; + add.s64 %rd10, %rd9, %rd5; + st.global.f32 [%rd10], %f4; + +$L__BB2_2: + ret; + +} + diff --git a/tools/cuda-bridge/kernels_ptx.h b/tools/cuda-bridge/kernels_ptx.h new file mode 100644 index 0000000..41f08fd --- /dev/null +++ b/tools/cuda-bridge/kernels_ptx.h @@ -0,0 +1,256 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* generated by embed_ptx.py from kernels.ptx - do not edit */ +static const unsigned char kernels_ptx[] = { + 0x2f, 0x2f, 0x0a, 0x2f, 0x2f, 0x20, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x4e, 0x56, 0x49, 0x44, 0x49, + 0x41, 0x20, 0x4e, 0x56, 0x56, 0x4d, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, + 0x6c, 0x65, 0x72, 0x0a, 0x2f, 0x2f, 0x0a, 0x2f, 0x2f, 0x20, 0x43, 0x6f, + 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x20, 0x49, 0x44, 0x3a, 0x20, 0x43, 0x4c, 0x2d, 0x33, 0x32, 0x32, 0x36, + 0x37, 0x33, 0x30, 0x32, 0x0a, 0x2f, 0x2f, 0x20, 0x43, 0x75, 0x64, 0x61, + 0x20, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x20, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2c, 0x20, 0x72, 0x65, 0x6c, 0x65, + 0x61, 0x73, 0x65, 0x20, 0x31, 0x32, 0x2e, 0x30, 0x2c, 0x20, 0x56, 0x31, + 0x32, 0x2e, 0x30, 0x2e, 0x31, 0x34, 0x30, 0x0a, 0x2f, 0x2f, 0x20, 0x42, + 0x61, 0x73, 0x65, 0x64, 0x20, 0x6f, 0x6e, 0x20, 0x4e, 0x56, 0x56, 0x4d, + 0x20, 0x37, 0x2e, 0x30, 0x2e, 0x31, 0x0a, 0x2f, 0x2f, 0x0a, 0x0a, 0x2e, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x38, 0x2e, 0x30, 0x0a, + 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x20, 0x73, 0x6d, 0x5f, 0x37, + 0x35, 0x0a, 0x2e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x73, + 0x69, 0x7a, 0x65, 0x20, 0x36, 0x34, 0x0a, 0x0a, 0x09, 0x2f, 0x2f, 0x20, + 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x6c, 0x09, 0x6d, 0x61, 0x69, 0x6c, 0x5f, + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x0a, 0x2e, 0x67, 0x6c, 0x6f, 0x62, + 0x61, 0x6c, 0x20, 0x2e, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x20, 0x31, 0x20, + 0x2e, 0x62, 0x38, 0x20, 0x6b, 0x4d, 0x61, 0x69, 0x6c, 0x5b, 0x34, 0x30, + 0x5d, 0x20, 0x3d, 0x20, 0x7b, 0x31, 0x31, 0x36, 0x2c, 0x20, 0x31, 0x30, + 0x34, 0x2c, 0x20, 0x31, 0x30, 0x31, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, + 0x31, 0x30, 0x38, 0x2c, 0x20, 0x31, 0x30, 0x35, 0x2c, 0x20, 0x31, 0x31, + 0x36, 0x2c, 0x20, 0x31, 0x31, 0x36, 0x2c, 0x20, 0x31, 0x30, 0x38, 0x2c, + 0x20, 0x31, 0x30, 0x31, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x31, 0x30, + 0x33, 0x2c, 0x20, 0x31, 0x31, 0x32, 0x2c, 0x20, 0x31, 0x31, 0x37, 0x2c, + 0x20, 0x33, 0x32, 0x2c, 0x20, 0x31, 0x30, 0x39, 0x2c, 0x20, 0x39, 0x37, + 0x2c, 0x20, 0x31, 0x31, 0x30, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x31, + 0x31, 0x35, 0x2c, 0x20, 0x39, 0x37, 0x2c, 0x20, 0x31, 0x32, 0x31, 0x2c, + 0x20, 0x31, 0x31, 0x35, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x31, 0x32, + 0x31, 0x2c, 0x20, 0x31, 0x31, 0x31, 0x2c, 0x20, 0x31, 0x31, 0x37, 0x2c, + 0x20, 0x33, 0x39, 0x2c, 0x20, 0x31, 0x31, 0x38, 0x2c, 0x20, 0x31, 0x30, + 0x31, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x31, 0x30, 0x33, 0x2c, 0x20, + 0x31, 0x31, 0x31, 0x2c, 0x20, 0x31, 0x31, 0x36, 0x2c, 0x20, 0x33, 0x32, + 0x2c, 0x20, 0x31, 0x30, 0x39, 0x2c, 0x20, 0x39, 0x37, 0x2c, 0x20, 0x31, + 0x30, 0x35, 0x2c, 0x20, 0x31, 0x30, 0x38, 0x2c, 0x20, 0x30, 0x7d, 0x3b, + 0x0a, 0x0a, 0x2e, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x20, 0x2e, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x6b, + 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x28, 0x0a, 0x09, 0x2e, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x20, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x6d, 0x61, 0x69, 0x6c, + 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x5f, 0x30, 0x2c, 0x0a, 0x09, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x20, 0x2e, 0x75, 0x33, 0x32, 0x20, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x6b, + 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, + 0x31, 0x0a, 0x29, 0x0a, 0x7b, 0x0a, 0x09, 0x2e, 0x72, 0x65, 0x67, 0x20, + 0x2e, 0x70, 0x72, 0x65, 0x64, 0x20, 0x09, 0x25, 0x70, 0x3c, 0x32, 0x3e, + 0x3b, 0x0a, 0x09, 0x2e, 0x72, 0x65, 0x67, 0x20, 0x2e, 0x62, 0x31, 0x36, + 0x20, 0x09, 0x25, 0x72, 0x73, 0x3c, 0x33, 0x3e, 0x3b, 0x0a, 0x09, 0x2e, + 0x72, 0x65, 0x67, 0x20, 0x2e, 0x62, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, + 0x3c, 0x36, 0x3e, 0x3b, 0x0a, 0x09, 0x2e, 0x72, 0x65, 0x67, 0x20, 0x2e, + 0x62, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x3c, 0x37, 0x3e, 0x3b, + 0x0a, 0x0a, 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x2e, 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x31, 0x2c, 0x20, + 0x5b, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, + 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x30, 0x5d, 0x3b, 0x0a, 0x09, + 0x6c, 0x64, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x2e, 0x75, 0x33, 0x32, + 0x20, 0x09, 0x25, 0x72, 0x32, 0x2c, 0x20, 0x5b, 0x6d, 0x61, 0x69, 0x6c, + 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x5f, 0x31, 0x5d, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, 0x75, + 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x33, 0x2c, 0x20, 0x25, 0x63, 0x74, + 0x61, 0x69, 0x64, 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, + 0x75, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x34, 0x2c, 0x20, 0x25, 0x6e, + 0x74, 0x69, 0x64, 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, + 0x75, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x35, 0x2c, 0x20, 0x25, 0x74, + 0x69, 0x64, 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x61, 0x64, 0x2e, 0x6c, + 0x6f, 0x2e, 0x73, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x31, 0x2c, 0x20, + 0x25, 0x72, 0x33, 0x2c, 0x20, 0x25, 0x72, 0x34, 0x2c, 0x20, 0x25, 0x72, + 0x35, 0x3b, 0x0a, 0x09, 0x73, 0x65, 0x74, 0x70, 0x2e, 0x67, 0x65, 0x2e, + 0x73, 0x33, 0x32, 0x20, 0x09, 0x25, 0x70, 0x31, 0x2c, 0x20, 0x25, 0x72, + 0x31, 0x2c, 0x20, 0x25, 0x72, 0x32, 0x3b, 0x0a, 0x09, 0x40, 0x25, 0x70, + 0x31, 0x20, 0x62, 0x72, 0x61, 0x20, 0x09, 0x24, 0x4c, 0x5f, 0x5f, 0x42, + 0x42, 0x30, 0x5f, 0x32, 0x3b, 0x0a, 0x0a, 0x09, 0x63, 0x76, 0x74, 0x61, + 0x2e, 0x74, 0x6f, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x75, + 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x32, 0x2c, 0x20, 0x25, 0x72, + 0x64, 0x31, 0x3b, 0x0a, 0x09, 0x63, 0x76, 0x74, 0x2e, 0x73, 0x36, 0x34, + 0x2e, 0x73, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x64, 0x33, 0x2c, 0x20, + 0x25, 0x72, 0x31, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, 0x75, 0x36, + 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x34, 0x2c, 0x20, 0x6b, 0x4d, 0x61, + 0x69, 0x6c, 0x3b, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x2e, 0x73, 0x36, 0x34, + 0x20, 0x09, 0x25, 0x72, 0x64, 0x35, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x34, + 0x2c, 0x20, 0x25, 0x72, 0x64, 0x33, 0x3b, 0x0a, 0x09, 0x6c, 0x64, 0x2e, + 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x6e, 0x63, 0x2e, 0x75, 0x38, + 0x20, 0x09, 0x25, 0x72, 0x73, 0x31, 0x2c, 0x20, 0x5b, 0x25, 0x72, 0x64, + 0x35, 0x5d, 0x3b, 0x0a, 0x09, 0x63, 0x76, 0x74, 0x2e, 0x75, 0x31, 0x36, + 0x2e, 0x75, 0x38, 0x20, 0x09, 0x25, 0x72, 0x73, 0x32, 0x2c, 0x20, 0x25, + 0x72, 0x73, 0x31, 0x3b, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x2e, 0x73, 0x36, + 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x36, 0x2c, 0x20, 0x25, 0x72, 0x64, + 0x32, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x33, 0x3b, 0x0a, 0x09, 0x73, 0x74, + 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x75, 0x38, 0x20, 0x09, + 0x5b, 0x25, 0x72, 0x64, 0x36, 0x5d, 0x2c, 0x20, 0x25, 0x72, 0x73, 0x32, + 0x3b, 0x0a, 0x0a, 0x24, 0x4c, 0x5f, 0x5f, 0x42, 0x42, 0x30, 0x5f, 0x32, + 0x3a, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x3b, 0x0a, 0x0a, 0x7d, 0x0a, 0x09, + 0x2f, 0x2f, 0x20, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x6c, 0x09, 0x76, 0x61, + 0x64, 0x64, 0x0a, 0x2e, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x20, + 0x2e, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x76, 0x61, 0x64, 0x64, 0x28, + 0x0a, 0x09, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x20, 0x2e, 0x75, 0x36, + 0x34, 0x20, 0x76, 0x61, 0x64, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x5f, 0x30, 0x2c, 0x0a, 0x09, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x20, + 0x2e, 0x75, 0x36, 0x34, 0x20, 0x76, 0x61, 0x64, 0x64, 0x5f, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x5f, 0x31, 0x2c, 0x0a, 0x09, 0x2e, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x20, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x76, 0x61, 0x64, 0x64, + 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x32, 0x2c, 0x0a, 0x09, 0x2e, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x20, 0x2e, 0x75, 0x33, 0x32, 0x20, 0x76, + 0x61, 0x64, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x33, 0x0a, + 0x29, 0x0a, 0x7b, 0x0a, 0x09, 0x2e, 0x72, 0x65, 0x67, 0x20, 0x2e, 0x70, + 0x72, 0x65, 0x64, 0x20, 0x09, 0x25, 0x70, 0x3c, 0x32, 0x3e, 0x3b, 0x0a, + 0x09, 0x2e, 0x72, 0x65, 0x67, 0x20, 0x2e, 0x66, 0x33, 0x32, 0x20, 0x09, + 0x25, 0x66, 0x3c, 0x34, 0x3e, 0x3b, 0x0a, 0x09, 0x2e, 0x72, 0x65, 0x67, + 0x20, 0x2e, 0x62, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x3c, 0x36, 0x3e, + 0x3b, 0x0a, 0x09, 0x2e, 0x72, 0x65, 0x67, 0x20, 0x2e, 0x62, 0x36, 0x34, + 0x20, 0x09, 0x25, 0x72, 0x64, 0x3c, 0x31, 0x31, 0x3e, 0x3b, 0x0a, 0x0a, + 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x2e, 0x75, + 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x31, 0x2c, 0x20, 0x5b, 0x76, + 0x61, 0x64, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x30, 0x5d, + 0x3b, 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x2e, + 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x32, 0x2c, 0x20, 0x5b, + 0x76, 0x61, 0x64, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x31, + 0x5d, 0x3b, 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x2e, 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x33, 0x2c, 0x20, + 0x5b, 0x76, 0x61, 0x64, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, + 0x32, 0x5d, 0x3b, 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x2e, 0x75, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x32, 0x2c, 0x20, + 0x5b, 0x76, 0x61, 0x64, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, + 0x33, 0x5d, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, 0x75, 0x33, 0x32, + 0x20, 0x09, 0x25, 0x72, 0x33, 0x2c, 0x20, 0x25, 0x63, 0x74, 0x61, 0x69, + 0x64, 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, 0x75, 0x33, + 0x32, 0x20, 0x09, 0x25, 0x72, 0x34, 0x2c, 0x20, 0x25, 0x6e, 0x74, 0x69, + 0x64, 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, 0x75, 0x33, + 0x32, 0x20, 0x09, 0x25, 0x72, 0x35, 0x2c, 0x20, 0x25, 0x74, 0x69, 0x64, + 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x61, 0x64, 0x2e, 0x6c, 0x6f, 0x2e, + 0x73, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x31, 0x2c, 0x20, 0x25, 0x72, + 0x33, 0x2c, 0x20, 0x25, 0x72, 0x34, 0x2c, 0x20, 0x25, 0x72, 0x35, 0x3b, + 0x0a, 0x09, 0x73, 0x65, 0x74, 0x70, 0x2e, 0x67, 0x65, 0x2e, 0x73, 0x33, + 0x32, 0x20, 0x09, 0x25, 0x70, 0x31, 0x2c, 0x20, 0x25, 0x72, 0x31, 0x2c, + 0x20, 0x25, 0x72, 0x32, 0x3b, 0x0a, 0x09, 0x40, 0x25, 0x70, 0x31, 0x20, + 0x62, 0x72, 0x61, 0x20, 0x09, 0x24, 0x4c, 0x5f, 0x5f, 0x42, 0x42, 0x31, + 0x5f, 0x32, 0x3b, 0x0a, 0x0a, 0x09, 0x63, 0x76, 0x74, 0x61, 0x2e, 0x74, + 0x6f, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x75, 0x36, 0x34, + 0x20, 0x09, 0x25, 0x72, 0x64, 0x34, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x31, + 0x3b, 0x0a, 0x09, 0x6d, 0x75, 0x6c, 0x2e, 0x77, 0x69, 0x64, 0x65, 0x2e, + 0x73, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x64, 0x35, 0x2c, 0x20, 0x25, + 0x72, 0x31, 0x2c, 0x20, 0x34, 0x3b, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x2e, + 0x73, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x36, 0x2c, 0x20, 0x25, + 0x72, 0x64, 0x34, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x35, 0x3b, 0x0a, 0x09, + 0x63, 0x76, 0x74, 0x61, 0x2e, 0x74, 0x6f, 0x2e, 0x67, 0x6c, 0x6f, 0x62, + 0x61, 0x6c, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x37, + 0x2c, 0x20, 0x25, 0x72, 0x64, 0x32, 0x3b, 0x0a, 0x09, 0x61, 0x64, 0x64, + 0x2e, 0x73, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x38, 0x2c, 0x20, + 0x25, 0x72, 0x64, 0x37, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x35, 0x3b, 0x0a, + 0x09, 0x6c, 0x64, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x66, + 0x33, 0x32, 0x20, 0x09, 0x25, 0x66, 0x31, 0x2c, 0x20, 0x5b, 0x25, 0x72, + 0x64, 0x38, 0x5d, 0x3b, 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x67, 0x6c, 0x6f, + 0x62, 0x61, 0x6c, 0x2e, 0x66, 0x33, 0x32, 0x20, 0x09, 0x25, 0x66, 0x32, + 0x2c, 0x20, 0x5b, 0x25, 0x72, 0x64, 0x36, 0x5d, 0x3b, 0x0a, 0x09, 0x61, + 0x64, 0x64, 0x2e, 0x66, 0x33, 0x32, 0x20, 0x09, 0x25, 0x66, 0x33, 0x2c, + 0x20, 0x25, 0x66, 0x32, 0x2c, 0x20, 0x25, 0x66, 0x31, 0x3b, 0x0a, 0x09, + 0x63, 0x76, 0x74, 0x61, 0x2e, 0x74, 0x6f, 0x2e, 0x67, 0x6c, 0x6f, 0x62, + 0x61, 0x6c, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x39, + 0x2c, 0x20, 0x25, 0x72, 0x64, 0x33, 0x3b, 0x0a, 0x09, 0x61, 0x64, 0x64, + 0x2e, 0x73, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x31, 0x30, 0x2c, + 0x20, 0x25, 0x72, 0x64, 0x39, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x35, 0x3b, + 0x0a, 0x09, 0x73, 0x74, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, + 0x66, 0x33, 0x32, 0x20, 0x09, 0x5b, 0x25, 0x72, 0x64, 0x31, 0x30, 0x5d, + 0x2c, 0x20, 0x25, 0x66, 0x33, 0x3b, 0x0a, 0x0a, 0x24, 0x4c, 0x5f, 0x5f, + 0x42, 0x42, 0x31, 0x5f, 0x32, 0x3a, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x3b, + 0x0a, 0x0a, 0x7d, 0x0a, 0x09, 0x2f, 0x2f, 0x20, 0x2e, 0x67, 0x6c, 0x6f, + 0x62, 0x6c, 0x09, 0x76, 0x61, 0x64, 0x64, 0x5f, 0x6d, 0x75, 0x74, 0x61, + 0x74, 0x65, 0x64, 0x0a, 0x2e, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, + 0x20, 0x2e, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x76, 0x61, 0x64, 0x64, + 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x64, 0x28, 0x0a, 0x09, 0x2e, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x20, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x76, + 0x61, 0x64, 0x64, 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x30, 0x2c, 0x0a, 0x09, 0x2e, 0x70, + 0x61, 0x72, 0x61, 0x6d, 0x20, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x76, 0x61, + 0x64, 0x64, 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, + 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x31, 0x2c, 0x0a, 0x09, 0x2e, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x20, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x76, 0x61, 0x64, + 0x64, 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x5f, 0x32, 0x2c, 0x0a, 0x09, 0x2e, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x20, 0x2e, 0x75, 0x33, 0x32, 0x20, 0x76, 0x61, 0x64, 0x64, + 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x5f, 0x33, 0x0a, 0x29, 0x0a, 0x7b, 0x0a, 0x09, 0x2e, 0x72, + 0x65, 0x67, 0x20, 0x2e, 0x70, 0x72, 0x65, 0x64, 0x20, 0x09, 0x25, 0x70, + 0x3c, 0x32, 0x3e, 0x3b, 0x0a, 0x09, 0x2e, 0x72, 0x65, 0x67, 0x20, 0x2e, + 0x66, 0x33, 0x32, 0x20, 0x09, 0x25, 0x66, 0x3c, 0x35, 0x3e, 0x3b, 0x0a, + 0x09, 0x2e, 0x72, 0x65, 0x67, 0x20, 0x2e, 0x62, 0x33, 0x32, 0x20, 0x09, + 0x25, 0x72, 0x3c, 0x36, 0x3e, 0x3b, 0x0a, 0x09, 0x2e, 0x72, 0x65, 0x67, + 0x20, 0x2e, 0x62, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x3c, 0x31, + 0x31, 0x3e, 0x3b, 0x0a, 0x0a, 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, + 0x31, 0x2c, 0x20, 0x5b, 0x76, 0x61, 0x64, 0x64, 0x5f, 0x6d, 0x75, 0x74, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x30, + 0x5d, 0x3b, 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x2e, 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x32, 0x2c, 0x20, + 0x5b, 0x76, 0x61, 0x64, 0x64, 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x31, 0x5d, 0x3b, 0x0a, + 0x09, 0x6c, 0x64, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x2e, 0x75, 0x36, + 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x33, 0x2c, 0x20, 0x5b, 0x76, 0x61, + 0x64, 0x64, 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, + 0x61, 0x72, 0x61, 0x6d, 0x5f, 0x32, 0x5d, 0x3b, 0x0a, 0x09, 0x6c, 0x64, + 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x2e, 0x75, 0x33, 0x32, 0x20, 0x09, + 0x25, 0x72, 0x32, 0x2c, 0x20, 0x5b, 0x76, 0x61, 0x64, 0x64, 0x5f, 0x6d, + 0x75, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x5f, 0x33, 0x5d, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, 0x75, 0x33, + 0x32, 0x20, 0x09, 0x25, 0x72, 0x33, 0x2c, 0x20, 0x25, 0x63, 0x74, 0x61, + 0x69, 0x64, 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, 0x75, + 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x34, 0x2c, 0x20, 0x25, 0x6e, 0x74, + 0x69, 0x64, 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x6f, 0x76, 0x2e, 0x75, + 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x35, 0x2c, 0x20, 0x25, 0x74, 0x69, + 0x64, 0x2e, 0x78, 0x3b, 0x0a, 0x09, 0x6d, 0x61, 0x64, 0x2e, 0x6c, 0x6f, + 0x2e, 0x73, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x31, 0x2c, 0x20, 0x25, + 0x72, 0x33, 0x2c, 0x20, 0x25, 0x72, 0x34, 0x2c, 0x20, 0x25, 0x72, 0x35, + 0x3b, 0x0a, 0x09, 0x73, 0x65, 0x74, 0x70, 0x2e, 0x67, 0x65, 0x2e, 0x73, + 0x33, 0x32, 0x20, 0x09, 0x25, 0x70, 0x31, 0x2c, 0x20, 0x25, 0x72, 0x31, + 0x2c, 0x20, 0x25, 0x72, 0x32, 0x3b, 0x0a, 0x09, 0x40, 0x25, 0x70, 0x31, + 0x20, 0x62, 0x72, 0x61, 0x20, 0x09, 0x24, 0x4c, 0x5f, 0x5f, 0x42, 0x42, + 0x32, 0x5f, 0x32, 0x3b, 0x0a, 0x0a, 0x09, 0x63, 0x76, 0x74, 0x61, 0x2e, + 0x74, 0x6f, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x75, 0x36, + 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x34, 0x2c, 0x20, 0x25, 0x72, 0x64, + 0x31, 0x3b, 0x0a, 0x09, 0x6d, 0x75, 0x6c, 0x2e, 0x77, 0x69, 0x64, 0x65, + 0x2e, 0x73, 0x33, 0x32, 0x20, 0x09, 0x25, 0x72, 0x64, 0x35, 0x2c, 0x20, + 0x25, 0x72, 0x31, 0x2c, 0x20, 0x34, 0x3b, 0x0a, 0x09, 0x61, 0x64, 0x64, + 0x2e, 0x73, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x36, 0x2c, 0x20, + 0x25, 0x72, 0x64, 0x34, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x35, 0x3b, 0x0a, + 0x09, 0x63, 0x76, 0x74, 0x61, 0x2e, 0x74, 0x6f, 0x2e, 0x67, 0x6c, 0x6f, + 0x62, 0x61, 0x6c, 0x2e, 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, + 0x37, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x32, 0x3b, 0x0a, 0x09, 0x61, 0x64, + 0x64, 0x2e, 0x73, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x38, 0x2c, + 0x20, 0x25, 0x72, 0x64, 0x37, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x35, 0x3b, + 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, + 0x66, 0x33, 0x32, 0x20, 0x09, 0x25, 0x66, 0x31, 0x2c, 0x20, 0x5b, 0x25, + 0x72, 0x64, 0x38, 0x5d, 0x3b, 0x0a, 0x09, 0x6c, 0x64, 0x2e, 0x67, 0x6c, + 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x66, 0x33, 0x32, 0x20, 0x09, 0x25, 0x66, + 0x32, 0x2c, 0x20, 0x5b, 0x25, 0x72, 0x64, 0x36, 0x5d, 0x3b, 0x0a, 0x09, + 0x61, 0x64, 0x64, 0x2e, 0x66, 0x33, 0x32, 0x20, 0x09, 0x25, 0x66, 0x33, + 0x2c, 0x20, 0x25, 0x66, 0x32, 0x2c, 0x20, 0x25, 0x66, 0x31, 0x3b, 0x0a, + 0x09, 0x61, 0x64, 0x64, 0x2e, 0x66, 0x33, 0x32, 0x20, 0x09, 0x25, 0x66, + 0x34, 0x2c, 0x20, 0x25, 0x66, 0x33, 0x2c, 0x20, 0x30, 0x66, 0x33, 0x46, + 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3b, 0x0a, 0x09, 0x63, 0x76, 0x74, + 0x61, 0x2e, 0x74, 0x6f, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, + 0x75, 0x36, 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x39, 0x2c, 0x20, 0x25, + 0x72, 0x64, 0x33, 0x3b, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x2e, 0x73, 0x36, + 0x34, 0x20, 0x09, 0x25, 0x72, 0x64, 0x31, 0x30, 0x2c, 0x20, 0x25, 0x72, + 0x64, 0x39, 0x2c, 0x20, 0x25, 0x72, 0x64, 0x35, 0x3b, 0x0a, 0x09, 0x73, + 0x74, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x66, 0x33, 0x32, + 0x20, 0x09, 0x5b, 0x25, 0x72, 0x64, 0x31, 0x30, 0x5d, 0x2c, 0x20, 0x25, + 0x66, 0x34, 0x3b, 0x0a, 0x0a, 0x24, 0x4c, 0x5f, 0x5f, 0x42, 0x42, 0x32, + 0x5f, 0x32, 0x3a, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x3b, 0x0a, 0x0a, 0x7d, + 0x0a, 0x0a, + 0x00 +}; +static const unsigned int kernels_ptx_len = 2990; diff --git a/tools/ghidra/gsp-fwimage-570.144/scripts/Disasm.java b/tools/ghidra/gsp-fwimage-570.144/scripts/Disasm.java new file mode 100644 index 0000000..68a8a52 --- /dev/null +++ b/tools/ghidra/gsp-fwimage-570.144/scripts/Disasm.java @@ -0,0 +1,29 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.Listing; + +public class Disasm extends GhidraScript { + @Override + protected void run() throws Exception { + Listing listing = currentProgram.getListing(); + String[] args = getScriptArgs(); + for (String arg : args) { + String[] parts = arg.split(":"); + Address start = toAddr(parts[0]); + int count = parts.length > 1 ? Integer.decode(parts[1]) : 60; + println("### DISASM " + start + " count=" + count); + Address cur = start; + for (int i = 0; i < count; i++) { + Instruction insn = listing.getInstructionAt(cur); + if (insn == null) { + try { disassemble(cur); } catch (Exception e) { println(" disasm stop: " + e); break; } + insn = listing.getInstructionAt(cur); + } + if (insn == null) { println(" no instruction at " + cur); break; } + println(" " + insn.getAddress() + ": " + insn.toString()); + cur = insn.getMaxAddress().add(1); + } + } + } +} diff --git a/tools/ghidra/gsp-fwimage-570.144/scripts/DumpFuncs.java b/tools/ghidra/gsp-fwimage-570.144/scripts/DumpFuncs.java new file mode 100644 index 0000000..6948492 --- /dev/null +++ b/tools/ghidra/gsp-fwimage-570.144/scripts/DumpFuncs.java @@ -0,0 +1,35 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionManager; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; +import ghidra.util.task.TaskMonitor; + +public class DumpFuncs extends GhidraScript { + @Override + protected void run() throws Exception { + String[] args = getScriptArgs(); + FunctionManager fm = currentProgram.getFunctionManager(); + DecompInterface decompiler = new DecompInterface(); + decompiler.openProgram(currentProgram); + for (String arg : args) { + String s = arg.trim(); + Address addr = toAddr(s); + Function func = fm.getFunctionContaining(addr); + if (func == null) { + func = createFunction(addr, null); + } + println("### FUNC " + s + " -> " + (func != null ? func.getName() + " @ " + func.getEntryPoint() : "NOT FOUND")); + if (func != null) { + DecompileResults res = decompiler.decompileFunction(func, 120, monitor); + if (res != null && res.decompileCompleted()) { + println(res.getDecompiledFunction().getC()); + } else { + println("// decompile failed: " + (res != null ? res.getErrorMessage() : "null")); + } + } + } + decompiler.dispose(); + } +} diff --git a/tools/ghidra/gsp-fwimage-570.144/scripts/MemCheck.java b/tools/ghidra/gsp-fwimage-570.144/scripts/MemCheck.java new file mode 100644 index 0000000..6cb573e --- /dev/null +++ b/tools/ghidra/gsp-fwimage-570.144/scripts/MemCheck.java @@ -0,0 +1,26 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.mem.Memory; + +public class MemCheck extends GhidraScript { + @Override + protected void run() throws Exception { + String[] args = getScriptArgs(); + Memory mem = currentProgram.getMemory(); + println("min=" + mem.getMinAddress() + " max=" + mem.getMaxAddress()); + for (String arg : args) { + Address a = toAddr(arg.trim()); + boolean c = mem.contains(a); + println(arg + " contains=" + c); + if (c) { + byte[] b = new byte[16]; + int n = mem.getBytes(a, b); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) sb.append(String.format("%02x ", b[i])); + println(" bytes: " + sb); + } + } + } +} diff --git a/tools/ghidra/gsp-fwimage-570.144/scripts/ScanBranches.java b/tools/ghidra/gsp-fwimage-570.144/scripts/ScanBranches.java new file mode 100644 index 0000000..cff36f0 --- /dev/null +++ b/tools/ghidra/gsp-fwimage-570.144/scripts/ScanBranches.java @@ -0,0 +1,34 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.symbol.FlowType; + +public class ScanBranches extends GhidraScript { + @Override + protected void run() throws Exception { + Listing listing = currentProgram.getListing(); + String[] args = getScriptArgs(); + Address start = toAddr(args[0]); + Address end = toAddr(args[1]); + Address target = toAddr(args[2]); + Address cur = start; + int n = 0; + while (cur.compareTo(end) < 0 && n < 20000) { + Instruction insn = listing.getInstructionAt(cur); + if (insn == null) { + try { disassemble(cur); } catch (Exception e) { println("stop at " + cur + ": " + e); break; } + insn = listing.getInstructionAt(cur); + } + if (insn == null) { println("no insn at " + cur); break; } + for (Address a : insn.getFlows()) { + if (a.equals(target)) { + println("TARGET HIT: " + insn.getAddress() + ": " + insn.toString()); + } + } + cur = insn.getMaxAddress().add(1); + n++; + } + println("scanned " + n + " instructions from " + start + " to " + cur); + } +} diff --git a/tools/ghidra/gsp-fwimage-570.144/scripts/Xrefs.java b/tools/ghidra/gsp-fwimage-570.144/scripts/Xrefs.java new file mode 100644 index 0000000..65d4b7f --- /dev/null +++ b/tools/ghidra/gsp-fwimage-570.144/scripts/Xrefs.java @@ -0,0 +1,30 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionManager; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceIterator; +import ghidra.program.model.symbol.ReferenceManager; + +public class Xrefs extends GhidraScript { + @Override + protected void run() throws Exception { + String[] args = getScriptArgs(); + ReferenceManager rm = currentProgram.getReferenceManager(); + FunctionManager fm = currentProgram.getFunctionManager(); + for (String arg : args) { + Address target = toAddr(arg.trim()); + println("### XREFS to " + target); + ReferenceIterator it = rm.getReferencesTo(target); + int n = 0; + while (it.hasNext()) { + Reference r = it.next(); + Address from = r.getFromAddress(); + Function f = fm.getFunctionContaining(from); + println(" from " + from + " (" + (f != null ? f.getName() + "@" + f.getEntryPoint() : "?") + ") type=" + r.getReferenceType()); + n++; + } + println(" total=" + n); + } + } +} diff --git a/tools/guest-diag/guest_pin_stream.py b/tools/guest-diag/guest_pin_stream.py index 3b7d9b1..f89ed4a 100644 --- a/tools/guest-diag/guest_pin_stream.py +++ b/tools/guest-diag/guest_pin_stream.py @@ -40,6 +40,12 @@ SPAN = int(os.environ.get("STREAM_SPAN", "0x80000"), 16) LO2 = int(os.environ.get("STREAM_LO2", "0x120000000"), 16) HI2 = int(os.environ.get("STREAM_HI2", "0x121400000"), 16) SPAN2 = int(os.environ.get("STREAM_SPAN2", "0x180000"), 16) +# UVM semaphore pool window: the tracker pool word lives at +# 0x121000000 + 4*idx, above the CE window head span (LO2+SPAN2 = +# 0x120180000), so scan_window never reaches it. Scan every 4 KB page of +# [POOL_LO, POOL_HI) directly - cheap (16 pages) and host-visible as pins. +POOL_LO = int(os.environ.get("STREAM_POOL_LO", "0x121000000"), 16) +POOL_HI = int(os.environ.get("STREAM_POOL_HI", "0x121010000"), 16) # UVM system-memory AVMAs: the client's sysmem buffers (the DtoH staging # buffer among them) are /dev/zero mappings at high GPU VAs (e.g. # 0x79c21c800000). They are not inside the two fixed windows, and only the @@ -128,7 +134,7 @@ def new_sems(seen): def cdt_pid(): prefixes = tuple(p for p in os.environ.get("CLIENT_PREFIXES", - "cdt,va").split(",") if p) + "cdt,va,victory").split(",") if p) for d in os.listdir('/proc'): if not d.isdigit(): continue @@ -167,6 +173,35 @@ def scan_window(fd, lo, hi, span, out): base += REGION +def scan_range(fd, lo, hi, out): + """Present pages of one explicit VA range, every 4 KB page. + + Same pagemap present/PFN/PAMAX filtering as scan_window, but with no + REGION alignment or head-span limit: walks [lo, hi) directly. Used for + the UVM semaphore pool window, which sits past the CE head span. + """ + va = lo & ~0xFFF + while va < hi: + n = min(BATCH, (hi - va) // 0x1000) + if n <= 0: + break + os.lseek(fd, (va // 4096) * 8, 0) + data = os.read(fd, n * 8) + got = len(data) // 8 + if got <= 0: + break + for i in range(got): + e = int.from_bytes(data[i * 8:i * 8 + 8], 'little') + if not ((e >> 63) & 1): + continue + pfn = e & ((1 << 55) - 1) + pa = pfn << 12 + if not pa or (PAMAX and pa >= PAMAX): + continue + out[va + i * 0x1000] = pa + va += got * 0x1000 + + def scan_vmas(fd, pid, out): """Present pages of the client's high-VA nvidia//dev/zero mappings. @@ -237,6 +272,8 @@ def scan(pid): scan_window(fd, LO, HI, SPAN, out) if HI2 > LO2: scan_window(fd, LO2, HI2, SPAN2, out) + if POOL_HI > POOL_LO: + scan_range(fd, POOL_LO, POOL_HI, out) if VMA_EVERY and scan.loop % VMA_EVERY == 0: scan_vmas(fd, pid, out) finally: @@ -304,7 +341,7 @@ def main(): if cpa and (not PAMAX or cpa < PAMAX): cur[0x20400f000] = cpa new = [(va, pa) for va, pa in cur.items() if seen.get(va) != pa] - seen = cur + pending_cur = cur lines = [] for ctl in tracer.drain(): if ctl.startswith("#sem"): @@ -321,6 +358,7 @@ def main(): except OSError: pass if not new and not lines: + seen = pending_cur time.sleep(INTERVAL) continue if new: @@ -350,6 +388,7 @@ def main(): continue try: sock.sendall(payload) + seen = pending_cur except OSError: sock.close() sock = None diff --git a/tools/guest-diag/pin_gr_stream.py b/tools/guest-diag/pin_gr_stream.py index d8b6b61..7f8e4b8 100644 --- a/tools/guest-diag/pin_gr_stream.py +++ b/tools/guest-diag/pin_gr_stream.py @@ -58,6 +58,8 @@ def _start_cmd(): with open(GUEST_SCRIPT) as f: b64 = subprocess.run(["base64", "-w0"], stdin=f, 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); " @@ -65,10 +67,12 @@ def _start_cmd(): "/proc/$p/cmdline 2>/dev/null && sudo kill $p 2>/dev/null; " "fi; " "sudo rm -f /tmp/guest_pin_stream.pid; " - "sudo setsid nohup python3 /tmp/guest_pin_stream.py %d %d %s %s %s " + "sudo setsid nohup env CLIENT_PREFIXES='%s' " + "STREAM_VMA_EVERY='%s' " + "python3 /tmp/guest_pin_stream.py %d %d %s %s %s " ">/tmp/guest_pin_stream.log 2>&1 & sleep 2; " "sudo cat /tmp/guest_pin_stream.pid 2>/dev/null" - % (b64, 2, PORT, LO, HI, PAMAX)) + % (b64, prefixes, vma_every, 2, PORT, LO, HI, PAMAX)) def install_and_start(): @@ -189,18 +193,22 @@ def _flush(runs, seen): "echo 'FAIL 0x%x 0x%x 0x%x'" % (va, pa, n * 0x1000, PARAM, va, pa, n * 0x1000)) failed = set() - try: - r = subprocess.run(["sudo", "-n", "sh", "-c", "\n".join(script)], - capture_output=True, text=True, timeout=60) - for line in r.stdout.splitlines(): - m = re.match(r"FAIL (0x[0-9a-f]+) (0x[0-9a-f]+) (0x[0-9a-f]+)", - line.strip()) - if m: - failed.add((int(m.group(1), 16), int(m.group(2), 16), - int(m.group(3), 16) // 0x1000)) - except Exception as e: - log("batch inject failed: %s" % e) - failed = set(todo) + CHUNK = 200 + for start in range(0, len(script), CHUNK): + batch = script[start:start + CHUNK] + try: + r = subprocess.run(["sudo", "-n", "sh", "-c", "\n".join(batch)], + capture_output=True, text=True, timeout=60) + for line in r.stdout.splitlines(): + m = re.match(r"FAIL (0x[0-9a-f]+) (0x[0-9a-f]+) (0x[0-9a-f]+)", + line.strip()) + if m: + failed.add((int(m.group(1), 16), int(m.group(2), 16), + int(m.group(3), 16) // 0x1000)) + except Exception as e: + log("batch inject failed: %s" % e) + for va, pa, n in todo[start:start + CHUNK]: + failed.add((va, pa, n)) pages = 0 for va, pa, n in todo: |
