Compare commits

...

4 Commits

  1. 25
      README.md
  2. 74
      km1_kKaHyPar_sea20.ini
  3. 2
      kyupy
  4. 27
      main.py
  5. 3
      src/fsim/incremental.py
  6. 111
      src/fsim/ppsfp.py
  7. 12
      src/fsim/simple.py
  8. 122
      src/fsim/static.py
  9. 39
      tests/test_safsim.py

25
README.md

@ -44,14 +44,37 @@ Single-core `SAFSimSimple` on 1024 patterns (baseline):
| OS, CPU, RAM | `tests/c6288.bench` | `polito-itc99-b15-sky130` | | OS, CPU, RAM | `tests/c6288.bench` | `polito-itc99-b15-sky130` |
|--------------|---------------------|---------------------------| |--------------|---------------------|---------------------------|
| | **2.4k gates, 7.7k SAF** | **17k gates, 24k SAF** |
| MacOS, M3 Max, 32GiB (Stefan) | 1.52G | 1.97G | | MacOS, M3 Max, 32GiB (Stefan) | 1.52G | 1.97G |
| WSL, i7-14650HX, 15GiB (Zhang) | 1.68G | 2.03G | | WSL, i7-14650HX, 15GiB (Zhang) | 1.68G | 2.03G |
| Ubuntu, Xeon E5-1650, 251GiB (Nosaka) | 0.94G | 1.18G | | Ubuntu, Xeon E5-1650, 251GiB (Nosaka) | 0.94G | 1.18G |
| WSL2, ?, ? (Kawachi) | 0.82G | ? | | WSL2, i5-8250U, 8GiB (Kawachi) | 0.82G | 0.94G |
| GH Codespace, EPYC 7763 (Kelvin) | 1.03G | ? | | GH Codespace, EPYC 7763 (Kelvin) | 1.03G | ? |
| Ubuntu, i5-8265U, 8GiB (Kelvin) | 0.77G | ? | | Ubuntu, i5-8265U, 8GiB (Kelvin) | 0.77G | ? |
Single-core `SAFSimIncremental` on 1024 patterns:
| OS, CPU, RAM | `tests/c6288.bench` | `polito-itc99-b15-sky130` |
|--------------|---------------------|---------------------------|
| | **2.4k gates, 7.7k SAF** | **17k gates, 24k SAF** |
| MacOS, M3 Max, 32GiB (Stefan) | 2.83G | 7.71G |
| WSL, i7-14650HX, 15GiB (Zhang) | ? | ? |
| Ubuntu, Xeon E5-1650, 251GiB (Nosaka) | ? | ? |
| WSL2, i5-8250U, 8GiB (Kawachi) | 1.98G | 4.74G |
| GH Codespace, EPYC 7763 (Kelvin) | ? | ? |
| Ubuntu, i5-8265U, 8GiB (Kelvin) | ? | ? |
Single-core `SAFSimPPSFP` on 1024 patterns:
| OS, CPU, RAM | `tests/c6288.bench` | `polito-itc99-b15-sky130` |
|--------------|---------------------|---------------------------|
| | **2.4k gates, 7.7k SAF** | **17k gates, 24k SAF** |
| MacOS, M3 Max, 32GiB (Stefan) | 14.7G | 77.0G |
| WSL, i7-14650HX, 15GiB (Zhang) | ? | ? |
| Ubuntu, Xeon E5-1650, 251GiB (Nosaka) | ? | ? |
| WSL2, i5-8250U, 8GiB (Kawachi) | 7.33G | 38.1G |
| GH Codespace, EPYC 7763 (Kelvin) | ? | ? |
| Ubuntu, i5-8265U, 8GiB (Kelvin) | ? | ? |
## Notes ## Notes

74
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

2
kyupy

@ -1 +1 @@
Subproject commit 7a69ce901680164ad392a303f11b9f8067df5728 Subproject commit f0c10633bf1d4a2dbef7d83e494386b87ad93c07

27
main.py

@ -2,14 +2,13 @@
import argparse import argparse
import subprocess import subprocess
from pathlib import Path from pathlib import Path
import time
import numpy as np import numpy as np
from kyupy import verilog, bench, log, logic, batchrange, atalanta, stil from kyupy import verilog, bench, log, logic, batchrange, atalanta, stil
from kyupy.techlib import techlib_by_name, KYUPY 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.simple import SAFSimSimple
from fsim.incremental import SAFSimIncremental from fsim.incremental import SAFSimIncremental
from fsim.ppsfp import SAFSimPPSFP from fsim.ppsfp import SAFSimPPSFP
@ -23,11 +22,15 @@ 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('-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('-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('-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('--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".') 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() args = parser.parse_args()
if int(args.partitions) > 1:
assert args.algorithm == 'ppsfp', "partitioning is only supported for PPSFP."
if not (circuit_path := Path(args.circuit)).exists(): # fallback to published nix package. if not (circuit_path := Path(args.circuit)).exists(): # fallback to published nix package.
nix_cmd = f"nix build github:s-holst/benchmark-circuits#{args.circuit} --print-out-paths --no-link" nix_cmd = f"nix build github:s-holst/benchmark-circuits#{args.circuit} --print-out-paths --no-link"
benchmark_path = Path(subprocess.check_output(nix_cmd.split(), text=True).strip()) benchmark_path = Path(subprocess.check_output(nix_cmd.split(), text=True).strip())
@ -43,9 +46,7 @@ def main():
stats = {k.replace('__',''): v for k, v in c.stats(args.tlib).items() if k.startswith('__') or k.endswith('put')} 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=}') log.info(f'circuit {stats=}')
log.info(f'line role stats={LineRoles(c, args.tlib).stats}')
lr = LineRoles(c, args.tlib)
log.info(f'line role stats={lr.stats}')
c_resolved = c.copy() c_resolved = c.copy()
c_resolved.resolve_tlib_cells(args.tlib) c_resolved.resolve_tlib_cells(args.tlib)
@ -55,13 +56,8 @@ def main():
log.info(f'uncollapsed stuck-at fault count: {len(fs.saf_set)}') 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)}') log.info(f'collapsed stuck-at fault count: {len(fs.saf_equiv_classes)}')
ffr_stems = [] partitions = CircuitPartition(c_resolved, int(args.partitions))
for stem, _ in c_resolved.fanout_free_regions(KYUPY): partitions.print_stats()
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)}')
rng = np.random.default_rng(args.seed) rng = np.random.default_rng(args.seed)
@ -80,7 +76,10 @@ def main():
saf_collapsed = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32) saf_collapsed = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32)
rng.shuffle(saf_collapsed) rng.shuffle(saf_collapsed)
safsim = algorithms[args.algorithm](c_resolved, min(patterns.shape[1], 10240)) if int(args.partitions) > 1:
safsim = algorithms[args.algorithm](c_resolved, min(patterns.shape[1], 10240), partitions.partitions)
else:
safsim = algorithms[args.algorithm](c_resolved, min(patterns.shape[1], 10240))
log.info(f'{args.algorithm} {safsim.sim=}') log.info(f'{args.algorithm} {safsim.sim=}')
fclasses = safsim.classify_faults(saf_collapsed, patterns) fclasses = safsim.classify_faults(saf_collapsed, patterns)

3
src/fsim/incremental.py

@ -13,7 +13,8 @@ class SAFSimIncremental:
After priming the circuit with a batch of patterns, faults are injected and only the gates in its fan-out cone are re-simulated. After priming the circuit with a batch of patterns, faults are injected and only the gates in its fan-out cone are re-simulated.
""" """
def __init__(self, circuit_resolved: Circuit, batch_size: int): def __init__(self, circuit_resolved: Circuit, batch_size: int, partitioning: dict[int,set[int]]|None = None):
assert partitioning is None
self.sim = LogicSim2V(circuit_resolved, sims=batch_size) self.sim = LogicSim2V(circuit_resolved, sims=batch_size)
self.timers = Timers() self.timers = Timers()

111
src/fsim/ppsfp.py

@ -1,29 +1,92 @@
import time from collections import defaultdict
from collections.abc import Collection from collections.abc import Collection
import numpy as np import numpy as np
from kyupy import log, batchrange, cdiv, Timers, logic from kyupy import log, batchrange, cdiv, Timers
from kyupy.circuit import Circuit, Node from kyupy.circuit import Circuit, Node
from kyupy.logic_sim import LogicSim2V from kyupy.logic_sim import LogicSim2V
from kyupy.techlib import KYUPY 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
self.poppo_c_locs = base_sim.poppo_c_locs
def c_from_base(self, base_sim):
self.c[...] = base_sim.c
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))
def compute_ffr_obs(sim: LogicSim2V|LogicSim2VCone, ffr_stems: Collection[Node], obs_mask, timers):
ffr_obs = []
c_golden = sim.c.copy()
c_golden_poppo = c_golden[sim.poppo_c_locs]
for ffr_stem in ffr_stems:
if len(ffr_stem.outs) > 1:
sim.c_dirty[...] = 0
with timers['sim_ffr_prop']:
sim.c_prop(fault_line=ffr_stem.ins[0].index, fault_model=2)
with timers['sim_ffr_out_reduce']:
ffr_obs.append(np.bitwise_or.reduce(c_golden_poppo ^ sim.c[sim.poppo_c_locs], axis=0)[0] & obs_mask)
with timers['sim_ffr_reset']:
sim.c[...] = c_golden # clear fault
else: # primary output, completely observable
ffr_obs.append(obs_mask)
return np.stack(ffr_obs)
class SAFSimPPSFP: class SAFSimPPSFP:
"""A stuck-at fault simulator that uses explicit simulations only at roots of fan-out free regions. """A stuck-at fault simulator that uses explicit simulations only at roots of fan-out free regions.
""" """
def __init__(self, circuit_resolved: Circuit, batch_size: int): def __init__(self, circuit_resolved: Circuit, batch_size: int, partitioning: dict[int,set[int]]|None = None):
self.partitioning = partitioning
self.sim = LogicSim2V(circuit_resolved, sims=batch_size) self.sim = LogicSim2V(circuit_resolved, sims=batch_size)
self.timers = Timers() self.timers = Timers()
self.ffr_stem2idx = {} self.ffr_stem2idx = {}
self.lines2ffr_stem: list[Node|None] = [None] * len(circuit_resolved.lines) self.lines2ffr_stem: list[Node|None] = [None] * len(circuit_resolved.lines)
# origin line (the stem's driving line, where faults are injected in
# compute_ffr_obs) keyed by stem *node* index -- as stored in partitioning.
stem_idx2origin_line = {}
for stem, nodes in circuit_resolved.fanout_free_regions(KYUPY): for stem, nodes in circuit_resolved.fanout_free_regions(KYUPY):
self.ffr_stem2idx[stem] = len(self.ffr_stem2idx) self.ffr_stem2idx[stem] = len(self.ffr_stem2idx)
if len(stem.ins) > 0: # PI-fork stems have no driving line / no fault site
stem_idx2origin_line[stem.index] = stem.ins[0].index
for n in nodes + [stem]: for n in nodes + [stem]:
for il in n.ins.without_nones(): for il in n.ins.without_nones():
self.lines2ffr_stem[il.index] = stem self.lines2ffr_stem[il.index] = stem
self.sim_parts = {0: self.sim}
if self.partitioning is not None:
self.ffr_stem2part = {s: p for p, ss in self.partitioning.items() for s in ss}
if len(self.partitioning) > 1:
self.sim_parts = {
p: LogicSim2VCone(self.sim, {stem_idx2origin_line[s] for s in stems
if s in stem_idx2origin_line})
for p, stems in self.partitioning.items()}
def _gate_sensitivity(self, node: Node, pin: int) -> np.ndarray: def _gate_sensitivity(self, node: Node, pin: int) -> np.ndarray:
"""Patterns (as a packed bit-vector) for which `node`'s output is """Patterns (as a packed bit-vector) for which `node`'s output is
sensitive to a change on input `pin` (the Boolean difference d_o/d_pin), sensitive to a change on input `pin` (the Boolean difference d_o/d_pin),
@ -83,12 +146,21 @@ class SAFSimPPSFP:
c_golden = np.zeros_like(self.sim.c) c_golden = np.zeros_like(self.sim.c)
fclass_DS = set() fclass_DS = set()
nbatches = cdiv(patterns.shape[1], self.sim.sims) 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 = 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.zeros(self.sim.c.shape[2], dtype=np.uint8)
obs_mask = np.packbits(np.full(patterns.shape[1], 1, dtype=np.uint8), bitorder='little') obs_mask_start = np.packbits(np.full(patterns.shape[1], 1, dtype=np.uint8), bitorder='little')
obs_mask[:len(obs_mask_start)] = obs_mask_start
fault_to_stem_obs = obs_mask.copy() fault_to_stem_obs = obs_mask.copy()
fault_act = obs_mask.copy() fault_act = obs_mask.copy()
# collate all necessary stems into partitions
part2stems = defaultdict(set)
for fault in faults:
fault_site = fault//2
stem = self.lines2ffr_stem[fault_site]
assert stem is not None
part = 0 if self.partitioning is None else self.ffr_stem2part[stem.index]
part2stems[part].add(stem)
with self.timers['sim'], log.progress() as p: with self.timers['sim'], log.progress() as p:
for bidx, (bo, bs) in enumerate(batchrange(patterns.shape[1], self.sim.sims)): for bidx, (bo, bs) in enumerate(batchrange(patterns.shape[1], self.sim.sims)):
@ -97,31 +169,21 @@ class SAFSimPPSFP:
self.sim.c_dirty[...] = 1 self.sim.c_dirty[...] = 1
with self.timers['sim_full_prop']: with self.timers['sim_full_prop']:
self.sim.c_prop() 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): for part, ffr_stems in part2stems.items():
ffr_idxs = [self.ffr_stem2idx[stem] for stem in ffr_stems]
sim = self.sim_parts[part]
if isinstance(sim, LogicSim2VCone):
sim.c_from_base(self.sim)
ffr_obs[ffr_idxs] = compute_ffr_obs(sim, ffr_stems, obs_mask, self.timers)
for fault in faults:
fault_site = fault//2 fault_site = fault//2
fault_polarity = fault&1 fault_polarity = fault&1
ffr_stem = self.lines2ffr_stem[fault_site] ffr_stem = self.lines2ffr_stem[fault_site]
assert ffr_stem is not None assert ffr_stem is not None
ffr_idx = self.ffr_stem2idx[ffr_stem] 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 # observability of the fault site at the FFR stem by tracing
# the (unique, fan-out free) path from the site to the stem # the (unique, fan-out free) path from the site to the stem
# and requiring every gate along it to be sensitised. # and requiring every gate along it to be sensitised.
@ -141,6 +203,5 @@ class SAFSimPPSFP:
if np.bitwise_or.reduce(ffr_obs[ffr_idx] & fault_to_stem_obs & fault_act) != 0: if np.bitwise_or.reduce(ffr_obs[ffr_idx] & fault_to_stem_obs & fault_act) != 0:
fclass_DS.add(fault) fclass_DS.add(fault)
p.update(((fidx+1) / nfaults) * ((bidx+1) / nbatches))
return {'DS': fclass_DS, 'NO': set(faults) - fclass_DS} return {'DS': fclass_DS, 'NO': set(faults) - fclass_DS}

12
src/fsim/simple.py

@ -13,7 +13,8 @@ class SAFSimSimple:
Simplest brute-force simulation of all given stuck-at faults. Simplest brute-force simulation of all given stuck-at faults.
""" """
def __init__(self, circuit_resolved: Circuit, batch_size: int): def __init__(self, circuit_resolved: Circuit, batch_size: int, partitioning: dict[int,set[int]]|None = None):
assert partitioning is None
self.sim = LogicSim2V(circuit_resolved, sims=batch_size) self.sim = LogicSim2V(circuit_resolved, sims=batch_size)
self.timers = Timers() self.timers = Timers()
@ -31,14 +32,7 @@ class SAFSimSimple:
fault_site = fault//2 fault_site = fault//2
fault_polarity = fault&1 fault_polarity = fault&1
p.update((fidx+1) / len(faults), f'DS:{len(fclass_DS)} NO:{len(fclass_NO)}') p.update((fidx+1) / len(faults), f'DS:{len(fclass_DS)} NO:{len(fclass_NO)}')
for bo, bs in batchrange(patterns.shape[1], self.sim.sims): self.sim.simulate(patterns, syndrome, fault_line=fault_site, fault_model=fault_polarity)
self.sim.s_assign[:, :bs] = patterns[:, bo:bo+bs]
self.sim.s_to_c()
with self.timers['sim_prop']:
self.sim.c_prop(fault_line=fault_site, fault_model=fault_polarity)
with self.timers['sim_eval']:
self.sim.c_to_s()
syndrome[:, bo:bo+bs] = self.sim.s_result[:,:bs]
with self.timers['sim_eval2']: with self.timers['sim_eval2']:
if np.allclose(golden, syndrome): if np.allclose(golden, syndrome):
fclass_NO.add(fault) fclass_NO.add(fault)

122
src/fsim/static.py

@ -2,12 +2,15 @@
""" """
from collections import defaultdict from collections import defaultdict
from collections.abc import Iterable
from itertools import product from itertools import product
import numpy as np import numpy as np
import kahypar
from kyupy import log
from kyupy.circuit import Circuit, Node from kyupy.circuit import Circuit, Node
from kyupy.techlib import TechLib from kyupy.techlib import TechLib, KYUPY
class LineRoles: class LineRoles:
"""Computes signal line roles of a circuit depending on the structural reach of each signal. """Computes signal line roles of a circuit depending on the structural reach of each signal.
@ -193,3 +196,120 @@ class FaultSet:
@staticmethod @staticmethod
def fault_type_str(fault: int) -> str: def fault_type_str(fault: int) -> str:
return 'sa1' if fault&1 else 'sa0' 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)
self.fo_nodes = []
for fo_node, region_nodes in c_resolved.fanout_free_regions(KYUPY):
self.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(self.fo_nodes)}')
predecessors = defaultdict(set)
successors = defaultdict(set)
for fo_node in self.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 self.fo_nodes:
collect_ancestors(fo_node.index, predecessors, ancestors)
self.reachable = dict()
for fo_node in self.fo_nodes:
collect_reachable(fo_node.index, successors, self.reachable)
if k <= 1:
self.partitions = {0: {n.index for n in self.fo_nodes}}
return
stem2hnode = {s.index: i for i, s in enumerate(self.fo_nodes)}
num_hnodes = len(self.fo_nodes)
hnode_weights = [len(self.reachable[n.index]) for n in self.fo_nodes]
num_hnets = len(self.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.05)
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(self.fo_nodes[n].index)
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)}')

39
tests/test_safsim.py

@ -13,6 +13,7 @@ The three simulators are exercised side-by-side and must agree exactly:
""" """
import itertools import itertools
from collections import defaultdict
import numpy as np import numpy as np
import pytest import pytest
@ -41,9 +42,8 @@ def make_patterns(*specs):
return pat if pat.ndim == 2 else pat.reshape(-1, 1) return pat if pat.ndim == 2 else pat.reshape(-1, 1)
def classify(alg, circuit, faults, patterns): def classify(alg, circuit, faults, patterns, partitioning = None):
# PPSFP requires the batch size to match the pattern count (as main.py does). sim = alg(circuit, batch_size=patterns.shape[1], partitioning=partitioning)
sim = alg(circuit, patterns.shape[1])
return sim.classify_faults(list(faults), patterns) return sim.classify_faults(list(faults), patterns)
@ -150,7 +150,7 @@ def test_c17_output_fault_class(alg, c17_resolved):
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# s27 fixture: exposes the exact-vs-optimistic difference between simulators. # s27 fixture: small sequential benchmark.
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def _s27_setup(s27_bench, s27_resolved): def _s27_setup(s27_bench, s27_resolved):
@ -158,7 +158,7 @@ def _s27_setup(s27_bench, s27_resolved):
faults = list(fs.saf_equiv_classes.keys()) faults = list(fs.saf_equiv_classes.keys())
rng = np.random.default_rng(1) rng = np.random.default_rng(1)
pat = rng.choice([logic.ZERO, logic.ONE], pat = rng.choice([logic.ZERO, logic.ONE],
size=(len(s27_resolved.s_nodes(KYUPY)), 64)).astype(np.uint8) size=(len(s27_resolved.s_nodes(KYUPY)), 8)).astype(np.uint8)
return faults, pat return faults, pat
@ -167,24 +167,36 @@ def test_s27_exact_simulators_agree(s27_bench, s27_resolved):
assert len(faults) == 32 # collapsed fault count, see test_fault_set.test_s27 assert len(faults) == 32 # collapsed fault count, see test_fault_set.test_s27
simple = classify(SAFSimSimple, s27_resolved, faults, pat) simple = classify(SAFSimSimple, s27_resolved, faults, pat)
# This deterministic pattern set leaves exactly 6 faults unobserved.
assert len(simple['DS']) == 26
assert len(simple['NO']) == 6
incr = classify(SAFSimIncremental, s27_resolved, faults, pat) incr = classify(SAFSimIncremental, s27_resolved, faults, pat)
ppsfp = classify(SAFSimPPSFP, s27_resolved, faults, pat)
# The two exact simulators must classify every fault identically.
assert simple['DS'] == incr['DS'] assert simple['DS'] == incr['DS']
assert simple['NO'] == incr['NO'] assert simple['NO'] == incr['NO']
# This deterministic pattern set leaves exactly one fault unobserved. assert simple['DS'] == ppsfp['DS']
assert len(simple['DS']) == 31 assert simple['NO'] == ppsfp['NO']
assert len(simple['NO']) == 1
def test_s27_ppsfp_matches_exact(s27_bench, s27_resolved): def test_s27_ppsfp_partitioning(s27_bench, s27_resolved):
faults, pat = _s27_setup(s27_bench, s27_resolved) faults, pat = _s27_setup(s27_bench, s27_resolved)
stems = [stem.index for stem, _ in s27_resolved.fanout_free_regions(KYUPY)]
k = 4
rng = np.random.default_rng(1)
parts = rng.integers(0, k, size=len(stems))
partitioning = defaultdict(set)
for part, stem in zip(parts, stems):
partitioning[int(part)].add(stem)
print(partitioning)
exact = classify(SAFSimSimple, s27_resolved, faults, pat) exact = classify(SAFSimSimple, s27_resolved, faults, pat)
ppsfp = classify(SAFSimPPSFP, s27_resolved, faults, pat) ppsfp = classify(SAFSimPPSFP, s27_resolved, faults, pat, partitioning)
# With exact FFR-stem observability, PPSFP classifies identically to the
# brute-force simulator -- no longer an over-approximation.
assert ppsfp['DS'] == exact['DS'] assert ppsfp['DS'] == exact['DS']
assert ppsfp['NO'] == exact['NO'] assert ppsfp['NO'] == exact['NO']
@ -201,6 +213,7 @@ def test_ppsfp_multibranch_stem_observability(alg):
r = classify(alg, c, [a_sa0], pat) r = classify(alg, c, [a_sa0], pat)
assert r['DS'] == {a_sa0} assert r['DS'] == {a_sa0}
@pytest.mark.parametrize('alg', [SAFSimIncremental, SAFSimPPSFP]) @pytest.mark.parametrize('alg', [SAFSimIncremental, SAFSimPPSFP])
def test_all_simprims(mydir, alg): def test_all_simprims(mydir, alg):
c = verilog.load(mydir / 'all_kyupy_simprims.v') c = verilog.load(mydir / 'all_kyupy_simprims.v')

Loading…
Cancel
Save