diff --git a/km1_kKaHyPar_sea20.ini b/km1_kKaHyPar_sea20.ini new file mode 100644 index 0000000..8d66737 --- /dev/null +++ b/km1_kKaHyPar_sea20.ini @@ -0,0 +1,74 @@ +# general +mode=direct +objective=km1 +seed=-1 +cmaxnet=1000 +vcycles=0 +# main -> preprocessing -> min hash sparsifier +p-use-sparsifier=true +p-sparsifier-min-median-he-size=28 +p-sparsifier-max-hyperedge-size=1200 +p-sparsifier-max-cluster-size=10 +p-sparsifier-min-cluster-size=2 +p-sparsifier-num-hash-func=5 +p-sparsifier-combined-num-hash-func=100 +# main -> preprocessing -> community detection +p-detect-communities=true +p-detect-communities-in-ip=true +p-reuse-communities=false +p-max-louvain-pass-iterations=100 +p-min-eps-improvement=0.0001 +p-louvain-edge-weight=hybrid +p-large-he-threshold=1000 +# main -> preprocessing -> large he removal +p-smallest-maxnet-threshold=50000 +p-maxnet-removal-factor=0.01 +# main -> coarsening +c-type=ml_style +c-s=1 +c-t=160 +# main -> coarsening -> rating +c-rating-score=heavy_edge +c-rating-use-communities=true +c-rating-heavy_node_penalty=no_penalty +c-rating-acceptance-criterion=best_prefer_unmatched +c-fixed-vertex-acceptance-criterion=fixed_vertex_allowed +# main -> initial partitioning +i-mode=recursive +i-technique=multi +# initial partitioning -> coarsening +i-c-type=ml_style +i-c-s=1 +i-c-t=150 +# initial partitioning -> coarsening -> rating +i-c-rating-score=heavy_edge +i-c-rating-use-communities=true +i-c-rating-heavy_node_penalty=no_penalty +i-c-rating-acceptance-criterion=best_prefer_unmatched +i-c-fixed-vertex-acceptance-criterion=fixed_vertex_allowed +# initial partitioning -> initial partitioning +i-algo=pool +i-runs=20 +# initial partitioning -> bin packing +i-bp-algorithm=worst_fit +i-bp-heuristic-prepacking=false +i-bp-early-restart=true +i-bp-late-restart=true +# initial partitioning -> local search +i-r-type=twoway_fm +i-r-runs=-1 +i-r-fm-stop=simple +i-r-fm-stop-i=50 +# main -> local search +r-type=kway_fm_hyperflow_cutter_km1 +r-runs=-1 +r-fm-stop=adaptive_opt +r-fm-stop-alpha=1 +r-fm-stop-i=350 +# local_search -> flow scheduling and heuristics +r-flow-execution-policy=exponential +# local_search -> hyperflowcutter configuration +r-hfc-size-constraint=mf-style +r-hfc-scaling=16 +r-hfc-distance-based-piercing=true +r-hfc-mbc=true diff --git a/kyupy b/kyupy index ed9b5c6..f0c1063 160000 --- a/kyupy +++ b/kyupy @@ -1 +1 @@ -Subproject commit ed9b5c6cd67e78876c105643cd107ca9f32dcee2 +Subproject commit f0c10633bf1d4a2dbef7d83e494386b87ad93c07 diff --git a/main.py b/main.py index 3014e62..6e74ad7 100755 --- a/main.py +++ b/main.py @@ -2,14 +2,13 @@ import argparse import subprocess from pathlib import Path -import time import numpy as np from kyupy import verilog, bench, log, logic, batchrange, atalanta, stil from kyupy.techlib import techlib_by_name, KYUPY -from fsim.static import LineRoles, FaultSet +from fsim.static import LineRoles, FaultSet, CircuitPartition from fsim.simple import SAFSimSimple from fsim.incremental import SAFSimIncremental from fsim.ppsfp import SAFSimPPSFP @@ -23,7 +22,8 @@ def main(): parser.add_argument('-t', '--tlib', default='SKY130', help=f'Techlib for verilog circuit. Default: SKY130, available: {sorted(techlib_by_name.keys())}.') parser.add_argument('-p', '--patterns', default='1024', help='Pattern file or number of random patterns to simulate. Default: 1024.') parser.add_argument('-a', '--algorithm', default='simple', help=f'Fault simulation algorithm. Default: simple, available: {sorted(algorithms.keys())}.') - parser.add_argument('-o', '--output', default=None, help='') + parser.add_argument('-o', '--output', default=None, help='Write fault list to given file') + parser.add_argument('-k', '--partitions', default=1, help='Partition circuit into partitions') parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility. Default: 42.') parser.add_argument('circuit', help='Gate-level verilog, bench, or nix package to import. See available packages: "nix flake show github:s-holst/benchmark-circuits".') args = parser.parse_args() @@ -43,9 +43,7 @@ def main(): stats = {k.replace('__',''): v for k, v in c.stats(args.tlib).items() if k.startswith('__') or k.endswith('put')} log.info(f'circuit {stats=}') - - lr = LineRoles(c, args.tlib) - log.info(f'line role stats={lr.stats}') + log.info(f'line role stats={LineRoles(c, args.tlib).stats}') c_resolved = c.copy() c_resolved.resolve_tlib_cells(args.tlib) @@ -55,13 +53,8 @@ def main(): log.info(f'uncollapsed stuck-at fault count: {len(fs.saf_set)}') log.info(f'collapsed stuck-at fault count: {len(fs.saf_equiv_classes)}') - ffr_stems = [] - for stem, _ in c_resolved.fanout_free_regions(KYUPY): - if len(stem.outs) > 0 and stem.outs[0] is not None: - ffr_stems.append(stem.outs[0]) - ffr_stems = np.array(ffr_stems, dtype=np.uint32) - - log.info(f'FFR count: {len(ffr_stems)}') + partitions = CircuitPartition(c_resolved, int(args.partitions)) + partitions.print_stats() rng = np.random.default_rng(args.seed) diff --git a/src/fsim/ppsfp.py b/src/fsim/ppsfp.py index 8e19881..5aca9d3 100644 --- a/src/fsim/ppsfp.py +++ b/src/fsim/ppsfp.py @@ -7,6 +7,31 @@ from kyupy import log, batchrange, cdiv, Timers, logic from kyupy.circuit import Circuit, Node from kyupy.logic_sim import LogicSim2V from kyupy.techlib import KYUPY +from kyupy.logic_sim import LogicSim2V, c_prop_2v_cpu + + +class LogicSim2VCone: + def __init__(self, base_sim: LogicSim2V, origin_lines: set[int]): + self.sims = base_sim.sims + + self.ops = base_sim.cone_ops(origin_lines) + self.c_locs = base_sim.c_locs + self.c_len = len(self.c_locs) + + self.c = base_sim.c.copy() + self.c_dirty = np.full(self.c_len, 1, dtype=np.uint8) + self._full_mask = base_sim._full_mask + + def c_prop(self, fault_line=-1, fault_model=2, fault_mask=None): + if fault_mask is None: + fault_mask = self._full_mask # default: full mask + else: + if len(fault_mask) < self.c.shape[-1]: # pad mask with 0's if necessary + fault_mask2 = np.full(self.c.shape[-1], 0, dtype=np.uint8) + fault_mask2[:len(fault_mask)] = fault_mask + fault_mask = fault_mask2 + c_prop_2v_cpu(self.ops, self.c_locs, self.c, self.c_dirty, int(fault_line), fault_mask, int(fault_model)) + class SAFSimPPSFP: """A stuck-at fault simulator that uses explicit simulations only at roots of fan-out free regions. @@ -83,12 +108,15 @@ class SAFSimPPSFP: c_golden = np.zeros_like(self.sim.c) fclass_DS = set() nbatches = cdiv(patterns.shape[1], self.sim.sims) - nfaults = len(faults) ffr_obs = np.zeros((len(self.ffr_stem2idx), self.sim.c.shape[2]), dtype=np.uint8) - ffr_obs_valid = np.zeros(len(self.ffr_stem2idx), dtype=np.uint8) obs_mask = np.packbits(np.full(patterns.shape[1], 1, dtype=np.uint8), bitorder='little') fault_to_stem_obs = obs_mask.copy() fault_act = obs_mask.copy() + # collect necessary stems + ffr_stems = set() + for fault in faults: + fault_site = fault//2 + ffr_stems.add(self.lines2ffr_stem[fault_site]) with self.timers['sim'], log.progress() as p: for bidx, (bo, bs) in enumerate(batchrange(patterns.shape[1], self.sim.sims)): @@ -99,29 +127,30 @@ class SAFSimPPSFP: self.sim.c_prop() c_golden[...] = self.sim.c c_golden_poppo = c_golden[self.sim.poppo_c_locs] - ffr_obs_valid[...] = 0 - for fidx, fault in enumerate(faults): + # compute all necessary FFR observability vectors in ffr_obs + n_stems = len(ffr_stems) + for stem_progress, ffr_stem in enumerate(ffr_stems): + ffr_idx = self.ffr_stem2idx[ffr_stem] + if len(ffr_stem.outs) > 1: + self.sim.c_dirty[...] = 0 + with self.timers['sim_ffr_prop']: + self.sim.c_prop(fault_line=ffr_stem.ins[0].index, fault_model=2) + with self.timers['sim_ffr_out_reduce']: + ffr_obs[ffr_idx] = np.bitwise_or.reduce(c_golden_poppo ^ self.sim.c[self.sim.poppo_c_locs], axis=0) & obs_mask + with self.timers['sim_ffr_reset']: + self.sim.c[...] = c_golden # clear fault + else: # primary output, completely observable + ffr_obs[ffr_idx] = obs_mask + p.update(((stem_progress+1) / n_stems) * ((bidx+1) / nbatches)) + + for fault in faults: fault_site = fault//2 fault_polarity = fault&1 ffr_stem = self.lines2ffr_stem[fault_site] assert ffr_stem is not None ffr_idx = self.ffr_stem2idx[ffr_stem] - # compute FFR observability vector ffr_obs[ffr_idx] if necessary - if not ffr_obs_valid[ffr_idx]: - if len(ffr_stem.outs) > 1: - self.sim.c_dirty[...] = 0 - with self.timers['sim_ffr_prop']: - self.sim.c_prop(fault_line=ffr_stem.ins[0].index, fault_model=2) - with self.timers['sim_ffr_out_reduce']: - ffr_obs[ffr_idx] = np.bitwise_or.reduce(c_golden_poppo ^ self.sim.c[self.sim.poppo_c_locs], axis=0) & obs_mask - with self.timers['sim_ffr_reset']: - self.sim.c[...] = c_golden # clear fault - else: # primary output, completely observable - ffr_obs[ffr_idx] = obs_mask - ffr_obs_valid[ffr_idx] = 1 - # observability of the fault site at the FFR stem by tracing # the (unique, fan-out free) path from the site to the stem # and requiring every gate along it to be sensitised. @@ -141,6 +170,5 @@ class SAFSimPPSFP: if np.bitwise_or.reduce(ffr_obs[ffr_idx] & fault_to_stem_obs & fault_act) != 0: fclass_DS.add(fault) - p.update(((fidx+1) / nfaults) * ((bidx+1) / nbatches)) return {'DS': fclass_DS, 'NO': set(faults) - fclass_DS} \ No newline at end of file diff --git a/src/fsim/static.py b/src/fsim/static.py index fa4a5b5..5a30321 100644 --- a/src/fsim/static.py +++ b/src/fsim/static.py @@ -2,12 +2,15 @@ """ from collections import defaultdict +from collections.abc import Iterable from itertools import product import numpy as np +import kahypar +from kyupy import log from kyupy.circuit import Circuit, Node -from kyupy.techlib import TechLib +from kyupy.techlib import TechLib, KYUPY class LineRoles: """Computes signal line roles of a circuit depending on the structural reach of each signal. @@ -193,3 +196,121 @@ class FaultSet: @staticmethod def fault_type_str(fault: int) -> str: return 'sa1' if fault&1 else 'sa0' + + +def collect_ancestors(key: int, predecessors: dict[int,set[int]], ancestors: dict[int,set[int]]) -> set[int]: + if key not in ancestors: + anc: set[int] = {key} + if key in predecessors: + for pred in predecessors[key]: + anc.update(collect_ancestors(pred, predecessors, ancestors)) + ancestors[key] = anc + return ancestors[key] + + +def collect_reachable(key: int, successors: dict[int,set[int]], reachable: dict[int,set[int]]) -> set: + if key not in reachable: + suc = {key} + if key in successors: + for s in successors[key]: + suc.update(collect_reachable(s, successors, reachable)) + reachable[key] = suc + return reachable[key] + + +def ffr_set_stats(stems: Iterable[int], reachable: dict[int,set[int]]): + all_reachable = set() + propagation_sum = 0 + stem_count = 0 + for n in stems: + stem_count += 1 + r = reachable[n] + all_reachable.update(r) + propagation_sum += len(r) + total_reachable_stems = len(all_reachable) + propagation_avg = propagation_sum / stem_count + propagation_avg_portion = propagation_avg / total_reachable_stems + return f'{stem_count:6d} {total_reachable_stems:7d} {propagation_avg_portion:.3f}' + + +class CircuitPartition: + + def __init__(self, c_resolved: Circuit, k: int = 1): + line2fo_node = np.full(len(c_resolved.lines), -1, dtype=np.int32) + fo_nodes = [] + for fo_node, region_nodes in c_resolved.fanout_free_regions(KYUPY): + fo_nodes.append(fo_node) + for n in [fo_node] + region_nodes: + for il in n.ins.without_nones(): + line2fo_node[il] = fo_node + + log.info(f'FFR count: {len(fo_nodes)}') + + predecessors = defaultdict(set) + successors = defaultdict(set) + for fo_node in fo_nodes: + if len(fo_node.outs) > 1: + for ol in fo_node.outs.without_nones(): + assert line2fo_node[ol] >= 0, f'unknown {ol}' + predecessors[int(line2fo_node[ol])].add(fo_node.index) + successors[fo_node.index].add(int(line2fo_node[ol])) + + ancestors = dict() + for fo_node in fo_nodes: + collect_ancestors(fo_node.index, predecessors, ancestors) + + reachable = dict() + for fo_node in fo_nodes: + collect_reachable(fo_node.index, successors, reachable) + + if k <= 1: + return {0: {n.index for n in fo_nodes}} + + stem2hnode = {s.index: i for i, s in enumerate(fo_nodes)} + + num_hnodes = len(fo_nodes) + hnode_weights = [len(reachable[n.index]) for n in fo_nodes] + + num_hnets = len(fo_nodes) + hnets = [[] for _ in range(num_hnodes)] + for stem, anc in ancestors.items(): + hnets[stem2hnode[stem]] = sorted(stem2hnode[n] for n in anc) + + hyperedge_indices = [] + hyperedges = [] + for hnet in hnets: + hyperedge_indices.append(len(hyperedges)) + hyperedges.extend(hnet) + hyperedge_indices.append(len(hyperedges)) + + edge_weights = [] + + hypergraph = kahypar.Hypergraph(num_hnodes, num_hnets, hyperedge_indices, hyperedges, k, edge_weights, hnode_weights) + + context = kahypar.Context() + context.suppressOutput(True) + context.loadINIconfiguration("km1_kKaHyPar_sea20.ini") + + context.setK(k) + context.setEpsilon(0.1) + + log.info('Partitioning...') + + kahypar.partition(hypergraph, context) + + log.info('Partitioning finished.') + + self.partitions = defaultdict(set) + for n in hypergraph.nodes(): + self.partitions[hypergraph.blockID(n)].add(fo_nodes[n].index) + self.fo_nodes = fo_nodes + self.reachable = reachable + + + def print_stats(self): + log.info('FFR set stats:') + log.info('[part] [size] [reach] [prop avg.]') + log.info(f'[all] {ffr_set_stats([n.index for n in self.fo_nodes], self.reachable)}') + + for i, stems in sorted(self.partitions.items()): + log.info(f'[{i:3d}] {ffr_set_stats(stems, self.reachable)}') \ No newline at end of file