diff options
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.py | 299 |
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() |
