Skip to content

Commit 8b42269

Browse files
authored
Merge pull request #8 from dau-dev/tkp/cli
add benchmarks
2 parents 8624ce3 + 355a840 commit 8b42269

17 files changed

Lines changed: 1500 additions & 73 deletions

Makefile

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ lint-js: ## run js linter
4545
cd js; pnpm lint
4646

4747
lint-docs: ## lint docs with mdformat and codespell
48-
python -m mdformat --check README.md
49-
python -m codespell_lib README.md
48+
python -m mdformat --check README.md
49+
python -m codespell_lib README.md
5050

5151
lint: lint-js lint-py lint-docs ## run project linters
5252

@@ -62,8 +62,8 @@ fix-js: ## fix js formatting
6262
cd js; pnpm fix
6363

6464
fix-docs: ## autoformat docs with mdformat and codespell
65-
python -m mdformat README.md
66-
python -m codespell_lib --write README.md
65+
python -m mdformat README.md
66+
python -m codespell_lib --write README.md
6767

6868
fix: fix-js fix-py fix-docs ## run project autoformatters
6969

@@ -115,6 +115,39 @@ coverage: coverage-py coverage-js ## run all tests and collect test coverage
115115
# alias
116116
tests: test
117117

118+
##############
119+
# BENCHMARKS #
120+
##############
121+
.PHONY: benchmark benchmark-local benchmark-local-quick benchmark-cross-quick benchmark-cross-runtime benchmark-steady-state benchmark-compare
122+
123+
BENCHMARK_DIR := dau_sim/benchmarks
124+
BENCHMARK_RESULTS_DIR := $(BENCHMARK_DIR)/results
125+
BENCHMARK_FILES := $(BENCHMARK_DIR)/bench_*.py
126+
127+
benchmark: benchmark-local ## run full local pytest-benchmark suite
128+
129+
benchmark-local: ## run local pytest-benchmark suite
130+
mkdir -p $(BENCHMARK_RESULTS_DIR)
131+
python -m pytest $(BENCHMARK_FILES) -v --benchmark-only --benchmark-columns=mean,stddev,median,iqr,rounds --benchmark-save=local --benchmark-storage=file://$(BENCHMARK_RESULTS_DIR) --benchmark-json=$(BENCHMARK_RESULTS_DIR)/local.json
132+
133+
benchmark-local-quick: ## run quick local pytest-benchmark suite
134+
mkdir -p $(BENCHMARK_RESULTS_DIR)
135+
python -m pytest $(BENCHMARK_FILES) -v --benchmark-only --benchmark-min-rounds=1 --benchmark-max-time=0.02 --benchmark-columns=mean,stddev,median,rounds --benchmark-save=local-quick --benchmark-storage=file://$(BENCHMARK_RESULTS_DIR) --benchmark-json=$(BENCHMARK_RESULTS_DIR)/local-quick.json
136+
137+
benchmark-cross-quick: ## run quick cross-simulator benchmarks only
138+
mkdir -p $(BENCHMARK_RESULTS_DIR)
139+
python -m pytest $(BENCHMARK_DIR)/bench_cross_simulators.py -v --benchmark-only --benchmark-min-rounds=1 --benchmark-max-time=0.02 --benchmark-columns=mean,stddev,median,rounds --benchmark-save=cross-quick --benchmark-storage=file://$(BENCHMARK_RESULTS_DIR) --benchmark-json=$(BENCHMARK_RESULTS_DIR)/cross-quick.json
140+
141+
benchmark-cross-runtime: ## run cross-simulator benchmarks with larger cycle count for runtime-dominant comparisons
142+
mkdir -p $(BENCHMARK_RESULTS_DIR)
143+
DAU_BENCH_CYCLES=500000 python -m pytest $(BENCHMARK_DIR)/bench_cross_simulators.py -v --benchmark-only --benchmark-min-rounds=3 --benchmark-columns=mean,stddev,median,rounds --benchmark-save=cross-runtime-500k --benchmark-storage=file://$(BENCHMARK_RESULTS_DIR) --benchmark-json=$(BENCHMARK_RESULTS_DIR)/cross-runtime-500k.json
144+
145+
benchmark-steady-state: ## run compile-once steady-state runtime comparison harness
146+
python .benchmarks/steady_state_perf_compare.py --cycles 100000 --warmup 1 --repeats 5
147+
148+
benchmark-compare: ## compare latest benchmark run against previous saved run
149+
python -m pytest $(BENCHMARK_FILES) -v --benchmark-only --benchmark-compare --benchmark-storage=file://$(BENCHMARK_RESULTS_DIR)
150+
118151
###########
119152
# VERSION #
120153
###########

dau_sim/benchmarks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""pytest-benchmark suite for dau-sim."""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from dau_sim.benchmarks.compile_partitioning import compile_partitioned_module
6+
7+
8+
@pytest.mark.parametrize("n", [16, 64, 256, 1024])
9+
def test_benchmark_compile_partitioning(benchmark, n: int) -> None:
10+
benchmark.pedantic(lambda: compile_partitioned_module(n), rounds=1, iterations=1)
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
from __future__ import annotations
2+
3+
import atexit
4+
import os
5+
import shutil
6+
import subprocess
7+
import sys
8+
import tempfile
9+
import textwrap
10+
from pathlib import Path
11+
12+
import pytest
13+
14+
REPO_ROOT = Path(__file__).resolve().parents[2]
15+
AMARANTH_REPO = REPO_ROOT / "amaranth"
16+
17+
if str(REPO_ROOT) not in sys.path:
18+
sys.path.insert(0, str(REPO_ROOT))
19+
if str(AMARANTH_REPO) not in sys.path:
20+
sys.path.insert(0, str(AMARANTH_REPO))
21+
22+
23+
_VERILATOR_RUNTIME_CACHE: dict[int, tuple[Path, Path]] = {}
24+
CROSS_SIM_CYCLES = int(os.getenv("DAU_BENCH_CYCLES", "5000"))
25+
26+
27+
@atexit.register
28+
def _cleanup_verilator_runtime_cache() -> None:
29+
for td, _exe in _VERILATOR_RUNTIME_CACHE.values():
30+
shutil.rmtree(td, ignore_errors=True)
31+
32+
33+
def _make_counter():
34+
from amaranth import Elaboratable, Module as AModule, Signal
35+
36+
class Counter(Elaboratable):
37+
def __init__(self):
38+
self.en = Signal(1)
39+
self.count = Signal(32)
40+
41+
def elaborate(self, platform):
42+
m = AModule()
43+
with m.If(self.en):
44+
m.d.sync += self.count.eq(self.count + 1)
45+
return m
46+
47+
return Counter
48+
49+
50+
def _run_dau_sim(cycles: int) -> None:
51+
from dau_sim.compiler import compile_module
52+
from dau_sim.frontends import from_amaranth
53+
54+
Counter = _make_counter()
55+
dut = Counter()
56+
ir_mod = from_amaranth(dut)
57+
cm = compile_module(ir_mod)
58+
cm.run(cycles=cycles, inputs={"en": 1}, trace_signals=["count"], return_traces=False)
59+
60+
61+
def _run_amaranth_sim(cycles: int) -> None:
62+
from amaranth.sim import Simulator, Tick
63+
64+
Counter = _make_counter()
65+
dut = Counter()
66+
sim = Simulator(dut)
67+
sim.add_clock(1e-6)
68+
69+
def proc():
70+
yield dut.en.eq(1)
71+
for _ in range(cycles):
72+
yield Tick()
73+
74+
sim.add_process(proc)
75+
sim.run()
76+
77+
78+
def _run_amaranth_cxxsim(cycles: int) -> None:
79+
from amaranth.sim import Simulator, Tick
80+
81+
Counter = _make_counter()
82+
dut = Counter()
83+
sim = Simulator(dut, engine="cxxsim")
84+
85+
sim.add_clock(1e-6)
86+
87+
def proc():
88+
yield dut.en.eq(1)
89+
for _ in range(cycles):
90+
yield Tick()
91+
92+
sim.add_process(proc)
93+
sim.run()
94+
95+
96+
def _compile_verilator_binary(cycles: int, *, persist: bool = False) -> tuple[Path, Path]:
97+
verilator = shutil.which("verilator")
98+
if not verilator:
99+
raise RuntimeError("verilator not found on PATH")
100+
101+
verilog = textwrap.dedent(
102+
f"""
103+
`timescale 1ns/1ps
104+
module counter(
105+
input wire clk,
106+
input wire rst,
107+
input wire en,
108+
output reg [31:0] count
109+
);
110+
always @(posedge clk) begin
111+
if (rst) count <= 32'd0;
112+
else if (en) count <= count + 32'd1;
113+
end
114+
endmodule
115+
116+
module tb;
117+
reg clk = 0;
118+
reg rst = 1;
119+
reg en = 1;
120+
wire [31:0] count;
121+
integer i;
122+
123+
counter dut(
124+
.clk(clk),
125+
.rst(rst),
126+
.en(en),
127+
.count(count)
128+
);
129+
130+
initial begin
131+
#1 rst = 0;
132+
for (i = 0; i < {cycles}; i = i + 1) begin
133+
#1 clk = 1;
134+
#1 clk = 0;
135+
end
136+
$finish;
137+
end
138+
endmodule
139+
"""
140+
)
141+
142+
if persist:
143+
td = Path(tempfile.mkdtemp(prefix="dau_sim_verilator_rt_"))
144+
else:
145+
td = Path(tempfile.mkdtemp(prefix="dau_sim_verilator_"))
146+
src = td / "tb.v"
147+
src.write_text(verilog)
148+
cp = subprocess.run([verilator, "--binary", "--timing", "-Wno-fatal", str(src)], cwd=td, capture_output=True, text=True)
149+
if cp.returncode != 0:
150+
shutil.rmtree(td, ignore_errors=True)
151+
raise RuntimeError("verilator compile failed")
152+
exe = td / "obj_dir" / "Vtb"
153+
if not exe.exists():
154+
shutil.rmtree(td, ignore_errors=True)
155+
raise RuntimeError("verilator binary missing")
156+
return td, exe
157+
158+
159+
def _run_verilator_compiled(exe: Path) -> None:
160+
rp = subprocess.run([str(exe)], cwd=exe.parent.parent, capture_output=True, text=True)
161+
if rp.returncode != 0:
162+
raise RuntimeError("verilator run failed")
163+
164+
165+
def _run_verilator(cycles: int) -> None:
166+
td, exe = _compile_verilator_binary(cycles, persist=False)
167+
try:
168+
_run_verilator_compiled(exe)
169+
finally:
170+
shutil.rmtree(td, ignore_errors=True)
171+
172+
173+
def _run_verilator_runtime(cycles: int) -> None:
174+
if cycles not in _VERILATOR_RUNTIME_CACHE:
175+
_VERILATOR_RUNTIME_CACHE[cycles] = _compile_verilator_binary(cycles, persist=True)
176+
_td, exe = _VERILATOR_RUNTIME_CACHE[cycles]
177+
_run_verilator_compiled(exe)
178+
179+
180+
def _prepare_verilator_runtime(cycles: int) -> None:
181+
if cycles not in _VERILATOR_RUNTIME_CACHE:
182+
_VERILATOR_RUNTIME_CACHE[cycles] = _compile_verilator_binary(cycles, persist=True)
183+
184+
185+
def _ensure_backend_available(backend: str) -> None:
186+
if backend in {"dau-sim", "amaranth-sim", "cxxsim"}:
187+
try:
188+
_make_counter()
189+
if backend == "cxxsim":
190+
import inspect
191+
192+
from amaranth.sim import Simulator
193+
194+
if "cxxsim" not in inspect.getsource(Simulator.__init__):
195+
pytest.skip("cxxsim unavailable: this Amaranth build exposes only pysim")
196+
except Exception as exc:
197+
pytest.skip(f"{backend} unavailable: {exc}")
198+
199+
if backend in {"verilator-compile-run", "verilator-runtime"} and shutil.which("verilator") is None:
200+
pytest.skip("verilator unavailable: not found on PATH")
201+
202+
203+
@pytest.mark.parametrize(
204+
"backend,runner",
205+
[
206+
("dau-sim", _run_dau_sim),
207+
("amaranth-sim", _run_amaranth_sim),
208+
("cxxsim", _run_amaranth_cxxsim),
209+
("verilator-compile-run", _run_verilator),
210+
("verilator-runtime", _run_verilator_runtime),
211+
],
212+
)
213+
def test_benchmark_cross_simulators(benchmark, backend: str, runner) -> None:
214+
_ensure_backend_available(backend)
215+
if backend == "verilator-runtime":
216+
# Compile once outside the measured region; benchmark only executable runtime.
217+
_prepare_verilator_runtime(CROSS_SIM_CYCLES)
218+
benchmark.name = f"cross_simulators.{backend}"
219+
benchmark(lambda: runner(CROSS_SIM_CYCLES))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from dau_sim.benchmarks.selective_settle import run_partitioned_seq
6+
7+
8+
@pytest.mark.parametrize("n", [16, 64, 256, 512])
9+
@pytest.mark.parametrize("stmts_per_component", [1, 8, 32])
10+
def test_benchmark_selective_settle(benchmark, n: int, stmts_per_component: int) -> None:
11+
benchmark.pedantic(lambda: run_partitioned_seq(n, stmts_per_component, cycles=200), rounds=1, iterations=1)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
from dau_sim.compiler import compile_module
4+
from dau_sim.ir.expr import Binary, BinaryOp, Const, SignalRef
5+
from dau_sim.ir.module import ClockDomain, CombBlock, Module, Port, Signal
6+
from dau_sim.ir.stmt import Assign
7+
from dau_sim.ir.types import PortDirection, Shape
8+
9+
10+
def _make_partitioned_comb_module(n: int) -> Module:
11+
a = Signal("a", Shape(8))
12+
internal_signals = tuple(Signal(f"s{i}", Shape(8)) for i in range(n))
13+
output_signals = tuple(Signal(f"o{i}", Shape(8)) for i in range(n))
14+
15+
comb_blocks = tuple(
16+
CombBlock(
17+
stmts=(
18+
Assign(
19+
target=f"o{i}",
20+
value=Binary(
21+
shape=Shape(8),
22+
op=BinaryOp.ADD,
23+
left=SignalRef(shape=Shape(8), name="a" if i == 0 else f"s{i}"),
24+
right=Const(shape=Shape(8), value=i & 0xFF),
25+
),
26+
),
27+
)
28+
)
29+
for i in range(n)
30+
)
31+
32+
return Module(
33+
name=f"partition_compile_{n}",
34+
ports=(Port(a, PortDirection.INPUT),),
35+
signals=internal_signals + output_signals,
36+
comb_blocks=comb_blocks,
37+
clock_domains=(ClockDomain("sync", clk="clk"),),
38+
)
39+
40+
41+
def compile_partitioned_module(n: int) -> None:
42+
"""Compile a synthetic module with many disconnected comb blocks."""
43+
compile_module(_make_partitioned_comb_module(n))

0 commit comments

Comments
 (0)