Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions dau_sim/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from rich.table import Table

from dau_sim.api import Simulator
from dau_sim.perf import analyze_node_separation, benchmark_module, evaluate_delta

app = typer.Typer(help="dau-sim command line interface")
console = Console()
Expand Down Expand Up @@ -75,21 +74,25 @@ def perf_sv(
verilator_cps: float | None = typer.Option(None, "--verilator-cps", min=0.0, help="Optional Verilator baseline cycles/sec."),
) -> None:
parsed_inputs = _parse_kv_pairs(inputs)
sim = Simulator.from_sv_file(str(path), top=top)

bench = benchmark_module(
sim.module,
cycles=cycles,
repeats=repeats,
warmup=warmup,
inputs=parsed_inputs,
)
sep = analyze_node_separation(sim.module)
delta = evaluate_delta(
bench.cycles_per_second,
amaranth_cycles_per_second=amaranth_cps,
verilator_cycles_per_second=verilator_cps,
from dau_sim.config import run_request_config

result = run_request_config(
"task",
"tasks/analysis/perf-sv",
model_values={
"path": path,
"top": top,
"cycles": cycles,
"repeats": repeats,
"warmup": warmup,
"inputs": parsed_inputs,
"amaranth_cycles_per_second": amaranth_cps,
"verilator_cycles_per_second": verilator_cps,
},
)
bench = result.benchmark
sep = result.node_separation
delta = result.delta

perf_table = Table(title="Performance")
perf_table.add_column("Metric")
Expand Down
107 changes: 107 additions & 0 deletions dau_sim/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from __future__ import annotations

from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any

from ccflow import ModelRegistry
from ccflow.utils.hydra import ConfigLoadResult, cfg_run, load_config as base_load_config
from omegaconf import OmegaConf

__all__ = ("compose_config", "load_config", "profile_names", "request_config", "run_request_config")


def load_config(
overrides: Sequence[str] | None = None,
*,
overwrite: bool = False,
config_dir: str | None = None,
version_base: str | None = None,
) -> ModelRegistry:
result = _load_base_config(overrides, config_dir=config_dir, version_base=version_base)
registry = ModelRegistry.root()
registry.load_config(result.cfg, overwrite=overwrite)
return registry


def request_config(
request_kind: str,
request_name: str,
*,
model_values: Mapping[str, Any] | None = None,
overrides: Sequence[str] | None = None,
config_dir: str | None = None,
version_base: str | None = None,
) -> ConfigLoadResult:
result = _load_base_config(
(f"{request_kind}={request_name}", *(overrides or ())),
config_dir=config_dir,
version_base=version_base,
)
for key, value in (model_values or {}).items():
OmegaConf.update(result.cfg, f"model.{key}", _config_value(value), merge=False, force_add=True)
return result


def run_request_config(
request_kind: str,
request_name: str,
*,
model_values: Mapping[str, Any] | None = None,
overrides: Sequence[str] | None = None,
config_dir: str | None = None,
version_base: str | None = None,
):
return cfg_run(
request_config(
request_kind,
request_name,
model_values=model_values,
overrides=overrides,
config_dir=config_dir,
version_base=version_base,
).cfg
)


def compose_config(
overrides: Sequence[str] | None = None,
*,
config_dir: str | None = None,
version_base: str | None = None,
) -> ConfigLoadResult:
return _load_base_config(overrides, config_dir=config_dir, version_base=version_base)


def profile_names(*, config_dir: str | None = None, version_base: str | None = None) -> tuple[str, ...]:
result = _load_base_config(config_dir=config_dir, version_base=version_base, debug=True)
return tuple(sorted(result.group_options.get("profile/profiles", ())))


def _load_base_config(
overrides: Sequence[str] | None = None,
*,
config_dir: str | None = None,
version_base: str | None = None,
debug: bool = False,
) -> ConfigLoadResult:
parent_dir = str(Path(__file__).resolve().parent)
return base_load_config(
root_config_dir=parent_dir,
root_config_name="base",
config_dir=config_dir,
overrides=list(overrides or ()),
version_base=version_base,
basepath=parent_dir,
debug=debug,
)


def _config_value(value: Any) -> Any:
if isinstance(value, Path):
return str(value)
if isinstance(value, tuple | list):
return [_config_value(item) for item in value]
if isinstance(value, Mapping):
return {str(key): _config_value(item) for key, item in value.items()}
return value
19 changes: 19 additions & 0 deletions dau_sim/config/base.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# @package _global_

defaults:
- _self_
- optional task: null
- optional profile: null
- callable: callable

context: {}

cli:
model:
_target_: ccflow.FlowOptions
evaluator:
_target_: ccflow.evaluators.MultiEvaluator
evaluators:
- _target_: ccflow.evaluators.GraphEvaluator
- _target_: ccflow.evaluators.MemoryCacheEvaluator
cacheable: true
3 changes: 3 additions & 0 deletions dau_sim/config/callable/callable.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @package _global_

callable: /model
9 changes: 9 additions & 0 deletions dau_sim/config/profile/profiles/ready-valid-sum.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @package profile

_target_: dau_sim.integrations.verilator_profiles.VerilatorProfile
name: ready-valid-sum
sources:
- package://dau_sim/tests/sv/ready_valid_sum.sv
- package://dau_sim/tests/sv/ready_valid_sum_tb.sv
top_module: ready_valid_sum_tb
expect_stdout: READY_VALID_SUM_TB_OK
12 changes: 12 additions & 0 deletions dau_sim/config/task/tasks/analysis/perf-sv.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @package model

_target_: dau_sim.perf.PerfSvTask
path: ???
top: null
cycles: 30000
repeats: 3
warmup: 1
inputs: null
clock_period_us: 1.0
amaranth_cycles_per_second: null
verilator_cycles_per_second: null
8 changes: 2 additions & 6 deletions dau_sim/integrations/cocotb.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@
from collections.abc import Mapping, Sequence
from pathlib import Path

from pydantic import BaseModel, ConfigDict
from dau_sim.integrations.profiles import SimulationProfile

DEFAULT_BUILD_ARGS = ("--timing", "-Wno-fatal")


class CocotbProfile(BaseModel):
class CocotbProfile(SimulationProfile):
"""A registered cocotb bench: HDL sources, the toplevel they build,
and the cocotb test module that drives it."""

model_config = ConfigDict(frozen=True)

name: str
sources: tuple[Path, ...]
hdl_toplevel: str
test_module: str

Expand Down
58 changes: 58 additions & 0 deletions dau_sim/integrations/profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations

from importlib.resources import files
from pathlib import Path
from typing import Any

from ccflow import BaseModel
from hydra.errors import ConfigCompositionException
from pydantic import ConfigDict, field_validator

PACKAGE_URI_PREFIX = "package://"


class SimulationProfile(BaseModel):
model_config = ConfigDict(frozen=True)

name: str
sources: tuple[Path, ...]

@field_validator("sources", mode="before")
@classmethod
def _resolve_sources(cls, sources: Any) -> tuple[Path, ...]:
return tuple(_resolve_source(source) for source in sources)


def available_simulation_profiles(*, config_dir: str | None = None) -> tuple[str, ...]:
from dau_sim.config import profile_names

return profile_names(config_dir=config_dir)


def resolve_simulation_profile(name: str, *, config_dir: str | None = None) -> SimulationProfile:
from dau_sim.config import load_config, profile_names

known_names = profile_names(config_dir=config_dir)
if name not in known_names:
known = ", ".join(known_names)
raise KeyError(f"unknown simulation profile {name!r}; expected one of: {known}")
try:
profile = load_config([f"profile=profiles/{name}"], overwrite=True, config_dir=config_dir)["profile"]
except (ConfigCompositionException, KeyError):
known = ", ".join(known_names)
raise KeyError(f"unknown simulation profile {name!r}; expected one of: {known}") from None
if not isinstance(profile, SimulationProfile):
raise TypeError(f"profile config {name!r} did not produce a SimulationProfile")
return profile


def _resolve_source(source: Path | str) -> Path:
if isinstance(source, Path):
return source
if not source.startswith(PACKAGE_URI_PREFIX):
return Path(source)
resource = source.removeprefix(PACKAGE_URI_PREFIX)
package, separator, resource_name = resource.partition("/")
if not package or not separator or not resource_name:
raise ValueError(f"invalid package resource URI: {source}")
return Path(str(files(package).joinpath(resource_name)))
57 changes: 16 additions & 41 deletions dau_sim/integrations/verilator_profiles.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,27 @@
from __future__ import annotations

from pathlib import Path
from dau_sim.integrations.profiles import SimulationProfile, available_simulation_profiles, resolve_simulation_profile

from pydantic import BaseModel, ConfigDict


class VerilatorProfile(BaseModel):
model_config = ConfigDict(frozen=True)

name: str
sources: tuple[Path, ...]
class VerilatorProfile(SimulationProfile):
top_module: str
expect_stdout: str


_SV_TESTBENCH_DIR = Path(__file__).resolve().parents[1] / "tests" / "sv"

# dau-sim is a generic simulation package: only its own benches are built in.
# Domain packages register their profiles (register_verilator_profile) or
# ship artlink manifests resolved by their own tooling.
_PROFILES: dict[str, VerilatorProfile] = {
"ready-valid-sum": VerilatorProfile(
name="ready-valid-sum",
sources=(
_SV_TESTBENCH_DIR / "ready_valid_sum.sv",
_SV_TESTBENCH_DIR / "ready_valid_sum_tb.sv",
),
top_module="ready_valid_sum_tb",
expect_stdout="READY_VALID_SUM_TB_OK",
),
}


def available_verilator_profiles() -> tuple[str, ...]:
return tuple(sorted(_PROFILES))


def register_verilator_profile(profile: VerilatorProfile, *, replace: bool = False) -> None:
"""Open registration: packages and user code add profiles without
editing dau-sim (mirrors the dau-build config-overlay idiom)."""
if not replace and profile.name in _PROFILES:
raise ValueError(f"verilator profile {profile.name!r} is already registered; pass replace=True to override")
_PROFILES[profile.name] = profile
def available_verilator_profiles(*, config_dir: str | None = None) -> tuple[str, ...]:
return tuple(
name
for name in available_simulation_profiles(config_dir=config_dir)
if isinstance(resolve_simulation_profile(name, config_dir=config_dir), VerilatorProfile)
)


def resolve_verilator_profile(name: str) -> VerilatorProfile:
def resolve_verilator_profile(name: str, *, config_dir: str | None = None) -> VerilatorProfile:
try:
return _PROFILES[name]
except KeyError as exc:
known = ", ".join(available_verilator_profiles())
raise KeyError(f"unknown verilator profile {name!r}; expected one of: {known}") from exc
profile = resolve_simulation_profile(name, config_dir=config_dir)
except KeyError:
known = ", ".join(available_verilator_profiles(config_dir=config_dir))
raise KeyError(f"unknown verilator profile {name!r}; expected one of: {known}") from None
if not isinstance(profile, VerilatorProfile):
raise TypeError(f"simulation profile {name!r} is not a VerilatorProfile")
return profile
Loading
Loading