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 | cc79b478843f0230fe6aa8ecd822de7ba71c891e (patch) | |
| tree | 5de760ddec4e45cc23d39ce7502635f5c0711217 /debug/risc-v-gsp-rm-vm/plugins | |
| parent | 40351167dd209012ab485e6164ce94f0b8965422 (diff) | |
| download | open-iov-cc79b478843f0230fe6aa8ecd822de7ba71c891e.tar.gz open-iov-cc79b478843f0230fe6aa8ecd822de7ba71c891e.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/plugins')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/plugins/mcp_server.py | 434 |
1 files changed, 433 insertions, 1 deletions
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 # --------------------------------------------------------------------------- |
