"""Defines the FABulousTileTimingModel class.
It is responsible for extracting timing information for a specific tile in the FABulous
project.
It reads the project files, initializes synthesis-level and physical-level timing models
using the HdlnxTimingModel class, and provides methods to calculate delays for internal
and external PIPs (Programmable Interconnect Points) using either structural or physical
approaches.
"""
import re
from pathlib import Path
from loguru import logger
from fabulous.fabric_cad.timing_model.hdlnx.hdlnx_timing_model import HdlnxTimingModel
from fabulous.fabric_cad.timing_model.models import (
InternalPipCacheEntry,
TimingModelConfig,
TimingModelMode,
TimingModelStaTools,
TimingModelSynthTools,
)
from fabulous.fabric_cad.timing_model.tools.specification import StaTool, SynthTool
from fabulous.fabric_cad.timing_model.tools.sta_tools.opensta import OpenStaTool
from fabulous.fabric_cad.timing_model.tools.synth_tools.yosys import YosysTool
from fabulous.fabric_definition.fabric import Fabric
from fabulous.fabric_definition.supertile import SuperTile
[docs]
class FABulousTileTimingModel:
"""Reads the FABulous project files and extracts timing information.
Initializes the FABulousTileTimingModel with the given configuration and
fabric definition. The configuration object must match the TimingModelConfig
schema defined in the models module.
- It initializes both synthesis-level and physical-level timing models
using the HdlnxTimingModel class.
- It provides methods to calculate delays for internal PIPs
(within the switch matrix) and external PIPs (between the tile and the
next tile) using either structural or physical approaches.
Supported synthesis tools:
- Yosys, keyword: "yosys"
Supported static timing analysis (STA) tools:
- OpenSTA, keyword: "opensta"
Parameters
----------
config : TimingModelConfig
Configuration object for the timing model.
fabric : Fabric
The FABulous fabric object.
tile_name : str | None
The name of the tile for which the timing model is being created.
"""
def __init__(
self, config: TimingModelConfig, fabric: Fabric, tile_name: str | None = None
) -> None:
self.fabric: Fabric = fabric
self.tile_name: str | None = tile_name
# Validate and parse the configuration using the
# TimingModelConfig dataclass.
self.tm_config: TimingModelConfig = config
# Determine if the tile is part of a SuperTile and set
# the unique_tile_name accordingly.
self.is_in_which_super_tile: str | None = None
self.unique_tile_name: str = self.tile_name
self._get_unique_tile_name()
# Verilog files for the tile.
self.verilog_files: list[Path] = None
# Init:
self.hdlnx_tm_synth: HdlnxTimingModel | None = None
self.hdlnx_tm_phys: HdlnxTimingModel | None = None
self._initialize_timing_models()
# Extract switch matrix information
# Check super_tile_type in config to filter the correct switch matrix
self.switch_matrix_hier_path: list[str] | None = None
self.switch_matrix_module_name: list[str] | None = None
self.internal_pips_grouped_by_inst: dict[str, list[str]] | None = None
self.internal_pips: list[str] | None = None
self._extract_switch_matrix_info()
self.internal_pip_cache: dict[str, InternalPipCacheEntry] = {}
logger.info("FABulous Timing Model initialized.")
def _get_project_rtl_files(self) -> None:
"""Find all the Verilog files for the tile in the project directory.
Exclude certain directories that are not relevant for synthesis. If custom
source files are specified in the configuration for this tile, use those
instead.
"""
exclude_dir_patterns: list[str] = ["macro", "user_design", "Test"]
self.verilog_files: list[Path] = self._find_matching_files(
self.tm_config.project_dir, r".*\.v$", exclude_dir_patterns
)
# Optionally override the default project Verilog files with custom ones
# specified for this tile. Only simple wildcard patterns are supported,
# e.g. "TileA/*.v" to include all Verilog files in the TileA directory.
if (
self.tm_config.custom_per_tile_source_files is not None
and self.unique_tile_name in self.tm_config.custom_per_tile_source_files
and (
rtl := self.tm_config.custom_per_tile_source_files[
self.unique_tile_name
].rtl_files
)
):
targets = rtl if isinstance(rtl, list) else [rtl]
result: list[Path] = []
for p in targets:
if "*" in p.name:
result.extend(p.parent.rglob(p.name))
continue
result.append(p)
self.verilog_files = result
logger.info(
f"Using RTL Verilog files for tile {self.unique_tile_name}:"
f"{'\n '.join(map(str, [''] + self.verilog_files))}"
)
def _get_unique_tile_name(self) -> None:
"""Determine if the tile is part of a SuperTile.
Sets the unique_tile_name accordingly.
- If the tile is found within a SuperTile, set unique_tile_name to the name of
that SuperTile and is_in_which_super_tile to the same name.
- If the tile is not found within any SuperTile, unique_tile_name remains as
the original tile name and is_in_which_super_tile remains None.
- This is necessary because the timing model needs to use the unique tile name
(which is the SuperTile name if the tile is part of a SuperTile) to find
the correct Verilog files and switch matrix information for the tile.
The original tile name is used for other purposes within the timing model.
"""
for unique_tiles in self.fabric.get_all_unique_tiles():
if isinstance(unique_tiles, SuperTile):
for composed_tile in unique_tiles.tiles:
if composed_tile.name == self.tile_name:
self.is_in_which_super_tile = unique_tiles.name
self.unique_tile_name = unique_tiles.name
break
def _cad_tools(self) -> dict[str, SynthTool | StaTool]:
"""Set up the synthesis and STA tools based on the configuration.
This method can be used to initialize the tools before creating
the timing models. New tools can be added here by extending the
match-case statements for synthesis and STA
tools.
Returns
-------
dict[str, SynthTool | StaTool]
A dictionary containing the synthesis and STA tools.
Raises
------
ValueError
If the synthesis or STA tool specified in the
configuration is not supported.
"""
synth_tool: SynthTool | None = None
sta_tool: StaTool | None = None
# Use match-case to select the synthesis and STA tools based on the
# configuration.
match self.tm_config.synth_program:
case TimingModelSynthTools.YOSYS:
synth_tool = YosysTool(
verilog_files=self.verilog_files,
liberty_files=self.tm_config.liberty_files,
top_name=self.unique_tile_name,
synth_executable=self.tm_config.synth_executable,
techmap_files=self.tm_config.techmap_files,
tiehi_cell_and_port=self.tm_config.tiehi_cell_and_port,
tielo_cell_and_port=self.tm_config.tielo_cell_and_port,
min_buf_cell_and_ports=self.tm_config.min_buf_cell_and_ports,
is_gate_level=False,
debug=self.tm_config.debug,
flat=False,
)
case _:
raise ValueError(
f"Unsupported synthesis tool: {self.tm_config.synth_program}"
)
# Use match-case to select the STA tool based on the configuration.
match self.tm_config.sta_program:
case TimingModelStaTools.OPENSTA:
sta_tool = OpenStaTool(
sta_executable=self.tm_config.sta_executable,
spef_files=None,
debug=self.tm_config.debug,
)
case _:
raise ValueError(f"Unsupported STA tool: {self.tm_config.sta_program}")
# Return the initialized tools in a dictionary for use in the timing
# model initialization.
return {"synth_tool": synth_tool, "sta_tool": sta_tool}
def _initialize_timing_models(self) -> None:
"""Initialize the synthesis and physical timing models using HdlnxTimingModel.
- The synthesis-level model is initialized with the RTL Verilog files
and the specified synthesis and STA tools.
- The physical-level model is initialized with the gate-level netlist,
and optionally with SPEF files for wire delay if consider_wire_delay
is True in the configuration.
Returns
-------
None
If the mode is STRUCTURAL, only the synthesis-level model is initialized
and the method returns None.
"""
logger.info(f"Initializing FABulous Timing Model for Tile: {self.tile_name}")
logger.info(f" SuperTile: {self.is_in_which_super_tile}")
# Initialize the synthesis-level timing model first, as it is needed
# to extract the switch matrix information and to find the relevant
# Verilog files for the physical-level model.
logger.info("Initializing Synthesis-level timing model...")
# Get the Verilog files for the project.
self._get_project_rtl_files()
# Initialize the synthesis and STA tools based on the configuration.
cad_tool = self._cad_tools()
synth_tool: SynthTool = cad_tool["synth_tool"]
sta_tool: StaTool = cad_tool["sta_tool"]
# Initialize the synthesis-level timing model.
self.hdlnx_tm_synth = HdlnxTimingModel(
sta_tool, synth_tool, self.tm_config.delay_type_str, self.tm_config.debug
)
# If the mode is STRUCTURAL, we only need the synthesis-level model and can skip
# initializing the physical-level model.
if self.tm_config.mode == TimingModelMode.STRUCTURAL:
logger.info(
"Mode is STRUCTURAL, skipping physical-level model initialization."
)
return
# For the physical-level model, we need to switch to the gate-level netlist.
logger.info("Initializing Physical-level timing model...")
# For the physical-level model, we need to switch to the gate-level netlist.
synth_tool.synth_rtl_files = Path(
f"{self.tm_config.project_dir}/Tile/{self.unique_tile_name}"
f"/macro/final_views/nl/{self.unique_tile_name}.nl.v"
)
# Optionally override the default netlist file with a custom one for this tile.
if (
self.tm_config.custom_per_tile_source_files is not None
and self.unique_tile_name in self.tm_config.custom_per_tile_source_files
and (
netl := self.tm_config.custom_per_tile_source_files[
self.unique_tile_name
].netlist_file
)
):
synth_tool.synth_rtl_files = netl
logger.info(
f"Using netlist file for tile {self.unique_tile_name}: "
f"{synth_tool.synth_rtl_files}"
)
# Disable synthesis for the physical-level model since we already
# have the gate-level netlist.
synth_tool.synth_passthrough = True
# Optionally load RC files for wire delay if consider_wire_delay
# is True in the configuration.
if self.tm_config.consider_wire_delay:
sta_tool.sta_rc_files = Path(
f"{self.tm_config.project_dir}/Tile/{self.unique_tile_name}"
f"/macro/final_views/spef/nom/{self.unique_tile_name}.nom.spef"
)
# Optionally override the default RC file with a custom one for this tile.
if (
self.tm_config.custom_per_tile_source_files is not None
and self.unique_tile_name in self.tm_config.custom_per_tile_source_files
and (
rc := self.tm_config.custom_per_tile_source_files[
self.unique_tile_name
].rc_file
)
):
sta_tool.sta_rc_files = rc
logger.info(
f"Use RC file for {self.unique_tile_name}: {sta_tool.sta_rc_files}"
)
# Initialize the physical-level timing model with the gate-level netlist.
self.hdlnx_tm_phys = HdlnxTimingModel(
sta_tool, synth_tool, self.tm_config.delay_type_str, self.tm_config.debug
)
def _find_matching_files(
self,
root_dir: Path,
file_pattern: str,
exclude_dir_patterns: list[str] | None = None,
exclude_file_patterns: list[str] | None = None,
) -> list[Path]:
"""Recursively traverse root_dir and return a list of Path objects.
for all files whose *name* matches file_pattern (regex).
- exclude_dir_patterns: list of regex patterns; directory names
matching any of these will be skipped completely.
- exclude_file_patterns: list of regex patterns; file names
matching any of these will be skipped.
Parameters
----------
root_dir : Path
Root directory to start the search.
file_pattern : str
Regex pattern to match file names.
exclude_dir_patterns : list[str] | None
List of regex patterns to exclude directories.
exclude_file_patterns : list[str] | None
List of regex patterns to exclude files.
Returns
-------
list[Path]
List of Path objects for matched files.
Raises
------
TypeError
If root_dir is not a Path object.
"""
if not isinstance(root_dir, Path):
raise TypeError("root_dir must be a Path object.")
file_re = re.compile(file_pattern)
exclude_dir_res = [re.compile(p) for p in (exclude_dir_patterns or [])]
exclude_file_res = [re.compile(p) for p in (exclude_file_patterns or [])]
matched_files: list[Path] = []
for dirpath, dirnames, filenames in root_dir.walk():
dirnames[:] = [
d for d in dirnames if not any(r.search(d) for r in exclude_dir_res)
]
for fname in filenames:
if any(r.search(fname) for r in exclude_file_res):
continue
if file_re.search(fname):
matched_files.append(dirpath / fname)
return matched_files
def _extract_switch_matrix_info(self) -> None:
"""Extract switch matrix information for the tile.
- Hierarchical path of the switch matrix instance
- Module name of the switch matrix
- Internal PIPs of the switch matrix, grouped by instance
- List of all internal PIPs of the switch matrix
The method uses the synthesis-level timing model to find the relevant
switch matrix instance and module based on regex patterns. It also
checks if the tile is part of a SuperTile to filter the correct switch
matrix information. Finally, it loads the internal PIPs of the switch
matrix for later use in delay calculations.
Raises
------
ValueError
If no switch matrix instance or module is found, or if multiple
instances/modules are found when not expected.
Returns
-------
None
If no switch matrix instance or module is found, a warning is
logged and the method returns None, indicating that all PIPs for
the tile will be considered external.
"""
logger.info("Extracting switch matrix information...")
self.switch_matrix_hier_path = self.hdlnx_tm_synth.find_instance_paths_by_regex(
r".*_switch_matrix$"
)
self.switch_matrix_module_name = self.hdlnx_tm_synth.find_verilog_modules_regex(
r"^[^/]*_switch_matrix$"
)
if (
len(self.switch_matrix_hier_path) == 0
or len(self.switch_matrix_module_name) == 0
):
logger.warning(
f"No switch matrix instance or module found. "
f"All PIPs for {self.tile_name} will be considered external."
)
return
if self.is_in_which_super_tile is None:
if (
len(self.switch_matrix_hier_path) > 1
or len(self.switch_matrix_module_name) > 1
):
raise ValueError(
"Multiple switch matrix instances or modules found "
"for a non-SuperTile."
)
self.switch_matrix_hier_path = self.switch_matrix_hier_path[0]
self.switch_matrix_module_name = self.switch_matrix_module_name[0]
logger.info(
f"Using switch matrix instance: {self.switch_matrix_hier_path}, "
f"module: {self.switch_matrix_module_name}"
)
else:
self.switch_matrix_hier_path = [
p for p in self.switch_matrix_hier_path if self.tile_name in p
]
self.switch_matrix_module_name = [
m for m in self.switch_matrix_module_name if self.tile_name in m
]
if (
len(self.switch_matrix_hier_path) == 0
or len(self.switch_matrix_module_name) == 0
):
raise ValueError(
f"No switch matrix instance or module found for SuperTile "
f"{self.unique_tile_name}"
)
if (
len(self.switch_matrix_hier_path) > 1
or len(self.switch_matrix_module_name) > 1
):
raise ValueError(
f"Multiple switch matrix instances or modules found Tile "
f"{self.tile_name} in SuperTile {self.unique_tile_name}."
)
self.switch_matrix_hier_path = self.switch_matrix_hier_path[0]
self.switch_matrix_module_name = self.switch_matrix_module_name[0]
logger.info(
f"Tile {self.tile_name} is part of super tile {self.unique_tile_name}."
)
logger.info("Loading internal PIPs...")
self.internal_pips_grouped_by_inst = (
self.hdlnx_tm_synth.get_module_instance_nets(self.switch_matrix_module_name)
)
self.internal_pips = self.hdlnx_tm_synth.get_instance_pins(
self.switch_matrix_hier_path
)
[docs]
def is_tile_internal_pip(self, pip_src: str, pip_dst: str) -> bool:
"""Check if both PIPs are internal PIPs of the switch matrix.
That means the path must be through a switch matrix multiplexer.
Its not a wire delay.
Parameters
----------
pip_src : str
Source PIP port name (e.g., "LB_O").
pip_dst : str
Destination PIP port name (e.g., "JN2BEG3").
Returns
-------
bool
True if both PIPs are internal PIPs of the switch matrix, False otherwise.
"""
instance_to_nets = self.internal_pips_grouped_by_inst
if instance_to_nets is None or pip_src == pip_dst:
return False
target = set([pip_src, pip_dst])
for _inst, net_list in instance_to_nets.items():
if target.issubset(set(net_list)):
return True
return False
[docs]
def internal_pip_delay_structural(self, pip_src: str, pip_dst: str) -> float:
"""Calculate delay between two PIPs in the switch matrix.
It is the fast variant that does not need physical design information,
but the results may be less accurate.
Parameters
----------
pip_src : str
Source PIP port name (e.g., "LB_O").
pip_dst : str
Destination PIP port name (e.g., "JN2BEG3").
Returns
-------
float
Delay in nanoseconds between the two PIPs.
"""
logger.info(
f"Timing extraction for tile: {self.tile_name}, PIP: {pip_src} -> {pip_dst}"
)
synth_model = self.hdlnx_tm_synth
pip_cache: InternalPipCacheEntry = self.internal_pip_cache.get(pip_dst, None)
if pip_cache is not None:
logger.info(
f"Cache hit for internal PIP {pip_src} -> {pip_dst}. "
f"Using cached synthesis-level information."
)
logger.info(
f"Finding synthesis-level switch matrix mux for PIPs {pip_src} -> {pip_dst}"
)
# Are pip_src and pip_dst connected through the same switch matrix multiplexer?
swm_mux_for_pips = (
synth_model.find_instances_paths_with_all_nets(
self.switch_matrix_module_name,
[pip_src, pip_dst],
filter_regex=self.switch_matrix_hier_path,
)
if pip_cache is None
else pip_cache.swm_mux_for_pips
)
if len(swm_mux_for_pips) > 1:
logger.warning(
f"Multiple switch matrix mux instances found "
f"for PIPs {pip_src} -> {pip_dst}. "
f"Using the first one: {swm_mux_for_pips[0]}"
)
logger.info(f" Found switch matrix mux instance: {swm_mux_for_pips[0]}")
# Get the resolved pins for the switch matrix mux instance.
swm_mux_resolved = (
synth_model.net_to_pin_paths_for_instance_resolved(swm_mux_for_pips[0])
if pip_cache is None
else pip_cache.swm_mux_resolved
)
logger.info("Switch matrix mux resolved pins for src and dst:")
logger.info(f" {pip_src}: {swm_mux_resolved[pip_src]}")
logger.info(f" {pip_dst}: {swm_mux_resolved[pip_dst]}")
if len(swm_mux_resolved[pip_src]) > 1:
logger.warning(
f"Multiple resolved pins found for PIP source {pip_src} "
f"in switch matrix mux instance {swm_mux_for_pips[0]}. "
f"Using the first one: {swm_mux_resolved[pip_src][0]}"
)
if len(swm_mux_resolved[pip_dst]) > 1:
logger.warning(
f"Multiple resolved pins found for PIP destination {pip_dst} "
f"in switch matrix mux instance {swm_mux_for_pips[0]}. "
f"Using the first one: {swm_mux_resolved[pip_dst][0]}"
)
logger.info(f"Calculating structural delay from {pip_src} to {pip_dst}")
# Calculate delay between pip_src and pip_dst using the
# synthesis-level timing model.
delay = synth_model.single_delay(
swm_mux_resolved[pip_src][0], swm_mux_resolved[pip_dst][0]
)
logger.info(f"Delay from {pip_src} to {pip_dst}: {delay} ns.")
# Begin ports of the swm mux are unique so we can use pip_dst as
# the key for caching.
self.internal_pip_cache[pip_dst] = InternalPipCacheEntry(
begin_pip=pip_dst,
swm_mux_for_pips=swm_mux_for_pips,
swm_nearest_ports_in=None,
swm_nearest_ports_out=None,
swm_output_pin=None,
swm_mux_resolved=swm_mux_resolved,
)
return delay
[docs]
def internal_pip_delay_physical(self, pip_src: str, pip_dst: str) -> float:
"""Calculate delay between two PIPs using physical design information.
This method uses the physical-level timing model to provide more
accurate delay estimates by considering the actual physical implementation.
Synthesis-level resolution (extract the realted module ports that are
connected to the SMW mux to which the PIP belongs)
Physical-level resolution map the synthesis-level top-level ports that
are related to the swm mux to physical-level swm mux pins to find the sm mux
output pin (Then we can calc the delay between pip_src and pip_dst). To
find the swm mux output we will use a method that we call earliest node
convergence. That means for MUX the topology we know that all inputs
converge to the output pin (mostly), so we can find the earliest common
node from all the input ports found above. Similar to graph betweenness
centrality subset, but here we want to find the node that minimizes the maximum
distance from all the input ports.
Parameters
----------
pip_src : str
Source PIP port name (e.g., "LB_O").
pip_dst : str
Destination PIP port name (e.g., "JN2BEG3").
Returns
-------
float
Delay in nanoseconds between the two PIPs.
"""
logger.info(
f"Timing extraction for tile: {self.tile_name}, PIP: {pip_src} -> {pip_dst}"
)
synth_model: HdlnxTimingModel = self.hdlnx_tm_synth
phys_model: HdlnxTimingModel = self.hdlnx_tm_phys
pip_cache: InternalPipCacheEntry = self.internal_pip_cache.get(pip_dst, None)
# Reference output ports for convergence checks.
ref_output_port: str = None
swm_nearest_ports_out: tuple[dict[str, list[str]], list[str]] | None = None
# Skip algorithms if we have a cache hit for the internal PIP, which means
# we have already resolved the switch matrix mux for a BEG pin.
if pip_cache is not None:
logger.info(
f"Cache hit for internal PIP {pip_src} -> {pip_dst}. "
f"Using cached physical-level information."
)
# ----------------------------#
# Synthesis-level resolution #
# ----------------------------#
logger.info(
f"Finding synthesis-level switch matrix mux for PIPs {pip_src} -> {pip_dst}"
)
# Algorithm_1: Are pip_src and pip_dst connected through the same
# switch matrix multiplexer?
swm_mux_for_pips: list[str] = (
synth_model.find_instances_paths_with_all_nets(
self.switch_matrix_module_name,
[pip_src, pip_dst],
filter_regex=self.switch_matrix_hier_path,
)
if pip_cache is None
else pip_cache.swm_mux_for_pips
)
if len(swm_mux_for_pips) > 1:
logger.warning(
f"Multiple swm mux instances found for PIPs {pip_src} -> {pip_dst}. "
f"Using the first one: {swm_mux_for_pips[0]}"
)
logger.info(f" Found switch matrix mux instance: {swm_mux_for_pips[0]}")
logger.info(
"Finding synthesis-level top-level ports connected "
"to the switch matrix mux nets..."
)
# Algorithm_2: Find the nearest top level ports connected to all the nets
# of the swm mux input pins. We reverse the timing graph to find the
# input ports (towards inputs). Good default value is 4. Fastest is 1.
swm_nearest_ports_in = (
synth_model.nearest_ports_from_instance_pin_nets(
swm_mux_for_pips[0], reverse=True, num_ports=1
)
if pip_cache is None
else pip_cache.swm_nearest_ports_in
)
swm_nearest_in_ports_for_each_swm_wire = swm_nearest_ports_in[0]
swm_nearest_in_ports_all = swm_nearest_ports_in[1]
swm_in_buf = len(swm_nearest_in_ports_all) == 1
# Algorithm_3: Convergence nodes must have a path to the output port of
# the sw mux. So we will use the output port as a sentinel to find the
# convergence node.
if swm_in_buf:
swm_nearest_ports_out = (
synth_model.nearest_ports_from_instance_pin_nets(
swm_mux_for_pips[0], reverse=False, num_ports=1
)
if pip_cache is None
else pip_cache.swm_nearest_ports_out
)
ref_output_port: str = swm_nearest_ports_out[0][f"{pip_dst}"][0]
logger.info("Single input Switch Matrix Mux detected.")
# ---------------------------#
# Physical-level resolution #
# ---------------------------#
logger.info("Starting physical extraction of the switch matrix mux for pips")
# Algorithm_4: Find the converging node (the output pin of the swm mux)
best_nodes, best_cost, dists = (
phys_model.earliest_common_nodes(
sources=swm_nearest_in_ports_all,
mode="max",
sentinel=ref_output_port,
prefer_sentinel_for_single_source=True,
follow_steps_to_sentinel=3,
)
if pip_cache is None
else pip_cache.swm_output_pin
)
swm_phys_output: str = best_nodes[0]
# ------------------------------------------------------------#
# Calculate the delay between the two PIPs at physical level #
# ------------------------------------------------------------#
logger.info(f"Calculating physical delay from {pip_src} to {pip_dst}")
# Algorithm_5: Calculate delay between pip_src and the converged output pin
# We use the 0st nearest port found for pip_src beacuse the list is sorted
# starting from the nearest port.
delay = phys_model.single_delay(
swm_nearest_in_ports_for_each_swm_wire[f"{pip_src}"][0], swm_phys_output
)
logger.info(f"Physical Delay from {pip_src} to {pip_dst}: {delay} ns.")
# Begin ports of the swm mux are unique so we can use pip_dst as the
# key for caching.
self.internal_pip_cache[pip_dst] = InternalPipCacheEntry(
begin_pip=pip_dst,
swm_mux_for_pips=swm_mux_for_pips,
swm_nearest_ports_in=swm_nearest_ports_in,
swm_nearest_ports_out=swm_nearest_ports_out,
swm_output_pin=(best_nodes, best_cost, dists),
swm_mux_resolved=None,
)
return delay
[docs]
def external_pip_delay_structural(self, pip_src: str, pip_dst: str) -> float:
"""Calculate delay for external PIPs between the tile and the next tile.
Structural approach. It is Tile to Tile, Tile port to SWM, SWM to SWM, SWM
output to tile port.
Parameters
----------
pip_src : str
Source PIP port name.
pip_dst : str
Destination PIP port name.
Returns
-------
float
Estimated delay in nanoseconds for the external PIP.
"""
logger.info(
f"Timing extraction for tile: {self.tile_name}, PIP: {pip_src} -> {pip_dst}"
)
logger.info(
f"Calculating structural delay for external PIP from {pip_src} to {pip_dst}"
)
synth_model = self.hdlnx_tm_synth
# In general try to avoid delay of 0.
default_delay: float = 0.001
# Must do for ports with indices, e.g., NN2BEG3 -> NN2BEG[3]
pip_src_port = re.sub(r"^(.*?)(\d+)$", r"\1[\2]", pip_src)
pip_dst_port = re.sub(r"^(.*?)(\d+)$", r"\1[\2]", pip_dst)
# Tile interconnects, stitched fixed delay almost 0.
if pip_src_port in synth_model.output_ports:
logger.info(
f"Tile output {pip_src_port} to next tile input {pip_dst_port} "
f"stitched delay: {default_delay} ns"
)
return default_delay
# Tile input to nearest output (twist to the next tile input)
if pip_src_port in synth_model.input_ports:
out_port_list, out_port = synth_model.path_to_nearest_target_sentinel(
pip_src_port, synth_model.output_ports
)
logger.info(f"Port twist detected for {pip_src_port} to {pip_dst_port}:")
if out_port is None:
logger.warning(
f"No nearest port found for tile input {pip_src_port}. "
f"Using default delay {default_delay} ns"
)
return default_delay
delay = synth_model.single_delay(pip_src_port, out_port)
logger.info(
f"Delay from tile input {pip_src_port} to tile output "
f"{out_port}--{pip_dst_port}: {delay} ns."
)
return delay
# SWM output to the next SWM input
pip_cache: InternalPipCacheEntry = self.internal_pip_cache.get(pip_src, None)
if pip_cache is None:
logger.info(
f"SWMy output {pip_src} to next SWMy input {pip_dst} directly "
f"connected delay: {default_delay} ns"
)
return default_delay
# We just follow the wire to next swm input pin, also to maybe catch
# a buffer in between.
swm_output_pin: str = pip_cache.swm_mux_resolved[pip_src][0]
swm_next_input_pin: str = synth_model.follow_first_fanout_from_pins(
hier_pin_path=swm_output_pin, num_follow=2
)
delay = synth_model.single_delay(swm_output_pin, swm_next_input_pin)
logger.info(
f"SWMx output {pip_src} to next SWMx input {pip_dst} with delay: {delay} ns"
)
if delay < 1e-5:
return default_delay
return delay
[docs]
def external_pip_delay_physical(self, pip_src: str, pip_dst: str) -> float:
"""Calculate delay for external PIPs between the tile and the next tile.
Physical approach. It is Tile to Tile, Tile port to SWM, SWM to SWM, SWM output
to tile port. This method uses the physical-level timing model to provide more
accurate delay estimates by considering the actual physical implementation. For
tile interconnects, we assume a stitched connection with a fixed small delay.
Parameters
----------
pip_src : str
Source PIP port name.
pip_dst : str
Destination PIP port name.
Returns
-------
float
Estimated delay in nanoseconds for the external PIP.
"""
logger.info(
f"Timing extraction for tile: {self.tile_name}, PIP: {pip_src} -> {pip_dst}"
)
logger.info(
f"Calculating physical delay for external PIP from {pip_src} to {pip_dst}"
)
phys_model = self.hdlnx_tm_phys
default_delay: float = 0.001
# Must do for ports with indices, e.g., NN2BEG3 -> NN2BEG[3]
pip_src_port = re.sub(r"^(.*?)(\d+)$", r"\1[\2]", pip_src)
pip_dst_port = re.sub(r"^(.*?)(\d+)$", r"\1[\2]", pip_dst)
# Tile interconnects, stitched fixed delay almost 0.
if pip_src_port in phys_model.output_ports:
logger.info(
f"Tile output {pip_src_port} to next tile input {pip_dst_port} "
f"stitched delay: {default_delay} ns"
)
return default_delay
# Tile input to nearest output (twist to the next tile input)
if pip_src_port in phys_model.input_ports:
out_port_list, out_port = phys_model.path_to_nearest_target_sentinel(
pip_src_port, phys_model.output_ports
)
logger.info(f"Port twist detected for {pip_src_port} to {pip_dst_port}:")
if out_port is None:
logger.warning(
f"No nearest port found for tile input {pip_src_port}. "
f"Using default delay {default_delay} ns"
)
return default_delay
delay = phys_model.single_delay(pip_src_port, out_port)
logger.info(
f"Delay from tile input {pip_src_port} to tile output "
f"{out_port}--{pip_dst_port}: {delay} ns."
)
return delay
# SWM output to the next SWM input
pip_cache: InternalPipCacheEntry = self.internal_pip_cache.get(pip_src, None)
if pip_cache is None:
logger.info(
f"SWMy output {pip_src} to next SWMy input {pip_dst} directly "
f"connected delay: {default_delay} ns"
)
return default_delay
# We just follow the wire to next swm input pin, also to maybe catch
# a buffer in between.
swm_output_pin: str = pip_cache.swm_output_pin[0][0]
swm_next_input_pin: str = phys_model.follow_first_fanout_from_pins(
hier_pin_path=swm_output_pin, num_follow=2
)
delay = phys_model.single_delay(swm_output_pin, swm_next_input_pin)
logger.info(
f"SWMx output {pip_src} to next SWMx input {pip_dst} with delay: {delay} ns"
)
if delay < 1e-5:
return default_delay
return delay
[docs]
def internal_pip_delay(self, pip_src: str, pip_dst: str) -> float:
"""Choose the method to calculate internal PIP delay based on the mode.
(physical or structural).
Parameters
----------
pip_src : str
Source PIP port name.
pip_dst : str
Destination PIP port name.
Returns
-------
float
Calculated delay in nanoseconds for the internal PIP.
"""
if self.tm_config.mode == TimingModelMode.PHYSICAL:
return self.internal_pip_delay_physical(pip_src, pip_dst)
return self.internal_pip_delay_structural(pip_src, pip_dst)
[docs]
def external_pip_delay(self, pip_src: str, pip_dst: str) -> float:
"""Choose the method to calculate external PIP delay based on the mode.
(physical or structural).
Parameters
----------
pip_src : str
Source PIP port name.
pip_dst : str
Destination PIP port name.
Returns
-------
float
Calculated delay in nanoseconds for the external PIP.
"""
if self.tm_config.mode == TimingModelMode.PHYSICAL:
return self.external_pip_delay_physical(pip_src, pip_dst)
return self.external_pip_delay_structural(pip_src, pip_dst)
[docs]
def pip_delay(self, pip_src: str, pip_dst: str) -> float:
"""Calculate the delay for a PIP, choosing between internal and external.
Parameters
----------
pip_src : str
Source PIP port name.
pip_dst : str
Destination PIP port name.
Returns
-------
float
Calculated delay in nanoseconds for the PIP.
"""
d_scale: float = self.tm_config.delay_scaling_factor
def _round(x: float) -> float:
return round(x * d_scale, 3)
if self.is_tile_internal_pip(pip_src, pip_dst):
return _round(self.internal_pip_delay(pip_src, pip_dst))
return _round(self.external_pip_delay(pip_src, pip_dst))