From 90914a9a4ccfa5865f0f4815fc8f27a7174796ef Mon Sep 17 00:00:00 2001 From: booth-algo Date: Wed, 3 Jun 2026 16:45:58 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat(profiler):=20per-board=20cycle=20model?= =?UTF-8?q?s=20=E2=80=94=20load=20board=5Fconfigs=20YAMLs=20in=20isa=5Fana?= =?UTF-8?q?lysis.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds board_configs/nexys_a7.yaml (Artix-7 XC7A200T / Nexys Video) and v80.yaml (Alveo V80), plus load_board_config / cycle_model_from_board / a --board CLI in the ISA profiler, so a program's cycle cost can be scored against a specific FPGA's per-op latencies instead of plena_settings.toml. Only the board's compute cost model (the latency: section) is consumed; async memory timing (H_PREFETCH/H_STORE) stays uncharged, matching the behaviour simulator's async memory model. Extracted from the WIP profiling branch (sim PR #58) — deliberately excludes that branch's docs, the Rust DDR3/streaming memory model (lib/memory/streaming.rs, cli.rs, main.rs) and its serde Cargo deps, and the stale ATEN_UNROLL compare-harness edits that predate the ATEN_OPS_UNROLL rename. Verified: the profiler runs against a real decoder ASM for both nexys_a7 and v80. --- .../testbench/aten/compare/isa_analysis.py | 92 +++++++++++++++++++ .../testbench/board_configs/nexys_a7.yaml | 78 ++++++++++++++++ .../testbench/board_configs/v80.yaml | 62 +++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 transactional_emulator/testbench/board_configs/nexys_a7.yaml create mode 100644 transactional_emulator/testbench/board_configs/v80.yaml diff --git a/transactional_emulator/testbench/aten/compare/isa_analysis.py b/transactional_emulator/testbench/aten/compare/isa_analysis.py index 79552bf7..8a96d640 100644 --- a/transactional_emulator/testbench/aten/compare/isa_analysis.py +++ b/transactional_emulator/testbench/aten/compare/isa_analysis.py @@ -291,3 +291,95 @@ def analyze_asm( "instruction_types_dynamic": dict(sorted(type_counts.items())), "selected_opcodes_dynamic": {op: op_counts[op] for op in selected_opcodes if op_counts[op]}, } + + +# --- Per-board cycle models ------------------------------------------------ +# Load a SimulatorCycleModel from a board config YAML (board_configs/.yaml) +# instead of plena_settings.toml, so the profiler can score a program against a +# specific FPGA's per-op cycle costs. Only the compute cost model (the board's +# `latency:` section) is consumed here; async memory timing (H_PREFETCH/H_STORE) +# is not statically charged, matching the behaviour simulator's async memory +# model and load_behavior_cycle_model above. + +_BOARD_CONFIG_DIR = _repo_root_from_here() / "board_configs" + + +def load_board_config(board: str) -> dict[str, Any]: + """Load a board config YAML by name (e.g. 'nexys_a7', 'v80').""" + import yaml + + path = _BOARD_CONFIG_DIR / f"{board}.yaml" + if not path.exists(): + available = sorted(p.stem for p in _BOARD_CONFIG_DIR.glob("*.yaml")) + raise FileNotFoundError(f"Board config '{board}' not found. Available: {available}") + with open(path) as f: + return yaml.safe_load(f) + + +def cycle_model_from_board(board_cfg: dict[str, Any], *, mlen: int = 64, vlen: int = 64) -> SimulatorCycleModel: + """Build a SimulatorCycleModel from a board config's `latency:` section.""" + lat = board_cfg["latency"] + dc_en = 1 if lat.get("dc_lib_en", False) else 0 + return SimulatorCycleModel( + settings_path=Path(board_cfg.get("name", "board_config")), + dc_en=dc_en, + latency_profile=board_cfg.get("name"), + mlen=mlen, + vlen=vlen, + systolic_processing_overhead=lat["systolic_processing_overhead"], + vector_add_cycles=lat["vector_add_cycles"], + vector_mul_cycles=lat["vector_mul_cycles"], + vector_exp_cycles=lat["vector_exp_cycles"], + vector_reci_cycles=lat["vector_reci_cycles"], + vector_max_cycles=lat["vector_max_cycles"], + vector_sum_cycles=lat["vector_sum_cycles"], + scalar_fp_basic_cycles=lat["scalar_fp_basic_cycles"], + scalar_fp_exp_cycles=lat["scalar_fp_exp_cycles"], + scalar_fp_sqrt_cycles=lat["scalar_fp_sqrt_cycles"], + scalar_fp_reci_cycles=lat["scalar_fp_reci_cycles"], + scalar_int_basic_cycles=lat["scalar_int_basic_cycles"], + ) + + +def main() -> None: + import argparse + + available = sorted(p.stem for p in _BOARD_CONFIG_DIR.glob("*.yaml")) if _BOARD_CONFIG_DIR.exists() else [] + + parser = argparse.ArgumentParser(description="Profile PLENA ASM cycle cost against a board config") + parser.add_argument("asm_file", type=Path, help="Path to generated_asm_code.asm") + parser.add_argument("--board", default="nexys_a7", help=f"Board config name (from board_configs/). Available: {available}") + parser.add_argument("--mlen", type=int, default=64, help="MLEN for matrix-op cycle cost (default: 64)") + parser.add_argument("--clock-mhz", type=float, default=None, help="Override clock (default: board clock_mhz)") + parser.add_argument("--json", action="store_true", help="Output raw JSON instead of a summary") + args = parser.parse_args() + + board_cfg = load_board_config(args.board) + cycle_model = cycle_model_from_board(board_cfg, mlen=args.mlen, vlen=args.mlen) + clock_mhz = args.clock_mhz or float(board_cfg.get("clock_mhz", 100.0)) + + result = analyze_asm(args.asm_file.read_text(), cycle_model=cycle_model) + + if args.json: + import json + + print(json.dumps(result, indent=2, default=str)) + return + + cycles = result["estimated_cycles"] + print(f"=== {args.asm_file.name} ({board_cfg.get('name', args.board)} @{clock_mhz:.0f}MHz, MLEN={args.mlen}) ===") + print(f"Source lines: {result['source_lines']:,}") + print(f"Static instrs: {result['static_instruction_lines']:,}") + print(f"Dynamic instrs: {result['dynamic_instruction_count']:,}") + print(f"Estimated cycles: {cycles:,}") + print(f"Estimated time: {cycles / (clock_mhz * 1e3):.3f} ms @ {clock_mhz:.0f}MHz") + print(f"Cycle model: {result['cycle_model']}") + sel = result["selected_opcodes_dynamic"] + if sel: + print("\nSelected opcode dynamic counts:") + for op, n in sorted(sel.items(), key=lambda x: -x[1]): + print(f" {op:14s} {n:>14,}") + + +if __name__ == "__main__": + main() diff --git a/transactional_emulator/testbench/board_configs/nexys_a7.yaml b/transactional_emulator/testbench/board_configs/nexys_a7.yaml new file mode 100644 index 00000000..56ff95a4 --- /dev/null +++ b/transactional_emulator/testbench/board_configs/nexys_a7.yaml @@ -0,0 +1,78 @@ +# Digilent Nexys Video — Xilinx Artix-7 XC7A200T +# FPGA fabric only, no dedicated compute library (dc_lib_dis) + +name: nexys_a7 +description: Artix-7 XC7A200T on Nexys Video board +vendor: xilinx +family: artix7 +part: xc7a200t + +clock_mhz: 100 + +memory: + type: ddr3 + spec: DDR3-1600 + chip: MT41K256M16HA # single x16 chip on Nexys Video + bus_width: x16 # 2 bytes per transfer + physical_size_bytes: 536870912 # 512 MiB (Nexys Video onboard) + modeled_size_bytes: 137438953472 # 128 GiB (emulator model) + bus_width_bits: 512 # PLENA HBM abstraction layer width + peak_bandwidth_gbps: 1.6 # x16 bus: 2B × 800 MT/s + effective_bandwidth_gbps: 1.0 # ~60-65% efficiency (matches Vitis AI DPU B1152 avg) + matrix_sram_tiles: 4096 + vector_sram_depth: 4194304 + hbm_m_prefetch_amount: 64 + hbm_v_prefetch_amount: 4 + hbm_v_writeback_amount: 4 + # DDR3 latency at 100MHz PLENA clock (1 cycle = 10ns) + # MIG UI clock is 200MHz (PHY 400MHz); CDC FIFO bridges to 100MHz PLENA clock + # MIG avg read latency: 27.7 cycles (ZipCPU measured on Kintex-7 MIG) + # V prefetch: 4 rows × 64B = 256B → 256B / 1.0 GB/s = 256ns = 25.6 cyc + # M prefetch: 64 tiles × 64B = 4KB → 4KB / 1.0 GB/s = 4000ns = 400 cyc + ddr3_burst_cycles: 5 # 64B burst: CAS + transfer ≈ 40-50ns + ddr3_row_miss_penalty: 3 # tRP+tRCD ~30ns → 3 PLENA cycles + prefetch_v_cycles: 25 # 256B at ~1.0 GB/s effective + prefetch_m_cycles: 400 # 4KB at ~1.0 GB/s effective + store_v_cycles: 25 # write-back same as prefetch V + usb2_bandwidth_mbps: 38 # FT2232H practical (not theoretical 60) + +precision: + matrix_sram: {type: fp, sign: true, exponent: 8, mantissa: 7} # BF16 + vector_sram: {type: fp, sign: true, exponent: 8, mantissa: 7} # BF16 + hbm_act: {exponent: 4, mantissa: 3, scale_exponent: 8, block: 8} # MXFP8 E4M3 + hbm_weight: {exponent: 4, mantissa: 3, scale_exponent: 8, block: 8} + +latency: + dc_lib_en: false + systolic_processing_overhead: 0 + vector_add_cycles: 2 + vector_mul_cycles: 5 + vector_exp_cycles: 6 + vector_reci_cycles: 7 + vector_max_cycles: 4 + vector_sum_cycles: 20 + vector_shift_cycles: 1 + vector_prefix_scan_cycles: 9 + scalar_fp_basic_cycles: 1 + scalar_fp_exp_cycles: 2 + scalar_fp_sqrt_cycles: 2 + scalar_fp_reci_cycles: 2 + scalar_int_basic_cycles: 1 + +power: + tdp_watts: 7.5 + io_voltage: 3.3 + +resources: + luts: 134600 + ffs: 269200 + bram_36k: 365 + dsp48: 740 + +# Synthesis-proven tile configurations (tested on this board) +tile_hints: + - {mlen: 64, vlen: 64, blen: 4, batch_size: 1, status: proven} + - {mlen: 64, vlen: 64, blen: 16, batch_size: 1, status: proven} + - {mlen: 64, vlen: 64, blen: 16, batch_size: 2, status: proven} + - {mlen: 128, vlen: 128, blen: 64, batch_size: 1, status: untested} + - {mlen: 256, vlen: 256, blen: 64, batch_size: 1, status: exceeds_resources} diff --git a/transactional_emulator/testbench/board_configs/v80.yaml b/transactional_emulator/testbench/board_configs/v80.yaml new file mode 100644 index 00000000..bc400410 --- /dev/null +++ b/transactional_emulator/testbench/board_configs/v80.yaml @@ -0,0 +1,62 @@ +# AMD/Xilinx Versal V80 — AI Edge accelerator +# Dedicated compute library available (dc_lib_en) + +name: v80 +description: Versal V80 AI Edge with dedicated compute blocks +vendor: amd +family: versal +part: v80 + +clock_mhz: 400 + +memory: + hbm_size_bytes: 34359738368 # 32 GiB HBM2e + hbm_width_bits: 512 + hbm_bandwidth_gbps: 819.2 # 32 pseudo-channels × 3.2 Gbps + matrix_sram_tiles: 4096 + vector_sram_depth: 4194304 + hbm_m_prefetch_amount: 64 + hbm_v_prefetch_amount: 4 + hbm_v_writeback_amount: 4 + +precision: + matrix_sram: {type: fp, sign: true, exponent: 8, mantissa: 7} + vector_sram: {type: fp, sign: true, exponent: 8, mantissa: 7} + hbm_act: {exponent: 4, mantissa: 3, scale_exponent: 8, block: 8} + hbm_weight: {exponent: 4, mantissa: 3, scale_exponent: 8, block: 8} + +latency: + dc_lib_en: true + systolic_processing_overhead: 0 + vector_add_cycles: 1 + vector_mul_cycles: 1 + vector_exp_cycles: 1 + vector_reci_cycles: 2 + vector_max_cycles: 4 + vector_sum_cycles: 8 + vector_shift_cycles: 1 + vector_prefix_scan_cycles: 9 + scalar_fp_basic_cycles: 1 + scalar_fp_exp_cycles: 1 + scalar_fp_sqrt_cycles: 1 + scalar_fp_reci_cycles: 1 + scalar_int_basic_cycles: 1 + +power: + tdp_watts: 75 + io_voltage: 1.8 + +resources: + luts: 899840 + ffs: 1799680 + bram_36k: 967 + uram: 463 + dsp58: 1968 + aie_tiles: 304 + +# Synthesis-proven tile configurations (tested on this board) +tile_hints: + - {mlen: 64, vlen: 64, blen: 16, batch_size: 1, status: proven} + - {mlen: 128, vlen: 128, blen: 64, batch_size: 1, status: proven} + - {mlen: 256, vlen: 256, blen: 64, batch_size: 1, status: proven} + - {mlen: 256, vlen: 256, blen: 64, batch_size: 2, status: untested} From 9c1c6a45818d81ebc4b10ddee2e400ec397493a8 Mon Sep 17 00:00:00 2001 From: booth-algo Date: Wed, 3 Jun 2026 17:03:05 +0100 Subject: [PATCH 2/2] style: ruff format isa_analysis.py (wrap --board argparse line) --- transactional_emulator/testbench/aten/compare/isa_analysis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/transactional_emulator/testbench/aten/compare/isa_analysis.py b/transactional_emulator/testbench/aten/compare/isa_analysis.py index 8a96d640..15b0f95b 100644 --- a/transactional_emulator/testbench/aten/compare/isa_analysis.py +++ b/transactional_emulator/testbench/aten/compare/isa_analysis.py @@ -348,7 +348,9 @@ def main() -> None: parser = argparse.ArgumentParser(description="Profile PLENA ASM cycle cost against a board config") parser.add_argument("asm_file", type=Path, help="Path to generated_asm_code.asm") - parser.add_argument("--board", default="nexys_a7", help=f"Board config name (from board_configs/). Available: {available}") + parser.add_argument( + "--board", default="nexys_a7", help=f"Board config name (from board_configs/). Available: {available}" + ) parser.add_argument("--mlen", type=int, default=64, help="MLEN for matrix-op cycle cost (default: 64)") parser.add_argument("--clock-mhz", type=float, default=None, help="Override clock (default: board clock_mhz)") parser.add_argument("--json", action="store_true", help="Output raw JSON instead of a summary")