summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/tests/test_sim_cycles.py
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/tests/test_sim_cycles.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/tests/test_sim_cycles.py299
1 files changed, 299 insertions, 0 deletions
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()