"""FireSim-specific configuration dataclasses and YAML parsers."""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import yaml
from chia.cluster.log import get_logger
logger = get_logger("firesim.config")
[docs]
@dataclass
class FireSimBuildConfig:
"""A single bitstream build recipe, parsed from config_build_recipes.yaml.
Mirrors one stanza of a FireSim ``config_build_recipes.yaml`` (the recipe
name is the YAML key; the remaining fields come from that entry). Drives an
FPGA bitstream build for a given target design + FPGA platform.
Attributes:
name: Recipe name (the YAML key); used as the build's base name
(e.g. ``"midasexamples_gcd"``).
platform: FireSim platform (e.g. ``"f1"``, ``"f2"``);
from the recipe's ``PLATFORM``.
target_project: Chipyard target project (``TARGET_PROJECT``,
e.g. ``"firesim"``, or ``"midasexamples"`` for MIDAS example designs).
target_project_makefrag: Optional makefrag override for the target
project (``TARGET_PROJECT_MAKEFRAG``, e.g.
``"../../../generators/firechip/chip/src/main/makefrag/firesim"``);
``None`` to use the default.
design: Top-level design/harness module to build (``DESIGN``, e.g.
``"FireSim"``, or ``"GCD"`` for the MIDAS example).
target_config: Chipyard target Scala config (``TARGET_CONFIG``) selecting
the SoC configuration (e.g. ``"FireSimMegaBoomChiaBigCacheConfig"``,
``"FireSimRocketConfig"``).
platform_config: FireSim platform config (``PLATFORM_CONFIG``) selecting
host-side simulation options (e.g. ``"DefaultF2Config"``).
fpga_frequency: Target FPGA clock frequency in MHz
(from ``platform_config_args.fpga_frequency``, e.g. ``20``).
build_strategy: Vivado build strategy (from
``platform_config_args.build_strategy``, e.g. ``"TIMING"``).
bit_builder_recipe: Path to the FireSim bit-builder recipe YAML
(``bit_builder_recipe``) selecting the platform's build flow (e.g.
``"bit-builder-recipes/f2.yaml"``).
deploy_quintuplet: Optional override for the FireSim deploy quintuplet
(``PLATFORM-TARGET_PROJECT-DESIGN-TARGET_CONFIG-PLATFORM_CONFIG``,
e.g. ``"f2-firesim-FireSim-FireSimMegaBoomChiaBigCacheConfig-DefaultF2Config"``);
``None`` to let FireSim derive it.
post_build_hook: Optional path to a script run after the build completes.
metasim_customruntimeconfig: Optional custom runtime config for metasim
(software-simulation) builds.
build_id: User-specified build ID (e.g. ``"megaboom-baseline"``). If
omitted, an auto-generated ``{name}-{timestamp}-{short_uuid}`` is used.
build_group: Optional subdirectory under ``builds/`` for organizing builds
(e.g. ``builds/{build_group}/{build_id}/``). If omitted, builds are stored flat under
``builds/{build_id}/``.
incremental_base_build_id: ``build_id`` of a previous build whose DCPs to
use as Vivado incremental synthesis/implementation references
(~50% faster); ``None`` for a full build.
market: AWS instance market for the build host (``"ondemand"`` or
``"spot"``).
enable_pr: Whether to enable partial reconfiguration (PR) for this build.
pr_module_name: Reconfigurable module name when ``enable_pr`` is set.
pr_base_build_id: ``build_id`` of the PR base build (its presence triggers
the reconfigurable-module flow).
pr_partition_cell: PR partition cell; auto-discovered if omitted.
"""
name: str
platform: str
target_project: str
target_project_makefrag: str | None
design: str
target_config: str
platform_config: str
fpga_frequency: float
build_strategy: str
bit_builder_recipe: str
deploy_quintuplet: str | None = None
post_build_hook: str | None = None
metasim_customruntimeconfig: str | None = None
# User-specified build ID (e.g. "megaboom-baseline"). If omitted, an
# auto-generated {name}-{timestamp}-{short_uuid} is used.
build_id: str | None = None
# Optional subdirectory under builds/ for organizing builds
# (e.g. "prefetcher-experiments" → builds/prefetcher-experiments/{build_id}/).
# If omitted, builds are stored flat under builds/{build_id}/.
build_group: str | None = None
# Incremental compile: build_id of a previous build whose DCPs to use as
# Vivado incremental synthesis/implementation references (~50% faster).
incremental_base_build_id: str | None = None
# Partial reconfiguration (PR)
market: str = "ondemand"
enable_pr: bool = False
pr_module_name: str | None = None # reconfigurable module (e.g. "BestOffsetPrefetcher")
pr_base_build_id: str | None = None # build_id of the PR base build (triggers RM flow)
pr_partition_cell: str | None = None # auto-discovered if omitted
[docs]
@dataclass
class FireSimRunConfig:
"""Runtime configuration for an FPGA simulation run.
Describes a single FireSim run: which bitstream (AGFI) and simulation driver
to deploy, which workload to run, and on what instances. The build artifacts
(``agfi``, ``driver_s3_path``) are typically auto-derived from ``build_ref``
via :meth:`resolve_build`.
Attributes:
hw_config_name: Name of the hardware-database (hwdb) entry / hardware
config to run (the ``config_hwdb.yaml`` key, e.g.
``"firesim_megaboom_chia_bigcache"``).
build_ref: Build reference — path under ``builds/`` on S3 identifying a
build, of the form ``"{recipe}/{build_id}"``. Examples:
``"FireSimRocket-20260409-a1b2c3d4"`` (flat) or
``"experiments/FireSimRocket-20260409-a1b2c3d4"`` (grouped).
When set, ``agfi`` and ``driver_s3_path`` are auto-derived via
:meth:`resolve_build`.
agfi: AWS Global FPGA Image ID (e.g. ``"agfi-01234abcde"``) to
program onto the FPGA; auto-derived from ``build_ref`` if unset.
workload_name: Name of the workload to run (e.g. ``"spec17-intrate.json"``).
num_sims: Number of parallel simulation slots to launch.
instance_type: AWS instance type for the run host
(e.g. ``"f2.12xlarge"``).
plusarg_passthrough: Extra ``+plusarg`` flags passed through to the
simulator.
terminate_on_completion: Whether to terminate the run instance when the
run finishes.
driver_tarball_path: Local path to the simulation driver tarball
(legacy/fallback; prefer ``driver_s3_path``).
ami_id: Optional AMI ID for the run instance.
market: AWS instance market for the run host (``"ondemand"`` or
``"spot"``).
bootbinary_path: Local path to the boot binary (legacy/fallback; prefer
``bootbinary_s3_path``).
rootfs_path: Local path to the root filesystem image (legacy/fallback;
prefer ``rootfs_s3_path``).
result_id: Optional subdirectory under ``results/`` for organizing run
results (e.g. ``results/{result_id}/{suite_name}/``). If omitted, results are stored under
``results/{suite_name}/{timestamp}/``.
driver_s3_path: S3 URI of the simulation driver tarball (preferred — any
worker node can use it), e.g.
``"s3://firesim-chia-builds/builds/design/driver-bundle.tar.gz"``;
auto-derived from ``build_ref`` when :meth:`resolve_build` is called.
bootbinary_s3_path: S3 URI of the boot binary (preferred over the local
path).
rootfs_s3_path: S3 URI of the root filesystem image (preferred over the
local path).
"""
hw_config_name: str
# Build reference — path under builds/ on S3 that identifies a build.
# Examples: "FireSimRocket-20260409-a1b2c3d4" (flat) or
# "prefetcher-experiments/FireSimRocket-20260409-a1b2c3d4" (grouped).
# When set, agfi and driver_s3_path are auto-derived via resolve_build().
build_ref: str | None = None
agfi: str | None = None
workload_name: str = ""
num_sims: int = 1
instance_type: str = "f2.12xlarge"
plusarg_passthrough: str = ""
terminate_on_completion: bool = True
# Local paths (legacy/fallback)
driver_tarball_path: str | None = None
ami_id: str | None = None
market: str = "ondemand"
bootbinary_path: str | None = None
rootfs_path: str | None = None
# Optional subdirectory under results/ for organizing run results
# (e.g. "opt-branch-20260409" → results/{result_id}/{suite_name}/).
# If omitted, results are stored under results/{suite_name}/{timestamp}/.
result_id: str | None = None
# S3 paths (preferred — any worker node can use these)
# Auto-derived from build_ref when resolve_build() is called.
driver_s3_path: str | None = None
bootbinary_s3_path: str | None = None
rootfs_s3_path: str | None = None
[docs]
def resolve_build(self, s3_bucket: str) -> FireSimRunConfig:
"""Populate agfi and driver_s3_path from build_ref if not already set.
Fetches ``build-info.json`` from S3 for the referenced build and uses it
to fill in ``agfi`` and ``driver_s3_path``. Explicit values for either
field take precedence (they are not overwritten). A no-op if ``build_ref``
is unset or both derived fields are already populated.
Args:
s3_bucket: S3 bucket name holding the build's ``build-info.json``.
Returns:
``self``, with ``agfi`` and ``driver_s3_path`` populated from the
build referenced by ``build_ref`` where they were previously unset.
Returned for chaining.
"""
if not self.build_ref:
return self
if self.agfi and self.driver_s3_path:
return self # both already set, nothing to resolve
info = fetch_build_info(s3_bucket, self.build_ref)
if not self.agfi:
self.agfi = info.get("agfi")
if not self.driver_s3_path:
self.driver_s3_path = info.get("driver_s3_path")
return self
[docs]
@dataclass
class FireSimConfig:
"""Top-level FireSim configuration, parsed from the Chia cluster YAML.
Attributes:
chipyard_path: Filesystem path to the Chipyard checkout on the host.
deploy_path: Filesystem path to the FireSim deploy directory
(``sim/firesim`` workspace where builds/runs are launched).
build_recipes_file: Filename of the FireSim build-recipes YAML to load
(defaults to ``"config_build_recipes.yaml"``).
"""
chipyard_path: str
deploy_path: str
build_recipes_file: str = "config_build_recipes.yaml"
[docs]
def load_build_recipes(yaml_path: str) -> dict[str, FireSimBuildConfig]:
"""Parse a config_build_recipes.yaml file into FireSimBuildConfig objects.
Args:
yaml_path: Path to config_build_recipes.yaml.
Returns:
Dict mapping recipe name to FireSimBuildConfig. Empty if the YAML is
empty.
Raises:
FileNotFoundError: If ``yaml_path`` does not exist.
"""
path = Path(yaml_path)
if not path.exists():
raise FileNotFoundError(f"Build recipes file not found: {yaml_path}")
with open(path) as f:
raw = yaml.safe_load(f)
if not raw:
return {}
recipes: dict[str, FireSimBuildConfig] = {}
for name, cfg in raw.items():
if not isinstance(cfg, dict):
continue
platform_args = cfg.get("platform_config_args", {})
recipes[name] = FireSimBuildConfig(
name=name,
platform=cfg.get("PLATFORM", ""),
target_project=cfg.get("TARGET_PROJECT", ""),
target_project_makefrag=cfg.get("TARGET_PROJECT_MAKEFRAG"),
design=cfg.get("DESIGN", ""),
target_config=cfg.get("TARGET_CONFIG", ""),
platform_config=cfg.get("PLATFORM_CONFIG", ""),
fpga_frequency=platform_args.get("fpga_frequency", 75),
build_strategy=platform_args.get("build_strategy", "TIMING"),
bit_builder_recipe=cfg.get("bit_builder_recipe", ""),
deploy_quintuplet=cfg.get("deploy_quintuplet"),
post_build_hook=cfg.get("post_build_hook"),
metasim_customruntimeconfig=cfg.get("metasim_customruntimeconfig"),
build_id=cfg.get("build_id"),
build_group=cfg.get("build_group"),
)
logger.debug(f"Loaded build recipe: {name} ({recipes[name].platform})")
return recipes
[docs]
def load_hwdb(yaml_path: str) -> dict[str, dict[str, Any]]:
"""Parse config_hwdb.yaml into a dict of hw config name -> properties.
Args:
yaml_path: Path to config_hwdb.yaml.
Returns:
Dict mapping hw config name to its hwdb properties (agfi, etc.); empty
if the YAML does not parse to a mapping.
Raises:
FileNotFoundError: If ``yaml_path`` does not exist.
"""
path = Path(yaml_path)
if not path.exists():
raise FileNotFoundError(f"HWDB file not found: {yaml_path}")
with open(path) as f:
raw = yaml.safe_load(f)
return raw if isinstance(raw, dict) else {}
[docs]
def fetch_build_info(s3_bucket: str, build_ref: str) -> dict:
"""Fetch build-info.json from S3 for a build reference.
Args:
s3_bucket: S3 bucket name.
build_ref: Path under builds/ identifying a build.
Examples: "FireSimRocket-20260409-a1b2c3d4" (flat),
"prefetcher-experiments/FireSimRocket-20260409-a1b2c3d4" (grouped),
or "latest" / "mygroup/latest" for the most recent build.
Returns:
Parsed build-info dict with keys: agfi, driver_s3_path, build_id, etc.
Raises:
FileNotFoundError: If build-info.json does not exist at the expected path.
"""
import boto3
s3_key = f"builds/{build_ref}/build-info.json"
logger.info(f"Fetching build info from s3://{s3_bucket}/{s3_key}")
s3 = boto3.client("s3")
try:
response = s3.get_object(Bucket=s3_bucket, Key=s3_key)
return json.loads(response["Body"].read().decode())
except s3.exceptions.NoSuchKey:
raise FileNotFoundError(
f"Build info not found: s3://{s3_bucket}/{s3_key}. "
f"Check that build_ref '{build_ref}' is correct."
)
except Exception as e:
raise FileNotFoundError(
f"Cannot fetch build info from s3://{s3_bucket}/{s3_key}: {e}. "
f"Check bucket name, credentials, and network."
)