diff --git a/api/steps/bebop/p2e/02_buildbitstream_event.step.py b/api/steps/bebop/p2e/02_buildbitstream_event.step.py index fc953a9..4c99433 100644 --- a/api/steps/bebop/p2e/02_buildbitstream_event.step.py +++ b/api/steps/bebop/p2e/02_buildbitstream_event.step.py @@ -59,7 +59,8 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: os.makedirs(build_dir, exist_ok=True) build_cmd = ( - f"nix develop --ignore-environment --keep HOME --keep ALL_PROXY -c " + f"nix develop --ignore-env --keep-env-var HOME --keep-env-var ALL_PROXY " + f"--keep-env-var CARGO_TARGET_DIR -c " f"cargo run --release --features p2e -- build p2e " f"--rtl-dir=\"{vsrc_dir}\" " f"--out-dir=\"{build_dir}\"" diff --git a/api/steps/bebop/p2e/03_runworkload_api.step.py b/api/steps/bebop/p2e/03_runworkload_api.step.py index 88dace3..6fe1e14 100644 --- a/api/steps/bebop/p2e/03_runworkload_api.step.py +++ b/api/steps/bebop/p2e/03_runworkload_api.step.py @@ -23,6 +23,16 @@ async def handler(request: ApiRequest, ctx: FlowContext) -> ApiResponse: "message": "image and bitstream parameters are required", }, ) + if body.get("diff", False) and not body.get("golden-elf"): + return ApiResponse( + status=400, + body={ + "success": False, + "failure": True, + "returncode": 400, + "message": "--diff requires --golden-elf ", + }, + ) await ctx.enqueue({ "topic": "bebop.p2e.runworkload", diff --git a/api/steps/bebop/p2e/03_runworkload_event.step.py b/api/steps/bebop/p2e/03_runworkload_event.step.py index 7f0a9c0..a181d21 100644 --- a/api/steps/bebop/p2e/03_runworkload_event.step.py +++ b/api/steps/bebop/p2e/03_runworkload_event.step.py @@ -9,8 +9,10 @@ import glob import os import re +import shlex import sys from datetime import datetime +from pathlib import Path from motia import FlowContext, queue @@ -51,6 +53,41 @@ def case_uses_multi_fpga(build_dir: str) -> bool: return len([path for path in part_dirs if os.path.isdir(path)]) > 1 +def runtime_build_command(bbdir: str, chip: str, diff: bool, vsrc_dir: str, build_dir: str) -> tuple[str, str]: + manifest = Path(bbdir) / "bebop" / "Cargo.toml" + features = ["p2e"] + if diff: + manifest = Path(bbdir) / "examples" / "chips" / chip / "generated" / "bebop" / "Cargo.toml" + features.extend(["bemu", "difftest"]) + command = shlex.join( + [ + "env", + "BEBOP_P2E_RUNTIME_ONLY=1", + "BEBOP_P2E_REBUILD_RUNTIME=1", + f"VSRC_PATH={vsrc_dir}", + f"OUT_PATH={build_dir}", + "cargo", + "run", + "--release", + "--manifest-path", + str(manifest), + "--bin", + "bebop", + "--features", + ",".join(features), + "--", + "build", + "p2e", + "--rtl-dir", + vsrc_dir, + "--out-dir", + build_dir, + *(["--diff"] if diff else []), + ] + ) + return command, str(manifest.parent) + + async def handler(input_data: dict, ctx: FlowContext) -> None: origin_tid = get_origin_trace_id(input_data, ctx) try: @@ -70,6 +107,27 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: bitstream = input_data.get("bitstream", "") multi_fpga = bool(input_data.get("multi-fpga", False)) wave = bool(input_data.get("wave", False)) + diff = bool(input_data.get("diff", False)) + golden_elf = input_data.get("golden-elf", "") + golden_pk = bool(input_data.get("golden-pk", False)) + if diff and not golden_elf: + ctx.logger.error("--diff requires --golden-elf ") + await check_result( + ctx, 1, continue_run=False, + extra_fields={"error": "missing_golden_elf"}, + trace_id=origin_tid, + ) + return + if diff and not os.path.isfile(golden_elf): + ctx.logger.error(f"BEMU golden ELF not found: {golden_elf}") + await check_result( + ctx, 1, continue_run=False, + extra_fields={"error": "golden_elf_not_found", "golden_elf": golden_elf}, + trace_id=origin_tid, + ) + return + if diff: + golden_elf = os.path.abspath(golden_elf) if "wave_start" in input_data: ctx.logger.error("invalid parameter: --wave_start (use --wave-start)") await check_result( @@ -134,27 +192,42 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: # Rebuild the VVAC host runtime in the bitstream case. The bitstream is # deliberately left in place, so runtime/DPIC changes never trigger FPGA synthesis. - runtime_cmd = ( - f"env BEBOP_P2E_RUNTIME_ONLY=1 BEBOP_P2E_REBUILD_RUNTIME=1 " - f"cargo run --release --features p2e -- build p2e " - f"--rtl-dir=\"{vsrc_dir}\" " - f"--out-dir=\"{build_dir}\"" - ) + runtime_cmd, runtime_cwd = runtime_build_command(bbdir, chip, diff, vsrc_dir, build_dir) + runtime_env = {**os.environ.copy(), **bebop_cargo_env(bbdir, chip)} + if diff: + hpec_home = "/home/x-epic/hpe-24.12.01.s008" + runtime_env["BEBOP_BEMU_P2E_ABI"] = "1" + runtime_env["BEBOP_BEMU_CC"] = os.path.join( + hpec_home, "tools", "gcc-8.3.0", "gcc-8.3.0", "bin", "gcc" + ) + runtime_env["BEBOP_BEMU_CXX"] = os.path.join( + hpec_home, "tools", "gcc-8.3.0", "gcc-8.3.0", "bin", "g++" + ) + runtime_env["BEBOP_BEMU_DTC"] = os.path.join(bbdir, "result", "bin", "dtc") + runtime_env["CARGO_TARGET_DIR"] = os.path.join(bebop_dir, "target", f"{chip}-p2e-diff") + compiler_libs = [ + os.path.join(hpec_home, "tools", "gcc-8.3.0", "gmp-6.2.1", "lib"), + os.path.join(hpec_home, "tools", "gcc-8.3.0", "mpfr-4.1.0", "lib"), + os.path.join(hpec_home, "tools", "gcc-8.3.0", "mpc-1.2.1", "lib"), + ] + runtime_env["BEBOP_BEMU_COMPILER_LIBRARY_PATH"] = ":".join(compiler_libs) ctx.logger.info("Preparing bebop p2e runtime for the selected bitstream ...") runtime_result = await stream_run_logger_async( cmd=runtime_cmd, logger=ctx.logger, - cwd=bebop_dir, + cwd=runtime_cwd, stdout_prefix="bebop p2e runtime", stderr_prefix="bebop p2e runtime", - env={**os.environ.copy(), **bebop_cargo_env(bbdir, chip)}, + env=runtime_env, ) runtime_lib_dir = os.path.join(build_dir, "vvacDir", "runtimeDir", "lib", "lib_arm") rtcfg_path = os.path.join(build_dir, "vvacDir", "runtimeDir", "rtcfg") libvctb_path = os.path.join(runtime_lib_dir, "libvCtb.so") libstdcxx_path = os.path.join(runtime_lib_dir, "libstdc++.so.6") bebop_p2e_path = os.path.join(build_dir, "bebop-p2e") - runtime_artifacts = (rtcfg_path, libvctb_path, libstdcxx_path, bebop_p2e_path) + runtime_artifacts = [rtcfg_path, libvctb_path, libstdcxx_path, bebop_p2e_path] + if diff: + runtime_artifacts.append(os.path.join(build_dir, "libriscv.so")) if runtime_result.returncode != 0 or not all(os.path.isfile(path) for path in runtime_artifacts): missing = [path for path in runtime_artifacts if not os.path.isfile(path)] if missing: @@ -189,6 +262,10 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: run_cmd += " --wave" if wave_start is not None: run_cmd += f" --wave-start=\"{wave_start}\"" + if diff: + run_cmd += f" --diff --golden-elf={shlex.quote(golden_elf)}" + if golden_pk: + run_cmd += " --golden-pk" for trace_name in ("itrace", "mtrace", "pmctrace", "ctrace", "banktrace"): if input_data.get(trace_name, False): run_cmd += f" --{trace_name}" @@ -221,6 +298,8 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: "log_dir": run_log, "bdb_trace": os.path.join(run_log, "bdb.ndjson"), "uart_log": os.path.join(run_log, "uart.log"), + "bank_diff": os.path.join(run_log, "bank_diff.ndjson") if diff else None, + "diff": diff, "timestamp": timestamp, }, trace_id=origin_tid, diff --git a/api/steps/config/01_install_event.step.py b/api/steps/config/01_install_event.step.py index b752275..0012a32 100644 --- a/api/steps/config/01_install_event.step.py +++ b/api/steps/config/01_install_event.step.py @@ -112,6 +112,9 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: bemu = step4.install_bemu(chip, bbdir, gen) ctx.logger.info(f"Installed bemu crate: {bemu}") + bebop = step4.install_bebop(gen) + ctx.logger.info(f"Installed bebop crate: {bebop}") + # C. TO Workload BUILD SYSTEM workload = step4.install_workload(chip, bbdir, chip_name, gen) ctx.logger.info(f"Installed workload defs: {workload}") @@ -136,6 +139,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: "chip": chip_name, "arch": str(arch), "bemu": str(bemu), + "bebop": str(bebop), "workload": str(workload), } ) diff --git a/api/steps/config/scripts/4_install.py b/api/steps/config/scripts/4_install.py index 291fc77..2152182 100644 --- a/api/steps/config/scripts/4_install.py +++ b/api/steps/config/scripts/4_install.py @@ -125,6 +125,59 @@ def install_bemu(chip: pb.Chip, bbdir: Path, gen: Path) -> Path: return bemu / "Cargo.toml" +def install_bebop(gen: Path) -> Path: + bebop = gen.parent.parent / "generated" / "bebop" + bebop.mkdir(parents=True, exist_ok=True) + manifest = bebop / "Cargo.toml" + manifest.write_text( + """[workspace] +resolver = "2" + +[package] +name = "bebop" +version = "0.1.0" +edition = "2021" +build = "../../../../../bebop/build.rs" + +[[bin]] +name = "bebop" +path = "../../../../../bebop/src/main.rs" + +[features] +default = [] +verilator = ["dep:bebop-verilator"] +p2e = ["dep:bebop-p2e"] +bemu = ["dep:bebop-bemu"] +difftest = ["bebop-bemu/difftest"] + +[dependencies] +bebop-verilator = { path = "../../../../../bebop/src/nodes/verilator", optional = true } +bebop-p2e = { path = "../../../../../bebop/src/nodes/p2e", optional = true } +bebop-bemu = { path = "../../configs/generated/bemu", optional = true } +bebop-dasm = { path = "../../../../../bebop/src/nodes/lib/dasm" } +bebop-bank-hash = { path = "../../../../../bebop/src/nodes/lib/bank-hash" } +bebop-bemu-profile = { path = "../../../../../bebop/src/nodes/lib/bemu-profile" } +bebop-fd-redirect = { path = "../../../../../bebop/src/nodes/lib/fd-redirect" } +bebop-rtl-trace = { path = "../../../../../bebop/src/nodes/lib/rtl-trace" } +bebop-uart = { path = "../../../../../bebop/src/nodes/lib/uart" } +clap = { version = "4", features = ["derive"] } +libc = "0.2" +log = "0.4" +env_logger = "0.11" +nix = { version = "0.29", features = ["fs", "mman", "signal", "process"] } +toml = "0.8" +ctrlc = "3" +camino = "1.1" +snafu = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +duct = "0.13" +""", + encoding="utf-8", + ) + return manifest + + def install_workload(chip: pb.Chip, bbdir: Path, name: str, gen: Path) -> Path: if not chip.profiles: raise ValueError(f"chip {name}: no compiler profiles") diff --git a/api/steps/mill/scripts/mill.py b/api/steps/mill/scripts/mill.py index 4fe2123..6f2f858 100644 --- a/api/steps/mill/scripts/mill.py +++ b/api/steps/mill/scripts/mill.py @@ -12,7 +12,7 @@ def elaborate_cmd(main: str, config: str, out: str, *, seq_mem: bool = False) -> f" --repl-seq-mem --repl-seq-mem-file={shlex.quote(os.path.join(out, 'mems.conf'))}" ) return ( - f"mill -i __.test.runMain {main} {config} " + f"flock /tmp/buckyball-mill.lock mill -i __.test.runMain {main} {config} " "--disable-annotation-unknown --strip-debug-info -O=debug " f"--split-verilog -o={shlex.quote(out)}{extra}" ) diff --git a/api/steps/uvm/scripts/uvm_common.py b/api/steps/uvm/scripts/uvm_common.py index e40ee9a..d6baddf 100644 --- a/api/steps/uvm/scripts/uvm_common.py +++ b/api/steps/uvm/scripts/uvm_common.py @@ -1,4 +1,5 @@ import os +import re import shlex import sys import tomllib @@ -12,22 +13,16 @@ from utils.path import get_buckyball_path, log_dir from utils.stream_run import stream_run_logger +config_scripts = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "config", "scripts") +) +sys.path.insert(0, config_scripts) + def load_chip(bbdir: str, chip: str): - config_scripts = os.path.abspath( - os.path.join(os.path.dirname(__file__), "..", "..", "config", "scripts") - ) - if config_scripts not in sys.path: - sys.path.insert(0, config_scripts) - try: - import chip_pb2 - except ImportError as e: - raise FileNotFoundError( - f"missing {os.path.join(config_scripts, 'chip_pb2.py')}; run bbdev config --install" - ) from e + import chip_pb2 + path = Path(bbdir) / "examples" / "chips" / chip / "configs" / "generated" / "chip.pb" - if not path.is_file(): - raise FileNotFoundError(f"missing {path}; run bbdev config --install") msg = chip_pb2.Chip() msg.ParseFromString(path.read_bytes()) if not msg.name or not msg.cores: @@ -36,15 +31,7 @@ def load_chip(bbdir: str, chip: str): def ball_domain(chip): - d0 = chip.cores[0].balldomain - key = [(m.ball_id, m.ball_dir, m.in_bw, m.out_bw) for m in d0.mappings] - isa = [(e.mnemonic, e.funct7, e.bid) for e in d0.isa] - for core in chip.cores[1:]: - k = [(m.ball_id, m.ball_dir, m.in_bw, m.out_bw) for m in core.balldomain.mappings] - i = [(e.mnemonic, e.funct7, e.bid) for e in core.balldomain.isa] - if k != key or i != isa: - raise ValueError("chip.pb cores have different balldomains") - return d0 + return chip.cores[0].balldomain def selected_mappings(domain, ball: str | None): @@ -70,13 +57,23 @@ def vcs_defines(domain, mapping, bank_entries: int): return defs +def smatmul_accumulator_filename(rtl_dir: Path) -> str: + text = (rtl_dir / "SMatMulUnit.sv").read_text() + modules = re.findall(r"\b(accumulator_\d+x\d+)\s+accumulator_ext\s*\(", text) + if len(modules) != 1: + raise ValueError(f"expected one SMatMul accumulator instance, found {modules!r}") + return f"{modules[0]}.sv" + + def _filelist( - verify_dir: Path, ball_dir: str, uvm_rel: str, rtl_rel: str, sim_dir: Path + verify_dir: Path, ball_dir: str, uvm_rel: str, rtl_rel: str, rtl_dir: Path, sim_dir: Path ) -> str: src = verify_dir / "filelists" / f"{ball_dir}_ball.f" dst = sim_dir / f"{ball_dir}_ball.f" sim_dir.mkdir(parents=True, exist_ok=True) text = src.read_text() + if "@SMATMUL_ACCUMULATOR@" in text: + text = text.replace("@SMATMUL_ACCUMULATOR@", smatmul_accumulator_filename(rtl_dir)) dst.write_text(text.replace("@UVM@", uvm_rel).replace("@RTL@", rtl_rel)) return str(dst.relative_to(verify_dir)) @@ -90,7 +87,7 @@ def build_ball(bbdir: str, chip_name: str, mill_cfg: str, domain, mapping, ctx) sim_dir = verify_dir / "build" / chip_name uvm_rel = os.path.relpath(Path(bbdir) / "verify" / "uvm", verify_dir) rtl_rel = os.path.relpath(rtl_dir, verify_dir) - flist = _filelist(verify_dir, ball, uvm_rel, rtl_rel, sim_dir) + flist = _filelist(verify_dir, ball, uvm_rel, rtl_rel, rtl_dir, sim_dir) cargo = ( f"nix develop {shlex.quote(str(Path(bbdir) / 'verify'))} --command " f"cargo build --manifest-path {shlex.quote(str(casegen))}" @@ -180,7 +177,13 @@ def run_chip(bbdir: str, chip: str, ball: str | None, ctx, do_run: bool) -> dict msg = load_chip(bbdir, chip) domain = ball_domain(msg) mill_cfg = msg.mill.verilator_config - maps = selected_mappings(domain, ball) + maps = [ + m + for m in selected_mappings(domain, ball) + if (Path(bbdir) / "examples" / "balls" / m.ball_dir / "verify" / "filelists" / f"{m.ball_dir}_ball.f").is_file() + ] + if ball is not None and not maps: + raise ValueError(f"UVM filelist missing for ball {ball!r}") ran = [] covs = [] run_root = None diff --git a/api/tests/test_bebop_p2e_diff.py b/api/tests/test_bebop_p2e_diff.py new file mode 100644 index 0000000..084f53a --- /dev/null +++ b/api/tests/test_bebop_p2e_diff.py @@ -0,0 +1,64 @@ +import importlib.util +import runpy +import sys +import types +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[3] +API = ROOT / "bbdev" / "api" +sys.path.insert(0, str(API)) + +motia = types.ModuleType("motia") +motia.FlowContext = object +motia.queue = lambda name: name +sys.modules["motia"] = motia + +event_path = API / "steps" / "bebop" / "p2e" / "03_runworkload_event.step.py" +spec = importlib.util.spec_from_file_location("bebop_p2e_runworkload_event", event_path) +event = importlib.util.module_from_spec(spec) +spec.loader.exec_module(event) + + +class BebopP2eDiffTest(unittest.TestCase): + def test_bbdev_parses_diff_options(self): + cli = runpy.run_path(str(ROOT / "bbdev" / "bbdev")) + args = cli["parse_args"]([ + "bebop-p2e", + "--runworkload", + "--chip pebble --image fw_payload-lenet --bitstream image.bit " + "--diff --golden-elf golden.elf --golden-pk", + ]) + parsed = cli["extract_command_info"](args)["args"] + self.assertEqual(parsed["golden-elf"], "golden.elf") + self.assertTrue(parsed["diff"]) + self.assertTrue(parsed["golden-pk"]) + + def test_diff_runtime_build_enables_bemu_and_difftest(self): + command, cwd = event.runtime_build_command("/repo", "pebble", True, "/rtl", "/case") + self.assertIn("--features p2e,bemu,difftest", command) + self.assertIn("/repo/examples/chips/pebble/generated/bebop/Cargo.toml", command) + self.assertTrue(command.endswith("--diff")) + self.assertEqual(cwd, "/repo/examples/chips/pebble/generated/bebop") + + def test_normal_runtime_build_only_enables_p2e(self): + cli = runpy.run_path(str(ROOT / "bbdev" / "bbdev")) + args = cli["parse_args"]([ + "bebop-p2e", + "--runworkload", + "--chip pebble --image fw_payload-lenet --bitstream image.bit --diff=false", + ]) + parsed = cli["extract_command_info"](args)["args"] + self.assertFalse(parsed["diff"]) + + command, cwd = event.runtime_build_command("/repo", "pebble", False, "/rtl", "/case") + self.assertIn("--features p2e", command) + self.assertNotIn("bemu", command) + self.assertNotIn("difftest", command) + self.assertFalse(command.endswith("--diff")) + self.assertEqual(cwd, "/repo/bebop") + + +if __name__ == "__main__": + unittest.main() diff --git a/api/tests/test_uvm_chip_pb.py b/api/tests/test_uvm_chip_pb.py index 6aa2cb7..8419377 100644 --- a/api/tests/test_uvm_chip_pb.py +++ b/api/tests/test_uvm_chip_pb.py @@ -28,12 +28,13 @@ def test_defines(): c = _chip() d = ball_domain(c) m = selected_mappings(d, "lut")[0] - defs = vcs_defines(d, m) + defs = vcs_defines(d, m, 1024) assert defs == [ "+define+BB_IN_BW=2", "+define+BB_OUT_BW=1", "+define+BB_MMIO_READ_BW=0", "+define+BB_MMIO_WRITE_BW=0", + "+define+BB_BANK_ADDR_W=10", "+define+LUT_FUNCT7=66", ] @@ -47,7 +48,15 @@ def test_unknown_ball(): raise AssertionError("expected ValueError") +def test_heterogeneous_chip_uses_first_domain(): + c = _chip() + other = c.cores.add() + other.balldomain.mappings.add().ball_dir = "gemmini" + assert [m.ball_dir for m in ball_domain(c).mappings] == ["lut"] + + if __name__ == "__main__": test_defines() test_unknown_ball() + test_heterogeneous_chip_uses_first_domain() print("ok") diff --git a/bbdev b/bbdev index 1319b37..264d097 100755 --- a/bbdev +++ b/bbdev @@ -85,7 +85,7 @@ WORKFLOW_COMMANDS = { "clean": 'Clean Bebop P2E build directory. Args: "--chip [--output-dir ]"', "verilog": 'Generate Bebop P2E SystemVerilog for DDR4 backdoor. Args: "--chip [--output-dir ]"', "buildbitstream": 'Build Bebop P2E runtime case. Args: "--chip [--vsrc-dir ] [--output-dir ]"', - "runworkload": 'Run Bebop P2E workload. Args: "--chip --image --bitstream [--multi-fpga] [--wave] [--wave-start ] [--itrace] [--mtrace] [--pmctrace]"', + "runworkload": 'Run Bebop P2E workload. Args: "--chip --image --bitstream [--multi-fpga] [--wave] [--wave-start ] [--diff --golden-elf [--golden-pk]] [--itrace] [--mtrace] [--pmctrace]"', "batch": 'Run Bebop p2e nextest batch regression (serial). Args: "--chip --bitstream --test "', }, "dc": { @@ -332,6 +332,8 @@ def _parse_sub_args(arg_string: str) -> dict: key, value = token[2:].split("=", 1) if not key or value == "": raise ValueError(f"Invalid argument: {token}") + if value in ("true", "false"): + value = value == "true" args_dict[key] = value i += 1 else: @@ -612,6 +614,7 @@ def run_script_mode(args, cmd_info): # Serialize only engine startup. The reservation socket must be released before # iii can bind it, so without this lock another bbdev can select the same port # during that handoff. + os.makedirs(os.path.dirname(SERVER_START_LOCK), exist_ok=True) server_start_lock = open(SERVER_START_LOCK, "w") fcntl.flock(server_start_lock, fcntl.LOCK_EX) diff --git a/nix/overlay.nix b/nix/overlay.nix index 2619d2f..3f61db4 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -4,6 +4,7 @@ final: prev: { bbdevPythonPkgs = final.python312.withPackages (ps: with ps; [ pydantic requests + pyyaml gitpython humanfriendly doit