Source code for chia.chipyard.chipyard_hammer

"""chia.chipyard.chipyard_hammer — Chipyard VLSI (hammer) makefile nodes.

Chipyard wraps hammer-vlsi behind ``make`` in ``<chipyard>/vlsi``: the
``buildfile`` target elaborates the design and generates ``$(OBJ_DIR)/
hammer.d``, a make fragment that is ``-include``d to provide the flow targets
(``syn``, ``par``, ``drc``, ``lvs``, ``power``, ``redo-<action>``, ...).
Everything is parameterized by make variables — ``CONFIG``, ``tech_name``,
``TOOLS_CONF`` / ``TECH_CONF`` / ``INPUT_CONFS``, ``VLSI_TOP``, ``OBJ_DIR``,
``HAMMER_EXTRA_ARGS``, ... — so one generic target runner covers the whole
flow.  :meth:`ChipyardHammerNode.make` is that runner.

The chipyard checkout, its generated RTL, and the OBJ_DIR are PATH-BASED on
the worker, so chained targets (buildfile -> syn -> par) and report fetches
(:meth:`ChipyardHammerNode.collect`) must land on the SAME worker.
:class:`ChipyardHammerNode` enforces that via a placement group (see
:class:`chia.base.colocated.ColocatedNode` for the given / reserved / no-PG
construction modes).  ``ChipyardHammerNode.<fn>.chia_remote(...)`` (the class
attribute) is the raw, unpinned form for callers that handle placement
themselves.

Like ``chia.chipyard.chisel_build_node``, this module assumes the worker's
environment is already chipyard-ready (the chipyard docker images source the
env in their setup commands); use the ``env`` argument for per-call overrides.

This module is deliberately independent of ``chia.vlsi.hammer``, which wraps
bare ``hammer-vlsi`` CLI calls: here hammer is an implementation detail behind
chipyard's Makefile.
"""

import glob as _glob
import logging
import os
import signal
import subprocess
from dataclasses import dataclass, field

from chia.base.ChiaFunction import ChiaFunction
from chia.base.colocated import ColocatedNode

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------

[docs] @dataclass class ChipyardHammerResult: success: bool returncode: int target: str vlsi_dir: str # <chipyard>/vlsi on the worker that ran make obj_dir: str | None # the OBJ_DIR passed in (None if chipyard's default) stdout: str stderr: str # Manifest of every file under obj_dir after the run: relative path -> # size in bytes. Empty when obj_dir was not given (chipyard's default # OBJ_DIR embeds generated names this node does not compute). Contents # stay on the worker; fetch them with ChipyardHammerNode.collect pinned # to the same bundle. listing: dict[str, int] = field(default_factory=dict)
[docs] @dataclass class ChipyardHammerCollectResult: base_dir: str files: dict[str, str] # relpath -> text contents (errors="replace") skipped: dict[str, int] # matched but over max_bytes_per_file; size shown listing: dict[str, int] # fresh manifest of base_dir at collect time
# --------------------------------------------------------------------------- # Worker-side helpers (module-level so they resolve by import on the worker) # --------------------------------------------------------------------------- def _list_files(base_dir: str) -> dict[str, int]: """Manifest of every file under base_dir: relative path -> size in bytes.""" listing: dict[str, int] = {} for root, _dirs, names in os.walk(base_dir): for name in names: path = os.path.join(root, name) try: listing[os.path.relpath(path, base_dir)] = os.path.getsize(path) except OSError: pass # dangling symlink etc. return listing # --------------------------------------------------------------------------- # ChipyardHammerNode # ---------------------------------------------------------------------------
[docs] class ChipyardHammerNode(ColocatedNode): """Chipyard VLSI make / collect primitives sharing one placement. The members are ``@staticmethod @ChiaFunction(resources={"chipyard": 1})``; ``__init__`` re-binds each into a per-instance pinned form so ``node.<fn>.chia_remote(...)`` lands on this node's bundle:: with ChipyardHammerNode() as node: # reserves {"CPU": 1, "chipyard": 1} obj_dir = "/scratch/vlsi-build/run1" bf = get(node.make.chia_remote( "/home/ray/chipyard", "buildfile", config="RocketConfig", obj_dir=obj_dir, make_vars={"tech_name": "sky130"})) syn = get(node.make.chia_remote( "/home/ray/chipyard", "syn", config="RocketConfig", obj_dir=obj_dir, make_vars={"tech_name": "sky130"})) rpts = get(node.collect.chia_remote( obj_dir, ["syn-rundir/reports/**"])) # same worker, guaranteed """ _MEMBER_FNS = ("make", "collect") _DEFAULT_BUNDLE = {"CPU": 1, "chipyard": 1}
[docs] @staticmethod @ChiaFunction(resources={"chipyard": 1}) def make( chipyard_path: str, target: str, config: str | None = None, obj_dir: str | None = None, make_vars: dict[str, str] | None = None, jobs: int = 1, env: dict[str, str] | None = None, timeout_seconds: int = 86400, ) -> ChipyardHammerResult: """Run one ``make -C <chipyard>/vlsi <target>`` on a worker. Args: chipyard_path: Chipyard checkout root on the worker. target: Any vlsi Makefile target: "buildfile", "syn", "par", "drc", "lvs", "power", "redo-syn", "clean", ... (the flow targets exist after "buildfile" has generated hammer.d). config: Chipyard CONFIG (e.g. "RocketConfig"). Convenience for ``make_vars={"CONFIG": ...}``. obj_dir: Hammer build directory, passed as ``OBJ_DIR=`` and used for ``listing``. When None, chipyard derives its default (``vlsi/build/<long_name>-<TOP>``) and ``listing`` is empty. Pass the same value across buildfile/syn/par calls. make_vars: Any other vlsi Makefile variables, e.g. ``{"tech_name": "sky130", "VLSI_TOP": "ChipTop", "INPUT_CONFS": "a.yml b.yml", "HAMMER_EXTRA_ARGS": "-p x.yml"}``. Appended last, so they win over ``config``/``obj_dir``. jobs: make -j level (elaboration in "buildfile" benefits; the hammer flow targets manage their own parallelism). env: Extra environment variables layered over the worker's (assumed chipyard-ready) environment. timeout_seconds: Wall-clock limit for the subprocess. """ vlsi_dir = os.path.join(os.path.abspath(chipyard_path), "vlsi") if obj_dir is not None: obj_dir = os.path.abspath(obj_dir) cmd = ["make", "-C", vlsi_dir, f"-j{jobs}"] if config is not None: cmd.append(f"CONFIG={config}") if obj_dir is not None: cmd.append(f"OBJ_DIR={obj_dir}") for key, value in (make_vars or {}).items(): cmd.append(f"{key}={value}") cmd.append(target) logger.info(f"Running: {' '.join(cmd)}") # start_new_session puts the whole make/sbt/tool tree in one process # group; chia's pid_registry tracks the pgid so chia_cancel() can # kill it. proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True, env={**os.environ, **env} if env else None, ) try: stdout, stderr = proc.communicate(timeout=timeout_seconds) except subprocess.TimeoutExpired: os.killpg(proc.pid, signal.SIGKILL) stdout, stderr = proc.communicate() stderr += f"\nmake {target} timed out after {timeout_seconds}s" logger.error(f"make {target} timed out after {timeout_seconds}s") if proc.returncode != 0: logger.error(f"make {target} failed (rc={proc.returncode}); " f"stderr tail: {stderr[-500:] if stderr else '(empty)'}") return ChipyardHammerResult( success=proc.returncode == 0, returncode=proc.returncode, target=target, vlsi_dir=vlsi_dir, obj_dir=obj_dir, stdout=stdout, stderr=stderr, listing=_list_files(obj_dir) if obj_dir is not None else {}, )
[docs] @staticmethod @ChiaFunction(resources={"chipyard": 1}) def collect( base_dir: str, patterns: list[str], max_bytes_per_file: int | None = None, ) -> ChipyardHammerCollectResult: """Fetch text files from a previous make's OBJ_DIR (or any directory) on this worker. Dispatch via the pinned instance member (``node.collect.chia_remote``) so it lands on the worker that owns the files — an unpinned call may not. Args: base_dir: Directory a previous target ran in (typically the ``obj_dir`` passed to :meth:`make`). patterns: Globs relative to base_dir (``**`` is recursive), e.g. ``["syn-rundir/reports/**", "*.log"]``. Files matched by multiple patterns appear once. max_bytes_per_file: When set, files over this size are recorded in ``skipped`` instead of shipped through the object store — protects against a glob accidentally matching a netlist. ``None`` (and 0, the falsy edge) means no cap: everything matched is shipped. """ base_dir = os.path.abspath(base_dir) files: dict[str, str] = {} skipped: dict[str, int] = {} for pattern in patterns: for path in _glob.glob(os.path.join(base_dir, pattern), recursive=True): if not os.path.isfile(path): continue rel = os.path.relpath(path, base_dir) if rel in files or rel in skipped: continue try: size = os.path.getsize(path) except OSError: continue if max_bytes_per_file and (size > max_bytes_per_file): skipped[rel] = size continue with open(path, errors="replace") as f: files[rel] = f.read() if skipped: logger.warning( f"chipyard hammer collect skipped {len(skipped)} file(s) over " f"{max_bytes_per_file} bytes: {sorted(skipped)[:5]}" ) return ChipyardHammerCollectResult( base_dir=base_dir, files=files, skipped=skipped, listing=_list_files(base_dir), )