Skip to content
Open
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
60 changes: 43 additions & 17 deletions src/coffe/legacy_tran_sizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -989,13 +989,25 @@ def erf_inverter(sp_path,
inv_trise_str = spice_meas["meas_" + inv_name + "_trise"][0]

# Check if the HSPICE measurement failed. If it did, this might mean that the level
# restorers are too strong which messes up one of the transitions. Making the gate
# length for the level restorers larger could solve this problem.
# Note that it's also possible that something else is causing the failure...
# restorers are too strong which messes up one of the transitions, or that the
# inverter is sized too small to drive its load within the simulation window.
# Instead of aborting the whole run, skip ERF for this inverter and leave it at the
# symmetric (un-balanced) size. The combo will be naturally penalized in cost
# evaluation because run_combo / erf_combo also handle "failed" gracefully below.
if inv_tfall_str == "failed" or inv_trise_str == "failed":
print("ERROR: HSPICE measurement failed.")
print("Consider increasing level-restorers gate length by increasing the 'rest_length_factor' parameter in the input file.")
exit(1)
print("WARNING: HSPICE measurement failed for " + inv_name + " at N=P=" + str(inv_size) + ".")
print(" This usually means the inverter is too weak to drive its load,")
print(" or that level-restorers are too strong (try increasing 'rest_length_factor').")
print(" Leaving " + inv_name + " un-ERFed; this combo will lose to others via cost ranking.")
# Keep fpga_inst.transistor_sizes consistent with parameter_dict (already at N=P=inv_size).
if not fpga_inst.specs.use_finfet:
fpga_inst.transistor_sizes[nmos_name] = inv_size / fpga_inst.specs.min_tran_width
fpga_inst.transistor_sizes[pmos_name] = inv_size / fpga_inst.specs.min_tran_width
else:
fpga_inst.transistor_sizes[nmos_name] = inv_size
fpga_inst.transistor_sizes[pmos_name] = inv_size
sys.stdout.flush()
return

inv_tfall = float(inv_tfall_str)
inv_trise = float(inv_trise_str)
Expand Down Expand Up @@ -1115,10 +1127,20 @@ def erf(sp_path,
if not circuit_element.startswith("inv_"):
continue

# Get the tfall and trise delays for the inverter from the spice measurements
tfall = float(spice_meas["meas_" + circuit_element + "_tfall"][0])
trise = float(spice_meas["meas_" + circuit_element + "_trise"][0])
erf_error = abs((tfall - trise)/tfall)
# Get the tfall and trise delays for the inverter from the spice measurements.
# A "failed" value means HSPICE could not measure the .MEAS condition (signal did
# not cross supply_v/2 in time). Treat as a large delay so the ERF loop bails out
# via ERF_MAX_ITERATIONS rather than crashing in float().
tfall_str = spice_meas["meas_" + circuit_element + "_tfall"][0]
trise_str = spice_meas["meas_" + circuit_element + "_trise"][0]
if tfall_str == "failed" or trise_str == "failed":
tfall = 1.0
trise = 1.0
erf_error = 1.0
else:
tfall = float(tfall_str)
trise = float(trise_str)
erf_error = abs((tfall - trise)/tfall)
Comment on lines -1118 to +1143

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this change, need to add a warning message saying a sim failed, or else this silent failure may cause confusion


if not fpga_inst.specs.use_finfet :
nmos_nm_size = int(parameter_dict[circuit_element + "_nmos"][0]/1e-9)
Expand Down Expand Up @@ -1223,14 +1245,18 @@ def run_combo(fpga_inst, sp_path, element_names, combo, erf_ratios, spice_interf
parameter_dict[wire_name + "_cap"] = [rc_data[1]*1e-15]

# Run HSPICE with the current transistor size
spice_meas = spice_interface.run(sp_path, parameter_dict)
spice_meas = spice_interface.run(sp_path, parameter_dict)

# run returns a dict of measurements. For each key, we have a list of meaasurements.
# That;s why we add the [0]
# Extract total delay from measurements. A "failed" string means HSPICE could not
# measure the .MEAS condition; treat as 1.0 so the cost function deprioritizes this
# combo instead of crashing.
tfall_str = spice_meas["meas_total_tfall"][0]
trise_str = spice_meas["meas_total_trise"][0]
tfall = 1.0 if tfall_str == "failed" else float(tfall_str)
trise = 1.0 if trise_str == "failed" else float(trise_str)

# run returns a dict of measurements. For each key, we have a list of meaasurements.
# That;s why we add the [0]
# Extract total delay from measurements
tfall = float(spice_meas["meas_total_tfall"][0])
trise = float(spice_meas["meas_total_trise"][0])

return tfall, trise


Expand Down
2 changes: 1 addition & 1 deletion src/coffe/tran_sizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2672,7 +2672,7 @@ def size_bram_ckt(

past_cost = current_cost
current_cost = cost_lib.cost_function(
cost_lib.get_eval_area(fpga_inst, "global", fpga_inst.cb_mux, 1, 0),
cost_lib.get_eval_area(fpga_inst, "global", None, 1, 0),
get_current_delay(fpga_inst, 1),
area_opt_weight,
delay_opt_weight
Expand Down
28 changes: 28 additions & 0 deletions src/common/spice_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,34 @@ def init_atomic_libs() -> Dict[str, rg_ds.SpSubCkt]:
"PSEO" : f"{pfet_width_param} * (min_tran_width + 2 * trans_diffusion_length)",
"PDEO" : f"{pfet_width_param} * (min_tran_width + 2 * trans_diffusion_length)",
}
),
"nmos_lp_mtj" : rg_ds.SpSubCkt(
name = "nmos_lp_mtj",
element = "mnfet",
ports = mfet_ports,
params = {
"L" : "gate_length",
"M" : "1",
"nfin" : f"{nfet_width_param}",
"ASEO" : f"{nfet_width_param} * min_tran_width * trans_diffusion_length",
"ADEO" : f"{nfet_width_param} * min_tran_width * trans_diffusion_length",
"PSEO" : f"{nfet_width_param} * (min_tran_width + 2 * trans_diffusion_length)",
"PDEO" : f"{nfet_width_param} * (min_tran_width + 2 * trans_diffusion_length)",
}
),
"pmos_lp_mtj" : rg_ds.SpSubCkt(
name = "pmos_lp_mtj",
element = "mpfet",
ports = mfet_ports,
params = {
"L" : "gate_length",
"M" : "1",
"nfin" : f"{pfet_width_param}",
"ASEO" : f"{pfet_width_param} * min_tran_width * trans_diffusion_length",
"ADEO" : f"{pfet_width_param} * min_tran_width * trans_diffusion_length",
"PSEO" : f"{pfet_width_param} * (min_tran_width + 2 * trans_diffusion_length)",
"PDEO" : f"{pfet_width_param} * (min_tran_width + 2 * trans_diffusion_length)",
}
)
}
return sp_subckt_atomic_lib
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def pytest_collection_modifyitems(session: pytest.Session, config: pytest.Config
data['originalname'] = item.originalname
data['file'] = item.location[0]
data['markers'] = [marker.name for marker in item.own_markers]
data['fixturenames'] = list(getattr(item, 'fixturenames', []) or [])
data_list.append(data)
print(json.dumps(data_list))
# Remove all items (we don't want to execute the tests)
Expand Down
163 changes: 162 additions & 1 deletion tests/test_stratix_iv.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,169 @@ def test_stratix_iv_bram_passthrough(stratix_iv_bram_passthrough_tb: rg_ds.RadGe
def test_stratix_iv_bram(stratix_iv_bram_tb, request: pytest.FixtureRequest):
rg_args = copy.deepcopy(stratix_iv_bram_tb)
ret_val: Any = tests_common.run_rad_gen(
rg_args,
rg_args,
tests_common.get_rg_home(),
)


# ---------------------------------------------------------------------------
# Parametrized BRAM unit tests (22nm process)
# ---------------------------------------------------------------------------
# Each entry: (yaml_basename, label_for_obj_dir, pytest_markers)
# These mirror the BRAM configurations from legacy COFFE's input_files/BRAM/
# (Chiasson/Betz, FPT 2013) and exercise different paths through the BRAM
# generation code: varying memory geometry, SRAM vs MTJ technology, and
# pass_transistor vs transmission_gate switches.
# Parameter tuples: (yaml_basename, label_for_obj_dir)
# Each entry is wrapped in pytest.param() so we can attach distinct markers.
# Two case lists:
# * _BRAM_22NM_PASSTHROUGH_CASES — exercises the full COFFE BRAM netlist
# generation path in pass-through mode (slower, ~3-4 min per case).
# * _BRAM_22NM_CONF_INIT_CASES — just parses the yaml + initializes the
# Coffe dataclass (sub-second per case). Includes one extra case
# (pass_transistor switch) that the passthrough flow can't currently
# handle due to an unrelated assertion in src/coffe/mux.py:209.
# The MTJ case carries an extra `slow` marker because the MTJ-specific
# sub-circuits dominate sizing-iteration time; users can deselect with
# `pytest -m "not slow"`.
_BRAM_22NM_PASSTHROUGH_CASES = [
pytest.param(
("stratix_iv_rrg_bram_ram32.yml", "ram32_sram_tgate"),
marks = [pytest.mark.bram],
id = "ram32_sram_tgate",
),
pytest.param(
("stratix_iv_rrg_bram_ram64.yml", "ram64_sram_tgate"),
marks = [pytest.mark.bram],
id = "ram64_sram_tgate",
),
pytest.param(
("stratix_iv_rrg_bram_ram128.yml", "ram128_sram_tgate"),
marks = [pytest.mark.bram],
id = "ram128_sram_tgate",
),
pytest.param(
("stratix_iv_rrg_bram_mtj32.yml", "mtj32_tgate"),
marks = [pytest.mark.bram, pytest.mark.mtj, pytest.mark.slow],
id = "mtj32_tgate",
),
]

_BRAM_22NM_CONF_INIT_CASES = _BRAM_22NM_PASSTHROUGH_CASES + [
pytest.param(
("stratix_iv_rrg_bram_pt_switch.yml", "ram32_sram_ptran"),
marks = [pytest.mark.bram],
id = "ram32_sram_ptran",
),
]


@pytest.fixture
def stratix_iv_bram_22nm_tb(stratix_iv, request: pytest.FixtureRequest) -> rg_ds.RadGenArgs:
"""
Parametrized fixture that points the stratix_iv driver at one of the
22nm BRAM yaml configs and gives it a unique output directory.

Indirect-parametrize this fixture with a (yaml_basename, label) tuple.
"""
yaml_basename, label = request.param

tests_tree: rg_ds.Tree
tests_tree, test_grp_name, test_name, test_out_dpath, rg_home = tests_common.get_test_info()
cur_test_input_dpath: str = tests_tree.search_subtrees(
f"tests.data.{test_grp_name}.inputs",
is_hier_tag = True,
)[0].path

bram_yaml_fpath: str = os.path.join(cur_test_input_dpath, yaml_basename)
assert os.path.exists(bram_yaml_fpath), f"BRAM yaml path {bram_yaml_fpath} does not exist"

rg_args: rg_ds.RadGenArgs = copy.deepcopy(stratix_iv)
rg_args.subtool_args.fpga_arch_conf_path = bram_yaml_fpath
# pass_through skips the actual hspice simulations during sizing but still
# exercises the full COFFE BRAM netlist generation path. Combined with the
# inherited max_iterations = 1 this keeps each test to a single sizing pass.
rg_args.subtool_args.pass_through = True
rg_args.manual_obj_dir = os.path.join(
tests_tree.search_subtrees(f"tests.data.{test_grp_name}.outputs", is_hier_tag = True)[0].path,
f"stratix_iv_rrg_bram_22nm_{label}_passthrough_debug",
)
tests_common.write_fixture_json(rg_args)

return rg_args


@pytest.mark.stratix_iv
@pytest.mark.custom_fpga
@pytest.mark.parametrize(
"stratix_iv_bram_22nm_tb",
_BRAM_22NM_PASSTHROUGH_CASES,
indirect = True,
)
@skip_if_fixtures_only
def test_stratix_iv_bram_22nm_passthrough(
stratix_iv_bram_22nm_tb: rg_ds.RadGenArgs, request: pytest.FixtureRequest
):
"""
Runs the COFFE BRAM flow end-to-end in pass-through mode for each
legacy COFFE BRAM configuration at 22nm.

Pass-through skips the actual hspice simulations but still runs:
- YAML config parsing & arch_params validation
- FPGA + BRAM subcircuit object instantiation
- SPICE netlist generation for every BRAM subcircuit (memory cell,
row/col/conf decoders, sense amp, write driver, precharge,
wordline driver, RAM local mux, output crossbar, etc.)
- VPR architecture file emission with the BRAM block

These together cover the BRAM-specific code paths an
--enable_bram_module=1 run touches.

Note: the MTJ case is marked `slow` because its mtj-specific
sub-circuits add substantially to sizing-iteration time even in
pass-through mode. Deselect with `pytest -m "not slow"`.
"""
rg_args = copy.deepcopy(stratix_iv_bram_22nm_tb)
tests_common.run_rad_gen(
rg_args,
tests_common.get_rg_home(),
)


# Separate conf-init fixture/test: just verifies the BRAM yamls parse into
# valid CoffeArgs / Coffe data structures without invoking the COFFE flow.
@pytest.fixture
def stratix_iv_bram_22nm_conf_init_tb(stratix_iv_bram_22nm_tb) -> rg_ds.RadGenArgs:
rg_args = copy.deepcopy(stratix_iv_bram_22nm_tb)
rg_args.just_config_init = True
rg_args.subtool_args.pass_through = False
return rg_args


@pytest.mark.init
@pytest.mark.custom_fpga
@pytest.mark.parametrize(
"stratix_iv_bram_22nm_tb",
_BRAM_22NM_CONF_INIT_CASES,
indirect = True,
)
@skip_if_fixtures_only
def test_stratix_iv_bram_22nm_conf_init(
stratix_iv_bram_22nm_conf_init_tb: rg_ds.RadGenArgs, request: pytest.FixtureRequest
):
"""
Fast smoke test: confirms each BRAM yaml can be parsed and yields a
valid rad_gen Coffe configuration without running the rest of the flow.
"""
rg_args = copy.deepcopy(stratix_iv_bram_22nm_conf_init_tb)
rg_info, _ = tests_common.run_rad_gen(
rg_args,
tests_common.get_rg_home(),
)
# Basic sanity: the parsed config should have BRAM enabled and gate_length 22nm.
coffe_struct = rg_info["coffe"]
arch_params = coffe_struct.fpga_arch_conf["fpga_arch_params"]
assert arch_params["enable_bram_module"] == 1, "enable_bram_module not set to 1"
assert arch_params["gate_length"] == 22, "gate_length is not 22nm"


Loading