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 /debug/risc-v-gsp-rm-vm | |
| 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
Diffstat (limited to 'debug/risc-v-gsp-rm-vm')
67 files changed, 13564 insertions, 25 deletions
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() |
