partitioning for ppsfp
This commit is contained in:
1 parent
2928b5129c
commit
d30ad50b3c
6 files changed
+109
-57
No files matched your search
@@ -28,6 +28,9 @@ def main():
|
||||
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()
|
||||
|
||||
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.
|
||||
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())
|
||||
@@ -73,6 +76,9 @@ def main():
|
||||
saf_collapsed = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32)
|
||||
rng.shuffle(saf_collapsed)
|
||||
|
||||
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=}')
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
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.timers = Timers()
|
||||
|
||||
|
||||
+57
-24
@@ -1,9 +1,9 @@
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from collections.abc import Collection
|
||||
|
||||
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.logic_sim import LogicSim2V
|
||||
from kyupy.techlib import KYUPY
|
||||
@@ -21,6 +21,10 @@ class LogicSim2VCone:
|
||||
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:
|
||||
@@ -33,22 +37,56 @@ class LogicSim2VCone:
|
||||
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:
|
||||
"""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.timers = Timers()
|
||||
|
||||
self.ffr_stem2idx = {}
|
||||
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):
|
||||
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 il in n.ins.without_nones():
|
||||
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:
|
||||
"""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),
|
||||
@@ -109,14 +147,20 @@ class SAFSimPPSFP:
|
||||
fclass_DS = set()
|
||||
nbatches = cdiv(patterns.shape[1], self.sim.sims)
|
||||
ffr_obs = np.zeros((len(self.ffr_stem2idx), 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 = np.zeros(self.sim.c.shape[2], dtype=np.uint8)
|
||||
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_act = obs_mask.copy()
|
||||
# collect necessary stems
|
||||
ffr_stems = set()
|
||||
# collate all necessary stems into partitions
|
||||
part2stems = defaultdict(set)
|
||||
for fault in faults:
|
||||
fault_site = fault//2
|
||||
ffr_stems.add(self.lines2ffr_stem[fault_site])
|
||||
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:
|
||||
for bidx, (bo, bs) in enumerate(batchrange(patterns.shape[1], self.sim.sims)):
|
||||
@@ -125,24 +169,13 @@ class SAFSimPPSFP:
|
||||
self.sim.c_dirty[...] = 1
|
||||
with self.timers['sim_full_prop']:
|
||||
self.sim.c_prop()
|
||||
c_golden[...] = self.sim.c
|
||||
c_golden_poppo = c_golden[self.sim.poppo_c_locs]
|
||||
|
||||
# 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 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
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@ class SAFSimSimple:
|
||||
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.timers = Timers()
|
||||
|
||||
|
||||
+16
-17
@@ -237,18 +237,18 @@ 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 = []
|
||||
self.fo_nodes = []
|
||||
for fo_node, region_nodes in c_resolved.fanout_free_regions(KYUPY):
|
||||
fo_nodes.append(fo_node)
|
||||
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(fo_nodes)}')
|
||||
log.info(f'FFR count: {len(self.fo_nodes)}')
|
||||
|
||||
predecessors = defaultdict(set)
|
||||
successors = defaultdict(set)
|
||||
for fo_node in fo_nodes:
|
||||
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}'
|
||||
@@ -256,22 +256,23 @@ class CircuitPartition:
|
||||
successors[fo_node.index].add(int(line2fo_node[ol]))
|
||||
|
||||
ancestors = dict()
|
||||
for fo_node in fo_nodes:
|
||||
for fo_node in self.fo_nodes:
|
||||
collect_ancestors(fo_node.index, predecessors, ancestors)
|
||||
|
||||
reachable = dict()
|
||||
for fo_node in fo_nodes:
|
||||
collect_reachable(fo_node.index, successors, reachable)
|
||||
self.reachable = dict()
|
||||
for fo_node in self.fo_nodes:
|
||||
collect_reachable(fo_node.index, successors, self.reachable)
|
||||
|
||||
if k <= 1:
|
||||
return {0: {n.index for n in fo_nodes}}
|
||||
self.partitions = {0: {n.index for n in self.fo_nodes}}
|
||||
return
|
||||
|
||||
stem2hnode = {s.index: i for i, s in enumerate(fo_nodes)}
|
||||
stem2hnode = {s.index: i for i, s in enumerate(self.fo_nodes)}
|
||||
|
||||
num_hnodes = len(fo_nodes)
|
||||
hnode_weights = [len(reachable[n.index]) for n in fo_nodes]
|
||||
num_hnodes = len(self.fo_nodes)
|
||||
hnode_weights = [len(self.reachable[n.index]) for n in self.fo_nodes]
|
||||
|
||||
num_hnets = len(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)
|
||||
@@ -292,7 +293,7 @@ class CircuitPartition:
|
||||
context.loadINIconfiguration("km1_kKaHyPar_sea20.ini")
|
||||
|
||||
context.setK(k)
|
||||
context.setEpsilon(0.1)
|
||||
context.setEpsilon(0.05)
|
||||
|
||||
log.info('Partitioning...')
|
||||
|
||||
@@ -302,9 +303,7 @@ class CircuitPartition:
|
||||
|
||||
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
|
||||
self.partitions[hypergraph.blockID(n)].add(self.fo_nodes[n].index)
|
||||
|
||||
|
||||
def print_stats(self):
|
||||
|
||||
+26
-14
@@ -13,6 +13,7 @@ The three simulators are exercised side-by-side and must agree exactly:
|
||||
"""
|
||||
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -41,9 +42,8 @@ def make_patterns(*specs):
|
||||
return pat if pat.ndim == 2 else pat.reshape(-1, 1)
|
||||
|
||||
|
||||
def classify(alg, circuit, faults, patterns):
|
||||
# PPSFP requires the batch size to match the pattern count (as main.py does).
|
||||
sim = alg(circuit, patterns.shape[1])
|
||||
def classify(alg, circuit, faults, patterns, partitioning = None):
|
||||
sim = alg(circuit, batch_size=patterns.shape[1], partitioning=partitioning)
|
||||
return sim.classify_faults(list(faults), patterns)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
simple = classify(SAFSimSimple, s27_resolved, faults, pat)
|
||||
incr = classify(SAFSimIncremental, s27_resolved, faults, pat)
|
||||
|
||||
# The two exact simulators must classify every fault identically.
|
||||
assert simple['DS'] == incr['DS']
|
||||
assert simple['NO'] == incr['NO']
|
||||
# This deterministic pattern set leaves exactly 6 faults unobserved.
|
||||
assert len(simple['DS']) == 26
|
||||
assert len(simple['NO']) == 6
|
||||
|
||||
|
||||
def test_s27_ppsfp_matches_exact(s27_bench, s27_resolved):
|
||||
faults, pat = _s27_setup(s27_bench, s27_resolved)
|
||||
|
||||
exact = classify(SAFSimSimple, s27_resolved, faults, pat)
|
||||
incr = classify(SAFSimIncremental, s27_resolved, faults, pat)
|
||||
ppsfp = classify(SAFSimPPSFP, s27_resolved, faults, pat)
|
||||
|
||||
# With exact FFR-stem observability, PPSFP classifies identically to the
|
||||
# brute-force simulator -- no longer an over-approximation.
|
||||
assert simple['DS'] == incr['DS']
|
||||
assert simple['NO'] == incr['NO']
|
||||
assert simple['DS'] == ppsfp['DS']
|
||||
assert simple['NO'] == ppsfp['NO']
|
||||
|
||||
|
||||
def test_s27_ppsfp_partitioning(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)
|
||||
ppsfp = classify(SAFSimPPSFP, s27_resolved, faults, pat, partitioning)
|
||||
|
||||
assert ppsfp['DS'] == exact['DS']
|
||||
assert ppsfp['NO'] == exact['NO']
|
||||
|
||||
|
||||
Reference in new issue
Block a user