Browse Source

move baseline into class, support for pattern list import, fault list export, avoid resolving circuits twice.

main
stefan 2 weeks ago
parent
commit
9a41edda4c
  1. 2
      Makefile
  2. 25
      README.md
  3. 2
      kyupy
  4. 88
      main.py
  5. 47
      src/fsim/baseline.py
  6. 35
      src/fsim/static.py
  7. 16
      tests/test_fault_set.py

2
Makefile

@ -0,0 +1,2 @@
atpg:
quaigh atpg --output c6288.test tests/c6288.bench

25
README.md

@ -36,6 +36,31 @@ uv run jupyter lab
Choose `uv-env` as kernel in Jupyter Lab. Choose `uv-env` as kernel in Jupyter Lab.
## Performance Statistics
Numbers are given in `gfs/s` = `gates * faults * patterns / second`.
Stuck-at fault sim on 1024 patterns, baseline:
| OS, CPU, RAM | `tests/c6288.bench` | `polito-itc99-b15-sky130` |
|--------------|---------------------|---------------------------|
| MacOS, M3 Max, 32GiB (Stefan) | 1.52G | 1.97G |
| WSL, i7-14650HX, 15GiB (Zhang) | 1.68G | 2.03G |
## Notes
```
Common Fault Classes:
DT - Detected
DS - Detected By Simulation
DI - Detected By Implication
PT - Possibly detected
UD - Undetectable
UB - Undetectable - blocked
UR - Undetectable - redundant
AU - ATPG untestable
AN - ATPG untestable - not detected
ND - Not detected
NO - Not detected - not observed
```

2
kyupy

@ -1 +1 @@
Subproject commit 449bf9a0314c43cd0dfbafb6ad98c97d9c9803ce Subproject commit bda89dc1b0d4f16f9e538e13ec449ea949c755bd

88
main.py

@ -6,22 +6,22 @@ import time
import numpy as np import numpy as np
from kyupy import verilog, bench, log, logic, batchrange 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 kyupy.logic_sim import LogicSim2V
from fsim.static import LineRoles, FaultSet from fsim.static import LineRoles, FaultSet
from fsim.baseline import SAFSimSimple
def main(): def main():
parser = argparse.ArgumentParser(description='A basic stuck-at fault simulator.') parser = argparse.ArgumentParser(description='A basic stuck-at fault simulator.')
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='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('-o', '--output', default=None, help='')
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()
args.patterns = int(args.patterns)
args.tlib = techlib_by_name[args.tlib]
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"
@ -30,75 +30,71 @@ def main():
log.info(f'loading {circuit_path} ...') log.info(f'loading {circuit_path} ...')
if circuit_path.name.endswith('.bench'): if circuit_path.name.endswith('.bench'):
c = bench.load(circuit_path)
args.tlib = techlib_by_name['KYUPY'] args.tlib = techlib_by_name['KYUPY']
c = bench.load(circuit_path)
else: else:
args.tlib = techlib_by_name[args.tlib]
c = verilog.load(circuit_path, tlib=args.tlib) c = verilog.load(circuit_path, tlib=args.tlib)
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=}')
lr = LineRoles(c, args.tlib) lr = LineRoles(c, args.tlib)
log.info(f'line role stats={lr.stats}') log.info(f'line role stats={lr.stats}')
fs = FaultSet(c, args.tlib) c_resolved = c.copy()
c_resolved.resolve_tlib_cells(args.tlib)
fs = FaultSet(c, args.tlib, c_resolved)
log.info(f'fault sites: {len(fs.fault_sites)}') log.info(f'fault sites: {len(fs.fault_sites)}')
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)}')
c.resolve_tlib_cells(args.tlib)
ffr_stems = [] ffr_stems = []
for stem, _ in c.fanout_free_regions(KYUPY): for stem, _ in c_resolved.fanout_free_regions(KYUPY):
if len(stem.outs) > 0 and stem.outs[0] is not None: if len(stem.outs) > 0 and stem.outs[0] is not None:
ffr_stems.append(stem.outs[0]) ffr_stems.append(stem.outs[0])
ffr_stems = np.array(ffr_stems, dtype=np.uint32) ffr_stems = np.array(ffr_stems, dtype=np.uint32)
log.info(f'FFR count: {len(ffr_stems)}') log.info(f'FFR count: {len(ffr_stems)}')
sim = LogicSim2V(c, sims=min(args.patterns, 10240))
rng = np.random.default_rng(args.seed) rng = np.random.default_rng(args.seed)
if not (tests_path := Path(args.patterns)).exists(): # fallback to random patterns.
patterns = rng.choice( patterns = rng.choice(
[logic.ZERO, logic.ONE], [logic.ZERO, logic.ONE],
size=(sim.s_len, args.patterns), size=(len(c_resolved.s_nodes(KYUPY)), int(args.patterns)),
).astype(np.uint8) ).astype(np.uint8)
else:
log.info(f'loading {tests_path} ...')
if tests_path.name.endswith('.stil'):
patterns = stil.load(tests_path).tests(c_resolved)
else:
patterns = atalanta.load(tests_path).tests(c_resolved)
golden = np.zeros_like(patterns) saf_collapsed = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32)
log.info(f'{sim=}') rng.shuffle(saf_collapsed)
sim.simulate(patterns, golden)
log.info(f'golden sim finished.')
syndrome = np.zeros_like(patterns)
injection_faults = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32)
rng.shuffle(injection_faults)
undetected = set()
detected = set()
start_time = time.perf_counter() safsim = SAFSimSimple(c_resolved, min(patterns.shape[1], 10240))
log.info(f'{safsim.sim=}')
with log.progress() as p: fclasses = safsim.classify_faults(saf_collapsed, patterns)
for fidx, fault in enumerate(injection_faults):
fault_site = fault//2
fault_polarity = fault&1
p.update((fidx+1) / len(injection_faults), f'd:{len(detected)} u:{len(undetected)}')
for bo, bs in batchrange(patterns.shape[1], sim.sims):
sim.s_assign[:, :bs] = patterns[:, bo:bo+bs]
sim.s_to_c()
sim.c_prop(fault_line=fault_site, fault_model=fault_polarity)
sim.c_to_s()
syndrome[:, bo:bo+bs] = sim.s_result[:,:bs]
if np.allclose(golden, syndrome):
undetected.add(fault)
else:
detected.add(fault)
sim_time = time.perf_counter() - start_time log.info(f'fsim time: {safsim.sim_time:.2f}s')
log.info(f'fsim time: {sim_time:.2f}s') sim_performance = stats['comb'] * len(saf_collapsed) * patterns.shape[1] / safsim.sim_time
sim_performance = stats['comb'] * len(injection_faults) * patterns.shape[1] / sim_time
log.info(f'fsim performance: {sim_performance:.2e} gfp/s') log.info(f'fsim performance: {sim_performance:.2e} gfp/s')
log.info(f'detected by simulation: {len(detected)}/{len(injection_faults)} - {len(detected)/len(injection_faults)*100:.2f}%') log.info(f'detected by simulation (collapsed): {len(fclasses["DS"])}/{len(saf_collapsed)} - {len(fclasses["DS"])/len(saf_collapsed)*100:.2f}%')
if args.output is not None:
out_path = Path(args.output)
with open(out_path, 'w') as f:
log.info(f'Writing {out_path.absolute()} ...')
for flt_rep in fclasses["NO"]:
for flt in fs.saf_equiv_classes[flt_rep]:
f.write(f'{fs.fault_type_str(flt)}\tNO\t{fs.fault_site_str(c, args.tlib, flt)}\n')
for flt_rep in fclasses["DS"]:
for flt in fs.saf_equiv_classes[flt_rep]:
f.write(f'{fs.fault_type_str(flt)}\tDS\t{fs.fault_site_str(c, args.tlib, flt)}\n')
if __name__ == "__main__": if __name__ == "__main__":
main() main()

47
src/fsim/baseline.py

@ -0,0 +1,47 @@
import time
from collections.abc import Collection
import numpy as np
from kyupy import log, batchrange
from kyupy.circuit import Circuit
from kyupy.logic_sim import LogicSim2V
class SAFSimSimple:
def __init__(self, circuit_resolved: Circuit, batch_size: int):
self.sim = LogicSim2V(circuit_resolved, sims=batch_size)
self.sim_time = 0
pass
def classify_faults(self, faults: Collection[int], patterns: np.ndarray):
golden = np.zeros_like(patterns)
self.sim.simulate(patterns, golden)
log.info(f'golden sim finished.')
syndrome = np.zeros_like(patterns)
fclass_NO = set()
fclass_DS = set()
start_time = time.perf_counter()
with log.progress() as p:
for fidx, fault in enumerate(faults):
fault_site = fault//2
fault_polarity = fault&1
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.s_assign[:, :bs] = patterns[:, bo:bo+bs]
self.sim.s_to_c()
self.sim.c_prop(fault_line=fault_site, fault_model=fault_polarity)
self.sim.c_to_s()
syndrome[:, bo:bo+bs] = self.sim.s_result[:,:bs]
if np.allclose(golden, syndrome):
fclass_NO.add(fault)
else:
fclass_DS.add(fault)
self.sim_time += time.perf_counter() - start_time
return {'DS': fclass_DS, 'NO': fclass_NO}

35
src/fsim/static.py

@ -90,7 +90,7 @@ class FaultSet:
Stuck-at faults are collapsed using basic controlling-value theory of simple gates. Stuck-at faults are collapsed using basic controlling-value theory of simple gates.
""" """
def __init__(self, circuit: Circuit, tlib: TechLib): def __init__(self, circuit: Circuit, tlib: TechLib, circuit_resolved: Circuit):
lr = LineRoles(circuit, tlib) lr = LineRoles(circuit, tlib)
self.fault_sites = {l.index for l in circuit.lines self.fault_sites = {l.index for l in circuit.lines
@ -117,22 +117,22 @@ class FaultSet:
representative f always contains at least f (:math:`f \in` saf_equiv_classes[f]). representative f always contains at least f (:math:`f \in` saf_equiv_classes[f]).
""" """
def collect_equivalent_faults(circuit: Circuit, fault: int) -> set: def collect_equivalent_faults(circuit_resolved: Circuit, fault: int) -> set:
site = fault//2 site = fault//2
polarity = fault&1 polarity = fault&1
driver = circuit.lines[site].driver driver = circuit_resolved.lines[site].driver
# Stop collapsing at fanout stems, primary inputs, and cells whose # Stop collapsing at fanout stems, primary inputs, and cells whose
# output faults have no equivalent single-input fault (XOR/XNOR, MUX, the two-term # output faults have no equivalent single-input fault (XOR/XNOR, MUX, the two-term
# AND-OR/OR-AND cells, and sequential elements). # AND-OR/OR-AND cells, and sequential elements).
if len(driver.outs) > 1 or len(driver.ins) == 0 or driver.kind in ('XOR2', 'XOR3', 'XOR4', 'XNOR2', 'XNOR3', 'XNOR4', 'MUX21', 'AO22', 'AOI22', 'OA22', 'OAI22', 'DFF', 'LATCH'): return {fault} if len(driver.outs) > 1 or len(driver.ins) == 0 or driver.kind in ('XOR2', 'XOR3', 'XOR4', 'XNOR2', 'XNOR3', 'XNOR4', 'MUX21', 'AO22', 'AOI22', 'OA22', 'OAI22', 'DFF', 'LATCH'): return {fault}
if driver.kind == 'INV1': return {fault} | collect_equivalent_faults(circuit, driver.ins[0].index*2 + (1-polarity)) if driver.kind == 'INV1': return {fault} | collect_equivalent_faults(circuit_resolved, driver.ins[0].index*2 + (1-polarity))
if driver.kind in ('BUF1', '__fork__'): return {fault} | collect_equivalent_faults(circuit, driver.ins[0].index*2 + (polarity)) if driver.kind in ('BUF1', '__fork__'): return {fault} | collect_equivalent_faults(circuit_resolved, driver.ins[0].index*2 + (polarity))
def collapse(*faults: tuple) -> set: def collapse(*faults: tuple) -> set:
"""Collapse the output fault onto the given (input_line, stuck_value) pairs.""" """Collapse the output fault onto the given (input_line, stuck_value) pairs."""
equiv = {fault} equiv = {fault}
for site, value in faults: for site, value in faults:
equiv |= collect_equivalent_faults(circuit, site.index*2 + value) equiv |= collect_equivalent_faults(circuit_resolved, site.index*2 + value)
return equiv return equiv
# Simple gates: driving any input to its controlling value forces the output. # Simple gates: driving any input to its controlling value forces the output.
@ -167,11 +167,9 @@ class FaultSet:
if driver.kind == 'OA211': return collapse((driver.ins[2], 0), (driver.ins[3], 0)) if polarity == 0 else {fault} if driver.kind == 'OA211': return collapse((driver.ins[2], 0), (driver.ins[3], 0)) if polarity == 0 else {fault}
if driver.kind == 'OAI211': return collapse((driver.ins[2], 0), (driver.ins[3], 0)) if polarity == 1 else {fault} if driver.kind == 'OAI211': return collapse((driver.ins[2], 0), (driver.ins[3], 0)) if polarity == 1 else {fault}
raise ValueError(f'Unknown node kind {driver.kind}. Circuit should have been resolved to techlib.KYUPY?') raise ValueError(f'Unknown node kind {driver.kind}. circuit_resolved must be resolved to techlib.KYUPY with resolve_tlib_cells().')
remaining = self.saf_set.copy() remaining = self.saf_set.copy()
circuit_resolved = circuit.copy()
circuit_resolved.resolve_tlib_cells(tlib)
for n in circuit.reversed_topological_order(tlib): for n in circuit.reversed_topological_order(tlib):
for il in n.ins.without_nones(): for il in n.ins.without_nones():
for polarity in (0, 1): for polarity in (0, 1):
@ -181,13 +179,16 @@ class FaultSet:
remaining.difference_update(equiv) remaining.difference_update(equiv)
self.saf_equiv_classes[repr] = equiv self.saf_equiv_classes[repr] = equiv
self.circuit = circuit @staticmethod
self.tlib = tlib def fault_site_str(circuit: Circuit, tlib: TechLib, fault: int) -> str:
site = circuit.lines[fault//2]
def fault_str(self, fault: int) -> str:
site = self.circuit.lines[fault//2]
polarity = '@1' if fault&1 else '@0'
if site.driver.kind == '__fork__': if site.driver.kind == '__fork__':
return f'{site.reader.name}/{self.tlib.pin_name(site.reader.kind, site.reader_pin)}{polarity}' return f'{site.reader.name}/{tlib.pin_name(site.reader.kind, site.reader_pin)}'
elif site.driver.kind == 'input':
return f'{site.driver.name}'
else: else:
return f'{site.driver.name}/{self.tlib.pin_name(site.driver.kind, site.driver_pin, output=True)}{polarity}' return f'{site.driver.name}/{tlib.pin_name(site.driver.kind, site.driver_pin, output=True)}'
@staticmethod
def fault_type_str(fault: int) -> str:
return 'sa1' if fault&1 else 'sa0'

16
tests/test_fault_set.py

@ -4,15 +4,15 @@ from fsim.static import FaultSet
def test_trivial_inv(): def test_trivial_inv():
c = bench.parse('input(i) output(o) o=INV(i)') c = bench.parse('input(i) output(o) o=INV(i)')
fs = FaultSet(c, KYUPY) fs = FaultSet(c, KYUPY, c)
assert len(fs.saf_set) == 4 assert len(fs.saf_set) == 4
assert len(fs.saf_equiv_classes) == 2 assert len(fs.saf_equiv_classes) == 2
oline = c.forks['o'].ins[0] oline = c.forks['o'].ins[0]
iline = oline.driver.ins[0] iline = oline.driver.ins[0]
assert fs.fault_str(oline.index*2) == 'o/o@0' assert fs.fault_site_str(c, KYUPY, oline.index*2) == 'o/o'
assert fs.fault_str(oline.index*2+1) == 'o/o@1' assert fs.fault_site_str(c, KYUPY, oline.index*2+1) == 'o/o'
assert fs.fault_str(iline.index*2) == 'o/i0@0' assert fs.fault_site_str(c, KYUPY, iline.index*2) == 'o/i0'
assert fs.fault_str(iline.index*2+1) == 'o/i0@1' assert fs.fault_site_str(c, KYUPY, iline.index*2+1) == 'o/i0'
assert oline.index*2 in fs.saf_equiv_classes assert oline.index*2 in fs.saf_equiv_classes
assert iline.index*2+1 in fs.saf_equiv_classes[oline.index*2] assert iline.index*2+1 in fs.saf_equiv_classes[oline.index*2]
assert oline.index*2+1 in fs.saf_equiv_classes assert oline.index*2+1 in fs.saf_equiv_classes
@ -22,7 +22,7 @@ def test_and():
c = bench.parse('input(i0, i1) output(o) o=AND(i0, i1)') c = bench.parse('input(i0, i1) output(o) o=AND(i0, i1)')
for n in c.nodes: for n in c.nodes:
print(n) print(n)
fs = FaultSet(c, KYUPY) fs = FaultSet(c, KYUPY, c)
assert len(fs.saf_set) == 6 assert len(fs.saf_set) == 6
assert len(fs.saf_equiv_classes) == 4 assert len(fs.saf_equiv_classes) == 4
oline = c.forks['o'].ins[0] oline = c.forks['o'].ins[0]
@ -39,7 +39,7 @@ def test_and():
def _class_sizes(src): def _class_sizes(src):
"""Returns (#equiv class of output s-a-0, #equiv class of output s-a-1).""" """Returns (#equiv class of output s-a-0, #equiv class of output s-a-1)."""
c = bench.parse(src) c = bench.parse(src)
fs = FaultSet(c, KYUPY) fs = FaultSet(c, KYUPY, c)
oline = c.forks['o'].ins[0] oline = c.forks['o'].ins[0]
return (len(fs.saf_equiv_classes.get(oline.index*2, set())), return (len(fs.saf_equiv_classes.get(oline.index*2, set())),
len(fs.saf_equiv_classes.get(oline.index*2+1, set()))) len(fs.saf_equiv_classes.get(oline.index*2+1, set())))
@ -106,7 +106,7 @@ def test_s27():
G12 = NOR(G1, G7) G12 = NOR(G1, G7)
G13 = NOR(G2, G12) G13 = NOR(G2, G12)
''') ''')
fs = FaultSet(c, KYUPY) fs = FaultSet(c, KYUPY, c)
assert len(fs.saf_set) == 52 assert len(fs.saf_set) == 52
assert len(fs.saf_equiv_classes) == 32 assert len(fs.saf_equiv_classes) == 32
g11_line = c.cells['G11'].outs[0] g11_line = c.cells['G11'].outs[0]

Loading…
Cancel
Save