CharlesCNorton
Image-level person classification on EUPE-ViT-B features with a single free parameter
f5498f9 | """Synthesize the four decision variants with nosis and write circuit.json. | |
| python synth.py | |
| nosis is a pure-Python SystemVerilog to Lattice ECP5 synthesizer. Counts are | |
| LUT4s, carry cells and slices on that device. Adder trees land on the carry | |
| chain, so CCU2C rather than LUT4 is the resource that moves with the form of the | |
| decision, and `bound` records which resource limits each variant. | |
| """ | |
| import argparse | |
| import re | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) # repo root, for `common` | |
| from common import read_artifact, write_artifact # noqa: E402 | |
| HERE = Path(__file__).resolve().parent | |
| NOSIS_ROOT = Path(r'D:\nosis') | |
| VARIANTS = { | |
| 'sum': ('person_classifier_1p', 'runtime input'), | |
| 'sum_folded': ('person_classifier_sum_folded', 'baked'), | |
| 'popcount': ('person_classifier_popcount', 'runtime inputs'), | |
| 'popcount_folded': ('person_classifier_popcount_folded', 'baked'), | |
| } | |
| def synth_one(src: Path, top: str, build: Path, nosis_root: Path): | |
| build.mkdir(parents=True, exist_ok=True) | |
| r = subprocess.run( | |
| [sys.executable, '-m', 'nosis', str(src), '--top', top, '--stats', | |
| '-o', str(build / f'{top}.json')], | |
| cwd=str(nosis_root), capture_output=True, text=True) | |
| if r.returncode != 0: | |
| raise SystemExit(f'nosis failed on {src.name}:\n' | |
| f'{r.stdout[-2000:]}{r.stderr[-2000:]}') | |
| (build / f'{top}.log').write_text(r.stdout, encoding='utf-8') | |
| def grab(pattern, cast=int): | |
| m = re.search(pattern, r.stdout) | |
| return cast(m.group(1)) if m else None | |
| def text(pattern): | |
| m = re.search(pattern, r.stdout) | |
| return m.group(1) if m else None | |
| return {'slices': grab(r'Slices:\s+(\d+)'), 'lut4': grab(r'LUTs:\s+(\d+)'), | |
| 'ccu2c': grab(r'CCU2C:\s+(\d+)'), 'ffs': grab(r'FFs:\s+(\d+)'), | |
| 'bound': text(r'Bound:\s+(\S+)'), | |
| 'critical_path_ns': grab(r'Critical path delay:\s+([\d.]+)', float), | |
| 'device': text(r'Device:\s+(\S+)')} | |
| def main(): | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument('--rtl', type=Path, default=HERE / 'rtl') | |
| ap.add_argument('--build', type=Path, default=HERE / 'build') | |
| ap.add_argument('--nosis', type=Path, default=NOSIS_ROOT) | |
| ap.add_argument('--out', type=Path, default=HERE / 'circuit.json') | |
| args = ap.parse_args() | |
| accuracy = read_artifact(args.out)['accuracy'] | |
| variants = {} | |
| print(f"{'variant':>18}{'slices':>8}{'LUT4':>7}{'CCU2C':>7}{'bound':>7}{'ns':>8}") | |
| for name, (top, thresholds) in VARIANTS.items(): | |
| src = args.rtl / f'{name}.v' | |
| if not src.exists(): | |
| raise SystemExit(f'{src} missing; run rtl_gen.py first') | |
| s = synth_one(src, top, args.build, args.nosis) | |
| s.update({'rtl': f'rtl/{name}.v', 'thresholds': thresholds}) | |
| variants[name] = s | |
| print(f'{name:>18}{s["slices"]:>8}{s["lut4"]:>7}{s["ccu2c"]:>7}' | |
| f'{s["bound"]:>7}{s["critical_path_ns"]:>8.2f}', flush=True) | |
| device = next((v['device'] for v in variants.values() if v['device']), None) | |
| write_artifact(args.out, {'variants': variants, 'accuracy': accuracy}, | |
| generator='synth.py', | |
| tool='nosis', target={'family': 'ecp5', 'device': device}, | |
| inputs='40 signed INT8 feature channels at the classifier dims', | |
| note='LUT4, carry and slice counts on an ECP5, not abstract gates') | |
| print(f'\n[done] wrote {args.out}') | |
| if __name__ == '__main__': | |
| main() | |