├── examples ├── cybersecurity │ ├── requirements.txt │ ├── dataset.py │ ├── README.md │ ├── dataset_dump.py │ ├── models.py │ ├── neq2lut.py │ └── train.py └── jet_substructure │ ├── requirements.txt │ ├── config │ └── yaml_IP_OP_config.yml │ ├── README.md │ ├── dataset.py │ ├── dataset_dump.py │ ├── models.py │ ├── neq2lut.py │ └── train.py ├── src └── logicnets │ ├── __init__.py │ ├── init.py │ ├── bench.py │ ├── synthesis.py │ ├── verilog.py │ ├── util.py │ ├── quant.py │ └── nn.py ├── AUTHORS.rst ├── docker ├── entry-point.sh ├── run-docker.sh └── Dockerfile.cpu ├── .gitignore ├── pyproject.toml ├── README.md └── LICENSE.txt /examples/cybersecurity/requirements.txt: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | numpy 16 | tensorboard 17 | -------------------------------------------------------------------------------- /src/logicnets/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import importlib.metadata 16 | 17 | __version__ = importlib.metadata.version(__name__) 18 | -------------------------------------------------------------------------------- /examples/jet_substructure/requirements.txt: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | h5py 16 | pyyaml<6.0 17 | numpy 18 | pandas 19 | scikit-learn 20 | tensorboard 21 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2021 Xilinx, Inc 2 | .. 3 | .. Licensed under the Apache License, Version 2.0 (the "License"); 4 | .. you may not use this file except in compliance with the License. 5 | .. You may obtain a copy of the License at 6 | .. 7 | .. http://www.apache.org/licenses/LICENSE-2.0 8 | .. 9 | .. Unless required by applicable law or agreed to in writing, software 10 | .. distributed under the License is distributed on an "AS IS" BASIS, 11 | .. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | .. See the License for the specific language governing permissions and 13 | .. limitations under the License. 14 | 15 | ============ 16 | Contributors 17 | ============ 18 | 19 | * Nicholas Fraser (@nickfraser) 20 | * Yash Akhauri (@akhauriyash) 21 | * Yaman Umuroglu (@maltanar) 22 | -------------------------------------------------------------------------------- /docker/entry-point.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2021 Xilinx, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if [ ! -z "$VIVADO_PATH" ]; then 18 | source $VIVADO_PATH/settings64.sh 19 | else 20 | echo "Warning: \$VIVADO_PATH not defined. Continuing but without synthesis support." 21 | fi 22 | 23 | pip install /workspace/logicnets[example-all] 24 | 25 | exec "$@" 26 | 27 | -------------------------------------------------------------------------------- /src/logicnets/init.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import torch 16 | import torch.nn as nn 17 | from torch import Tensor 18 | from torch.nn import init 19 | 20 | # TODO: Expand to support tensors larger than 2 dimensions 21 | def random_restrict_fanin(mask: Tensor, fan_in: int) -> Tensor: 22 | vector_size, num_vectors = nn.init._calculate_fan_in_and_fan_out(mask) 23 | init.constant_(mask, 0.0) 24 | if len(mask.shape) == 2: 25 | for i in range(num_vectors): 26 | x = torch.randperm(vector_size)[:fan_in] 27 | mask[i][x] = 1 28 | else: 29 | assert False, "Unsupported mask shape, specified: %s" % (str(mask.shape)) 30 | return mask 31 | 32 | -------------------------------------------------------------------------------- /examples/cybersecurity/dataset.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | import torch 17 | from torch.utils.data import TensorDataset 18 | 19 | #! wget -O unsw_nb15_binarized.npz https://zenodo.org/record/4519767/files/unsw_nb15_binarized.npz?download=1 20 | def get_preqnt_dataset(data_file: str, split: str): 21 | unsw_nb15_data = np.load(data_file) 22 | splits = ["train", "test"] 23 | if not split in splits: 24 | print(f"Invalid dataset split: {split}") 25 | assert(False) 26 | part_data = unsw_nb15_data[split].astype(np.float32) 27 | part_data = torch.from_numpy(part_data) 28 | part_data_in = part_data[:, :-1] 29 | part_data_out = part_data[:, -1] 30 | return TensorDataset(part_data_in, part_data_out) 31 | 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Temporary and binary files 16 | *~ 17 | *.py[cod] 18 | *.so 19 | *.cfg 20 | !.isort.cfg 21 | !setup.cfg 22 | *.orig 23 | *.log 24 | *.pot 25 | __pycache__/* 26 | .cache/* 27 | .*.swp 28 | */.ipynb_checkpoints/* 29 | .DS_Store 30 | 31 | # Project files 32 | .ropeproject 33 | .project 34 | .pydevproject 35 | .settings 36 | .idea 37 | tags 38 | 39 | # Package files 40 | *.egg 41 | *.eggs/ 42 | .installed.cfg 43 | *.egg-info 44 | 45 | # Unittest and coverage 46 | htmlcov/* 47 | .coverage 48 | .tox 49 | junit.xml 50 | coverage.xml 51 | .pytest_cache/ 52 | 53 | # Build and docs folder/files 54 | build/* 55 | dist/* 56 | sdist/* 57 | docs/api/* 58 | docs/_rst/* 59 | docs/_build/* 60 | cover/* 61 | MANIFEST 62 | 63 | # Per-project virtualenvs 64 | .venv*/ 65 | -------------------------------------------------------------------------------- /examples/jet_substructure/config/yaml_IP_OP_config.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # Copyright (C) 2020 FastML 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | Inputs: 17 | - j_zlogz 18 | - j_c1_b0_mmdt 19 | - j_c1_b1_mmdt 20 | - j_c1_b2_mmdt 21 | - j_c2_b1_mmdt 22 | - j_c2_b2_mmdt 23 | - j_d2_b1_mmdt 24 | - j_d2_b2_mmdt 25 | - j_d2_a1_b1_mmdt 26 | - j_d2_a1_b2_mmdt 27 | - j_m2_b1_mmdt 28 | - j_m2_b2_mmdt 29 | - j_n2_b1_mmdt 30 | - j_n2_b2_mmdt 31 | - j_mass_mmdt 32 | - j_multiplicity 33 | 34 | Labels: 35 | - j_g 36 | - j_q 37 | - j_w 38 | - j_z 39 | - j_t 40 | 41 | KerasModel: three_layer_model 42 | KerasModelRetrain: three_layer_model_constraint 43 | KerasLoss: categorical_crossentropy 44 | L1Reg: 0.0001 45 | NormalizeInputs: 1 46 | InputType: Dense 47 | ApplyPca: false 48 | PcaDimensions: 10 49 | 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [project] 16 | name = "logicnets" 17 | description = "Co-Designed Neural Networks and Circuits for Extreme-Throughput Applications" 18 | authors = [ 19 | { name = "Nicholas J. Fraser", email = "nicholas.fraser@amd.com" }, 20 | ] 21 | classifiers = [ 22 | "Development Status :: 3 - Alpha", 23 | "Programming Language :: Python :: 3", 24 | "License :: OSI Approved :: Apache Software License", 25 | ] 26 | dependencies = [ 27 | "torch", 28 | "brevitas", 29 | ] 30 | dynamic = ["version"] 31 | 32 | [project.optional-dependencies] 33 | simulate = [ # Install dependencies required for functional simulation of generated verilog 34 | "pyverilator", 35 | ] 36 | example-jsc = [ # Install dependencies for jet_substructure example 37 | "logicnets[simulate]", 38 | "h5py", 39 | "pyyaml<6.0", 40 | "numpy", 41 | "pandas", 42 | "scikit-learn", 43 | "tensorboard", 44 | ] 45 | example-nid = [ # Install dependencies for cybersecurity example 46 | "logicnets[simulate]", 47 | "numpy", 48 | "tensorboard", 49 | ] 50 | example-all = [ # Install dependencies for all examples, if possible 51 | "logicnets[example-jsc, example-nid]", 52 | ] 53 | 54 | [build-system] 55 | requires = [ 56 | "setuptools >= 61.0.0", 57 | "wheel", 58 | "setuptools_scm[toml]>=6.2" 59 | ] 60 | build.backend = "setuptools.build_meta" 61 | 62 | [tool.setuptools_scm] 63 | 64 | -------------------------------------------------------------------------------- /src/logicnets/bench.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from functools import reduce 16 | 17 | def generate_lut_bench(input_fanin_bits, output_bits, lut_string): 18 | lut_neuron_template = """\ 19 | {input_string}\ 20 | {output_string}\ 21 | {lut_string}""" 22 | input_string = "" 23 | for i in range(input_fanin_bits): 24 | input_string += f"INPUT(M0[{i}])\n" 25 | output_string = "" 26 | for i in range(output_bits): 27 | output_string += f"OUTPUT(M1[{i}])\n" 28 | return lut_neuron_template.format( input_string=input_string, 29 | output_string=output_string, 30 | lut_string=lut_string) 31 | 32 | def generate_lut_input_string(input_fanin_bits): 33 | lut_input_string = "" 34 | for i in range(input_fanin_bits): 35 | if i == 0: 36 | lut_input_string += f"( M0[{i}]" 37 | elif i == input_fanin_bits-1: 38 | lut_input_string += f", M0[{i}] )\n" 39 | else: 40 | lut_input_string += f", M0[{i}]" 41 | return lut_input_string 42 | 43 | def sort_to_bench(input_state_space_bin_str, bin_output_states): 44 | sorted_bin_output_states = bin_output_states.tolist() 45 | input_state_space_flat_int = list(map(lambda l: int(reduce(lambda a,b: a+b, l),2), input_state_space_bin_str)) 46 | zipped_io_states = list(zip(input_state_space_flat_int, sorted_bin_output_states)) 47 | zipped_io_states.sort(key=lambda x: x[0], reverse=True) 48 | sorted_bin_output_states = list(map(lambda x: x[1], zipped_io_states)) 49 | return sorted_bin_output_states 50 | -------------------------------------------------------------------------------- /src/logicnets/synthesis.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import subprocess 17 | from shutil import which 18 | 19 | #xcvu9p-flgb2104-2-i 20 | # TODO: Add option to perform synthesis on a remote server 21 | def synthesize_and_get_resource_counts(verilog_dir, top_name, fpga_part = "xcku3p-ffva676-1-e", clk_name="clk", clk_period_ns=5.0, post_synthesis = 0): 22 | # old part : "xczu3eg-sbva484-1-i" 23 | # ensure that the OH_MY_XILINX envvar is set 24 | if "OHMYXILINX" not in os.environ: 25 | raise Exception("The environment variable OHMYXILINX is not defined.") 26 | # ensure that vivado is in PATH: source $VIVADO_PATH/settings64.sh 27 | if which("vivado") is None: 28 | raise Exception("vivado is not in PATH, ensure settings64.sh is sourced.") 29 | omx_path = os.environ["OHMYXILINX"] 30 | script = "vivadocompile.sh" 31 | # vivadocompile.sh 32 | call_omx = "zsh %s/%s %s %s %s %f %s" % (omx_path, script, top_name, clk_name, fpga_part, float(clk_period_ns), post_synthesis) 33 | call_omx = call_omx.split() 34 | proc = subprocess.Popen(call_omx, cwd=verilog_dir, stdout=subprocess.PIPE, env=os.environ) 35 | proc.communicate() 36 | 37 | vivado_proj_folder = "%s/results_%s" % (verilog_dir, top_name) 38 | res_counts_path = vivado_proj_folder + "/res.txt" 39 | 40 | with open(res_counts_path, 'r') as myfile: 41 | res_data = myfile.read().split("\n") 42 | ret = {} 43 | ret["vivado_proj_folder"] = vivado_proj_folder 44 | for res_line in res_data: 45 | res_fields = res_line.split("=") 46 | print(res_fields) 47 | try: 48 | ret[res_fields[0]] = float(res_fields[1]) 49 | except ValueError: 50 | ret[res_fields[0]] = 0 51 | except IndexError: 52 | ret[res_fields[0]] = 0 53 | if ret["WNS"] == 0: 54 | ret["fmax_mhz"] = 0 55 | else: 56 | ret["fmax_mhz"] = 1000.0 / (clk_period_ns - ret["WNS"]) 57 | return ret 58 | -------------------------------------------------------------------------------- /docker/run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2021 Xilinx, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | DOCKER_GID=$(id -g) 20 | DOCKER_GNAME=$(id -gn) 21 | DOCKER_UNAME=$(id -un) 22 | DOCKER_UID=$(id -u) 23 | DOCKER_TAG=$DOCKER_UNAME/logicnets:$(date +%Y%m%d%H%M) 24 | LOGICNETS_PATH=$(readlink -f $(dirname ${0})/../) 25 | LOGICNETS_MOUNT_POINT=/workspace/logicnets 26 | 27 | docker build \ 28 | -t ${DOCKER_TAG} \ 29 | -f docker/Dockerfile.cpu \ 30 | --build-arg GID=$DOCKER_GID \ 31 | --build-arg GNAME=$DOCKER_GNAME \ 32 | --build-arg UNAME=$DOCKER_UNAME \ 33 | --build-arg UID=$DOCKER_UID \ 34 | . 35 | 36 | DOCKER_EXEC="docker run --rm --shm-size 32G -i -t --init " 37 | 38 | echo "Mounting LogicNets to ${LOGICNETS_MOUNT_POINT} inside the docker container" 39 | DOCKER_EXEC+="-v ${LOGICNETS_PATH}:${LOGICNETS_MOUNT_POINT} " 40 | 41 | if [ ! -z "$VIVADO_SETTINGS_FILE" ]; then 42 | export VIVADO_SETTINGS_FILE=$(readlink -f $VIVADO_SETTINGS_FILE) # Resolve symlinks 43 | export VIVADO_PATH=$(dirname $VIVADO_SETTINGS_FILE) 44 | export VIVADO_MOUNT_POINT=$(dirname $VIVADO_PATH) 45 | echo "\$VIVADO_SETTINGS_FILE defined. Mounting ${VIVADO_MOUNT_POINT} inside the docker container" 46 | DOCKER_EXEC+="-v ${VIVADO_MOUNT_POINT}:${VIVADO_MOUNT_POINT}:ro " 47 | DOCKER_EXEC+="-e VIVADO_PATH=$VIVADO_PATH " 48 | else 49 | echo "\$VIVADO_SETTINGS_FILE not defined. Please set VIVADO_SETTINGS_FILE to point to the location of settings64.sh in your Vivado installation, if you want to synthesize your networks." 50 | fi 51 | 52 | if [ ! -z "$XILINXD_LICENSE_FILE" ]; then 53 | echo "\$XILINXD_LICENSE_FILE environment variable detected. Adding it docker environment." 54 | DOCKER_EXEC+="-e XILINXD_LICENSE_FILE=${XILINXD_LICENSE_FILE} " 55 | else 56 | echo "\$XILINXD_LICENSE_FILE not set, Vivado may not have the necessary license within the docker environment." 57 | fi 58 | 59 | if [ ! -z "$LM_LICENSE_FILE" ]; then 60 | echo "\$LM_LICENSE_FILE environment variable detected. Adding it docker environment." 61 | DOCKER_EXEC+="-e LM_LICENSE_FILE=${LM_LICENSE_FILE} " 62 | else 63 | echo "\$LM_LICENSE_FILE not set, Vivado may not have the necessary license within the docker environment." 64 | fi 65 | 66 | DOCKER_EXEC+="${DOCKER_TAG} /bin/bash" 67 | 68 | $DOCKER_EXEC 69 | 70 | -------------------------------------------------------------------------------- /examples/cybersecurity/README.md: -------------------------------------------------------------------------------- 1 | # LogicNets for Network Intrusion Detection Systems 2 | 3 | This example shows the accuracy that is attainable using the LogicNets methodology on the network intrusion detection task described in our 2020 FPL paper. 4 | This example is a reimplementation of that work. 5 | 6 | ## Prerequisites 7 | 8 | * LogicNets 9 | * numpy 10 | * tensorboard 11 | 12 | ## Installation 13 | 14 | If you're using the docker image, all the above prerequisites will be already installed. 15 | Otherwise, you can install the above dependencies with pip and/or conda. 16 | 17 | ## Download the Dataset 18 | 19 | In order to download the dataset, browse to the directory where this example is contained (e.g., `cd /path/to/logicnets/examples/cybersecurity/`) and run the following: 20 | 21 | ```bash 22 | mkdir -p data 23 | wget -O data/unsw_nb15_binarized.npz https://zenodo.org/record/4519767/files/unsw_nb15_binarized.npz?download=1 24 | ``` 25 | 26 | ## Usage 27 | 28 | To train the \"NID-S\", \"NID-M\" and \"NID-L\" networks described in our 2020 FPL paper, run the 29 | following: 30 | 31 | ```bash 32 | python train.py --arch --log-dir .// 33 | ``` 34 | 35 | To then generate verilog from this trained model, run the following: 36 | 37 | ```bash 38 | python neq2lut.py --arch --checkpoint .//best_accuracy.pth --log-dir .//verilog/ --add-registers 39 | ``` 40 | 41 | ## Results 42 | 43 | Your results may vary slightly, depending on your system configuration. 44 | The following results are attained when training on a CPU and synthesising with Vivado 2019.2: 45 | 46 | | Network Architecture | Test Accuracy (%) | LUTs | Flip Flops | Fmax (Mhz) | Latency (Cycles) | 47 | | --------------------- | ----------------- | ----- | ------------- | ------------- | ----------------- | 48 | | NID-S | 90.5 | 650 | 515 | 758.15 | 3 | 49 | | NID-M | 92.6 | 1649 | 1024 | 839.63 | 5 | 50 | | NID-L | 92.9 | 8106 | 1901 | 498.26 | 5 | 51 | 52 | Note, the model architectures reflect the architectures described in our [FPL'20 paper](https://arxiv.org/abs/2004.03021). 53 | 54 | ## Citation 55 | 56 | If you find this work useful for your research, please consider citing 57 | our paper below: 58 | 59 | ```bibtex 60 | @inproceedings{umuroglu2020logicnets, 61 | author = {Umuroglu, Yaman and Akhauri, Yash and Fraser, Nicholas J and Blott, Michaela}, 62 | booktitle = {Proceedings of the International Conference on Field-Programmable Logic and Applications}, 63 | title = {LogicNets: Co-Designed Neural Networks and Circuits for Extreme-Throughput Applications}, 64 | year = {2020}, 65 | pages = {291-297}, 66 | publisher = {IEEE Computer Society}, 67 | address = {Los Alamitos, CA, USA}, 68 | month = {sep} 69 | } 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /examples/jet_substructure/README.md: -------------------------------------------------------------------------------- 1 | # LogicNets for Jet-Substructure Classification 2 | 3 | This example shows the accuracy that is attainable using the LogicNets methodology on the jet substructure classification task described in our 2020 FPL paper. 4 | This example is a reimplementation of that work. 5 | 6 | ## Prerequisites 7 | 8 | * LogicNets 9 | * h5py 10 | * yaml<6.0 11 | * numpy 12 | * pandas 13 | * scikit-learn 14 | 15 | ## Installation 16 | 17 | If you're using the docker image, all the above prerequisites will be already installed. 18 | Otherwise, you can install the above dependencies with pip and/or conda. 19 | 20 | ## Download the Dataset 21 | 22 | In order to download the dataset, browse to the directory where this example is contained (e.g., `cd /path/to/logicnets/examples/jet_substructure/`) and run the following: 23 | 24 | ```bash 25 | mkdir -p data 26 | wget https://cernbox.cern.ch/index.php/s/jvFd5MoWhGs1l5v/download -O data/processed-pythia82-lhc13-all-pt1-50k-r1_h022_e0175_t220_nonu_truth.z 27 | ``` 28 | 29 | ## Usage 30 | 31 | To train the \"JSC-S\", \"JSC-M\" and \"JSC-L\" networks described in our 2020 FPL paper, run the 32 | following: 33 | 34 | ```bash 35 | python train.py --arch --log-dir .// 36 | ``` 37 | 38 | To then generate verilog from this trained model, run the following: 39 | 40 | ```bash 41 | python neq2lut.py --arch --checkpoint .//best_accuracy.pth --log-dir .//verilog/ --add-registers 42 | ``` 43 | 44 | ## Results 45 | 46 | Your results may vary slightly, depending on your system configuration. 47 | The following results are attained when training on a CPU and synthesising with Vivado 2019.2: 48 | 49 | | Network Architecture | Test Accuracy (%) | LUTs | Flip Flops | Fmax (Mhz) | Latency (Cycles) | 50 | | --------------------- | ----------------- | ----- | ------------- | ------------- | ----------------- | 51 | | JSC-S | 69.8 | 244 | 270 | 1353.18 | 5 | 52 | | JSC-M | 72.1 | 15526 | 881 | 576.70 | 5 | 53 | | JSC-L | 73.1 | 36415 | 2790 | 389.86 | 6 | 54 | 55 | Note, the model architectures reflect the architectures described in our [FPL'20 paper](https://arxiv.org/abs/2004.03021). 56 | 57 | ## Citation 58 | 59 | If you find this work useful for your research, please consider citing 60 | our paper below: 61 | 62 | ```bibtex 63 | @inproceedings{umuroglu2020logicnets, 64 | author = {Umuroglu, Yaman and Akhauri, Yash and Fraser, Nicholas J and Blott, Michaela}, 65 | booktitle = {Proceedings of the International Conference on Field-Programmable Logic and Applications}, 66 | title = {LogicNets: Co-Designed Neural Networks and Circuits for Extreme-Throughput Applications}, 67 | year = {2020}, 68 | pages = {291-297}, 69 | publisher = {IEEE Computer Society}, 70 | address = {Los Alamitos, CA, USA}, 71 | month = {sep} 72 | } 73 | ``` 74 | 75 | -------------------------------------------------------------------------------- /docker/Dockerfile.cpu: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM ubuntu:18.04 16 | 17 | ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 18 | 19 | WORKDIR /workspace 20 | 21 | ARG GID 22 | ARG GNAME 23 | ARG UNAME 24 | ARG UID 25 | 26 | # Install conda system prerequisites, commands based on: https://github.com/conda/conda-docker/blob/master/miniconda3/debian/Dockerfile 27 | RUN apt-get -qq update && apt-get -qq -y install curl bzip2 \ 28 | && apt-get autoclean \ 29 | && rm -rf /var/lib/apt/lists/* /var/log/dpkg.log 30 | 31 | # Install LogicNets system prerequisites 32 | RUN apt-get -qq update && apt-get -qq -y install verilator build-essential libx11-6 git \ 33 | && apt-get autoclean \ 34 | && rm -rf /var/lib/apt/lists/* /var/log/dpkg.log 35 | 36 | # Adding LogicNets dependency on OHMYXILINX 37 | RUN git clone https://bitbucket.org/maltanar/oh-my-xilinx.git 38 | ENV OHMYXILINX=/workspace/oh-my-xilinx 39 | 40 | # Add Nitro-parts-library 41 | RUN git clone https://github.com/dirjud/Nitro-Parts-lib-Xilinx.git 42 | ENV NITROPARTSLIB=/workspace/Nitro-Parts-lib-Xilinx 43 | 44 | # Create the user account to run LogicNets 45 | RUN groupadd -g $GID $GNAME 46 | RUN useradd -m -u $UID $UNAME -g $GNAME 47 | ENV UNAME_HOME=/home/$UNAME 48 | USER $UNAME 49 | ENV USER=$UNAME 50 | ENV HOME=/home/$UNAME 51 | 52 | # Install conda 53 | ENV CONDA_ROOT=$HOME/.local/miniconda3 54 | RUN mkdir -p $CONDA_ROOT 55 | ENV PATH=$CONDA_ROOT/bin:$PATH 56 | RUN curl -sSL https://repo.anaconda.com/miniconda/Miniconda3-py38_4.12.0-Linux-x86_64.sh -o /tmp/miniconda.sh \ 57 | && bash /tmp/miniconda.sh -bfp $CONDA_ROOT \ 58 | && rm -rf /tmp/miniconda.sh \ 59 | && conda install -y python=3.8 \ 60 | && conda update -y conda \ 61 | && conda clean --all --yes 62 | 63 | # Install conda-based prerequisites 64 | # Install LogicNets prerequisites - fails since 2023-07-07: https://github.com/Xilinx/logicnets/issues/34 65 | RUN conda install -y pytorch==1.5.1 torchvision==0.6.1 cpuonly -c pytorch && \ 66 | conda clean -ya 67 | RUN conda install -y pillow && \ 68 | conda clean -ya 69 | # Install Brevitas prerequisites 70 | RUN conda install -y packaging pyparsing && \ 71 | conda clean -ya 72 | RUN conda install -y docrep -c conda-forge && \ 73 | conda clean -ya 74 | # Install prerequisites for oh-my-xilinx 75 | RUN conda install -y zsh -c conda-forge && \ 76 | conda clean -ya 77 | 78 | # Install prerequisites for the JSC/NIDS examples 79 | # Later versions of protobuf (>=3.19) require GLIBC >=3.4 80 | RUN conda install -y wget h5py pyyaml=5 numpy pandas scikit-learn tensorboard protobuf\<3.19 && \ 81 | conda clean -ya 82 | 83 | # Install pip-based prerequisites 84 | ## Install LogicNets prerequisites 85 | #RUN pip install --no-cache-dir torch==1.5.1+cpu torchvision==0.6.1+cpu -f https://download.pytorch.org/whl/torch_stable.html 86 | # Install Brevitas 87 | RUN pip install --no-cache-dir git+https://github.com/Xilinx/brevitas.git@67be9b58c1c63d3923cac430ade2552d0db67ba5 88 | # Install prerequisites for pyverilator 89 | RUN pip install --no-cache-dir pyverilator 90 | 91 | # Install LogicNets library - SKIP this step and install during entry-point 92 | #RUN git clone git@github.com:Xilinx/logicnets.git && \ 93 | # cd logicnets && \ 94 | # pip install --upgrade .[example-all] 95 | 96 | # Add entry point script to install LogicNets and setup vivado. 97 | ENV LOCAL_PATH=$HOME/.local/bin 98 | RUN mkdir -p $LOCAL_PATH 99 | COPY docker/entry-point.sh $LOCAL_PATH 100 | ENV PATH=$LOCAL_PATH:$PATH 101 | ENTRYPOINT ["entry-point.sh"] 102 | CMD ["bash"] 103 | -------------------------------------------------------------------------------- /examples/jet_substructure/dataset.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # Copyright (C) 2020 FastML 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import h5py 17 | import yaml 18 | import numpy as np 19 | import pandas as pd 20 | from sklearn.model_selection import train_test_split 21 | from sklearn import preprocessing 22 | import torch 23 | from torch.utils.data import Dataset 24 | 25 | # Based off example from: https://github.com/hls-fpga-machine-learning/pytorch-training/blob/master/train/Data_loader.py 26 | # Creates a PyTorch Dataset from the h5 file input. 27 | # Returns labels as a one-hot encoded vector. 28 | # Input / output labels are contained in self.feature_labels / self.output_labels respectively 29 | class JetSubstructureDataset(Dataset): 30 | def __init__(self, input_file, config_file, split="train"): 31 | super().__init__() 32 | 33 | self.split = split 34 | 35 | with h5py.File(input_file, 'r') as h5py_file: 36 | tree_array = h5py_file["t_allpar_new"][()] 37 | 38 | with open(config_file, 'r') as f: 39 | self.config = yaml.load(f) 40 | 41 | # TODO: Add warnings about unused dictionary entries 42 | self.feature_labels = self.config["Inputs"] 43 | self.output_labels = self.config["Labels"] 44 | 45 | # Filter input file and convert inputs / outputs to numpy array 46 | dataset_df = pd.DataFrame(tree_array,columns=list(set(self.feature_labels+self.output_labels))) 47 | dataset_df = dataset_df.drop_duplicates() 48 | features_df = dataset_df[self.feature_labels] 49 | outputs_df = dataset_df[self.output_labels] 50 | X = features_df.values 51 | y = outputs_df.values 52 | if "j_index" in self.feature_labels: 53 | X = X[:,:-1] # drop the j_index feature 54 | if "j_index" in self.output_labels: 55 | # drop the j_index label 56 | y = y[:,:-1] 57 | self.output_labels = self.output_labels[:-1] 58 | X_train_val, X_test, y_train_val, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # Using the same dataset split as: https://github.com/hls-fpga-machine-learning/pytorch-training/blob/master/train/Data_loader.py 59 | if self.config["NormalizeInputs"]: 60 | scaler = preprocessing.StandardScaler().fit(X_train_val) 61 | # scaler = preprocessing.MinMaxScaler().fit(X_train_val) 62 | X_train_val = scaler.transform(X_train_val) 63 | X_test = scaler.transform(X_test) 64 | 65 | if self.config["ApplyPca"]: 66 | # Apply dimenionality reduction to the inputs 67 | with torch.no_grad(): 68 | dim = self.config["PcaDimensions"] 69 | X_train_val_fp64 = torch.from_numpy(X_train_val).double() 70 | X_test_fp64 = torch.from_numpy(X_test).double() 71 | U,S,V = torch.svd(X_train_val_fp64) 72 | X_train_val_pca_fp64 = torch.mm(X_train_val_fp64, V[:,0:dim]) 73 | X_test_pca_fp64 = torch.mm(X_test_fp64, V[:,0:dim]) 74 | variance_retained = 100*(S[0:dim].sum() / S.sum()) 75 | print(f"Dimensions used for PCA: {dim}") 76 | print(f"Variance retained (%): {variance_retained}") 77 | X_train_val = X_train_val_pca_fp64.float().numpy() 78 | X_test = X_test_pca_fp64.float().numpy() 79 | 80 | if self.split == "train": 81 | self.X = torch.from_numpy(X_train_val) 82 | self.y = torch.from_numpy(y_train_val) 83 | elif self.split == "test": 84 | self.X = torch.from_numpy(X_test) 85 | self.y = torch.from_numpy(y_test) 86 | 87 | def __len__(self): 88 | return len(self.X) 89 | 90 | def __getitem__(self, idx): 91 | return (self.X[idx], self.y[idx]) 92 | 93 | -------------------------------------------------------------------------------- /src/logicnets/verilog.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | def generate_register_verilog(module_name="myreg", param_name="DataWidth", input_name="data_in", output_name="data_out"): 16 | register_template = """\ 17 | module {module_name} #(parameter {param_name}=16) ( 18 | input [{param_name}-1:0] {input_name}, 19 | input wire clk, 20 | input wire rst, 21 | output reg [{param_name}-1:0] {output_name} 22 | ); 23 | always@(posedge clk) begin 24 | if(!rst) 25 | {output_name}<={input_name}; 26 | else 27 | {output_name}<=0; 28 | end 29 | endmodule\n 30 | """ 31 | return register_template.format( module_name=module_name, 32 | param_name=param_name, 33 | input_name=input_name, 34 | output_name=output_name) 35 | 36 | def generate_logicnets_verilog(module_name: str, input_name: str, input_bits: int, output_name: str, output_bits: int, module_contents: str): 37 | logicnets_template = """\ 38 | module {module_name} (input [{input_bits_1:d}:0] {input_name}, input clk, input rst, output[{output_bits_1:d}:0] {output_name}); 39 | {module_contents} 40 | endmodule\n""" 41 | return logicnets_template.format( module_name=module_name, 42 | input_name=input_name, 43 | input_bits_1=input_bits-1, 44 | output_name=output_name, 45 | output_bits_1=output_bits-1, 46 | module_contents=module_contents) 47 | 48 | def layer_connection_verilog(layer_string: str, input_string: str, input_bits: int, output_string: str, output_bits: int, output_wire=True, register=False): 49 | if register: 50 | layer_connection_template = """\ 51 | wire [{input_bits_1:d}:0] {input_string}w; 52 | myreg #(.DataWidth({input_bits})) {layer_string}_reg (.data_in({input_string}), .clk(clk), .rst(rst), .data_out({input_string}w));\n""" 53 | else: 54 | layer_connection_template = """\ 55 | wire [{input_bits_1:d}:0] {input_string}w; 56 | assign {input_string}w = {input_string};\n""" 57 | layer_connection_template += "wire [{output_bits_1:d}:0] {output_string};\n" if output_wire else "" 58 | layer_connection_template += "{layer_string} {layer_string}_inst (.M0({input_string}w), .M1({output_string}));\n" 59 | return layer_connection_template.format( layer_string=layer_string, 60 | input_string=input_string, 61 | input_bits=input_bits, 62 | input_bits_1=input_bits-1, 63 | output_string=output_string, 64 | output_bits_1=output_bits-1) 65 | 66 | def generate_lut_verilog(module_name, input_fanin_bits, output_bits, lut_string): 67 | lut_neuron_template = """\ 68 | module {module_name} ( input [{input_fanin_bits_1:d}:0] M0, output [{output_bits_1:d}:0] M1 ); 69 | 70 | (*rom_style = "distributed" *) reg [{output_bits_1:d}:0] M1r; 71 | assign M1 = M1r; 72 | always @ (M0) begin 73 | case (M0) 74 | {lut_string} 75 | endcase 76 | end 77 | endmodule\n""" 78 | return lut_neuron_template.format( module_name=module_name, 79 | input_fanin_bits_1=input_fanin_bits-1, 80 | output_bits_1=output_bits-1, 81 | lut_string=lut_string) 82 | 83 | def generate_neuron_connection_verilog(input_indices, input_bitwidth): 84 | connection_string = "" 85 | for i in range(len(input_indices)): 86 | index = input_indices[i] 87 | offset = index*input_bitwidth 88 | for b in reversed(range(input_bitwidth)): 89 | connection_string += f"M0[{offset+b}]" 90 | if not (i == len(input_indices)-1 and b == 0): 91 | connection_string += ", " 92 | return connection_string 93 | 94 | -------------------------------------------------------------------------------- /src/logicnets/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from functools import reduce 16 | 17 | import os 18 | import subprocess 19 | 20 | import torch 21 | 22 | # Return the indices associated with a '1' value 23 | # TODO: vectorise this function 24 | def fetch_mask_indices(mask: torch.Tensor) -> torch.LongTensor: 25 | local_mask = mask.detach().clone() 26 | fan_in = torch.sum(local_mask, dtype=torch.int64) 27 | indices = [0]*fan_in 28 | for i in range(fan_in): 29 | ind = torch.argmax(local_mask) 30 | indices[i] = ind 31 | local_mask[ind] = 0 32 | return tuple(indices) 33 | 34 | # Return a matrix which contains all input permutations 35 | # TODO: implement this function 36 | def generate_permutation_matrix(input_state_space) -> torch.Tensor: 37 | total_permutations = reduce(lambda a,b: a*b, map(lambda x: x.nelement(), input_state_space)) # Calculate the total number of permutations 38 | fan_in = len(input_state_space) 39 | permutations_matrix = torch.zeros((total_permutations,fan_in)) 40 | # TODO: is there a way to do this that is vectorised? 41 | for p in range(total_permutations): 42 | next_perm = p 43 | for f in range(fan_in): 44 | div_factor = input_state_space[f].nelement() 45 | index = next_perm % div_factor 46 | permutations_matrix[p,f] = input_state_space[f][index] 47 | next_perm = next_perm // div_factor 48 | return permutations_matrix 49 | 50 | # Prepare a directory for simulating post-synthesis verilog from Vivado. 51 | # Remove unused top-level entity that's produced by Vivado. 52 | # Move verilog files from the NITROPARTS library to the newly created directory. 53 | # TODO: Add assertions if nitroparts library doesn't exist, etc. 54 | # TODO: Instead of copying the nitroparts library to the directory, simply add a search path to pyverilator 55 | def proc_postsynth_file(code_dir): 56 | # The post synthesis file ("logicnet_post") needs some preparation work. 57 | # Two top level modules are included, "logicnets" and "GLBL". We do not need "GLBL", so we are deleting it. 58 | post_synth_file = open("%s/results_logicnet/logicnet_post_synth.v"%(code_dir)) 59 | post_synth_list = post_synth_file.readlines() 60 | post_synth_list_len = len(post_synth_list) 61 | post_synth_list_index = post_synth_list.index("`ifndef GLBL\n") 62 | post_synth_list_offset = post_synth_list_len - post_synth_list_index 63 | post_synth_list = post_synth_list[:-post_synth_list_offset] 64 | post_synth_file.close() 65 | post_synth_file = open("%s/results_logicnet/logicnet_post_synth.v"%(code_dir),"w") 66 | for element in post_synth_list: 67 | post_synth_file.write(element) 68 | post_synth_file.close() 69 | # Create post-synthesis simulation folder called "post_synthesis" 70 | call_omx = "mkdir %s/post_synth" % (code_dir) 71 | call_omx = call_omx.split() 72 | proc = subprocess.Popen(call_omx, stdout=subprocess.PIPE, env=os.environ) 73 | proc.communicate() 74 | # Copy post-synthesis Verilog file into the post-synthesis simulation folder 75 | call_omx = "cp %s/results_logicnet/logicnet_post_synth.v %s/post_synth/." % (code_dir, code_dir) 76 | call_omx = call_omx.split() 77 | proc = subprocess.Popen(call_omx, stdout=subprocess.PIPE, env=os.environ) 78 | proc.communicate() 79 | # Read "NITROPARTSLIB" environment variable and copy files into simulation folder 80 | npl_env = os.environ["NITROPARTSLIB"] 81 | call_omx = "cp -a %s/. %s/post_synth/." % (npl_env, code_dir) 82 | call_omx = call_omx.split() 83 | proc = subprocess.Popen(call_omx, stdout=subprocess.PIPE, env=os.environ) 84 | proc.communicate() 85 | 86 | def get_lut_cost(model): 87 | """ 88 | Compute LUTCost of the given model 89 | """ 90 | from .nn import SparseLinearNeq 91 | # Prevent circular import 92 | total_lut_cost = 0 93 | for _, module in model.named_modules(): 94 | if type(module) == SparseLinearNeq: 95 | lut_cost = module.lut_cost() 96 | total_lut_cost = total_lut_cost + lut_cost 97 | return total_lut_cost 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LogicNets: Co-Designed Neural Networks and Circuits for Extreme-Throughput Applications 2 | 3 | LogicNets is a methodology for designing, training and deploying sparse, 4 | quantized neural networks based on hardware building blocks. 5 | They are able to achieve extremely high throughput and low latency on Xilinx FPGAs. 6 | 7 | ## Description 8 | 9 | This project provides a set of library components and examples to help you design, 10 | train and deploy your own LogicNets networks. 11 | Note, this is considered to be an \'alpha\' release: expect API changes and issues. 12 | You can learn more about LogicNets from [this paper](https://arxiv.org/abs/2004.03021), or from the following YouTube videos: 13 | * [10-minute presentation at DATE'21](https://www.youtube.com/watch?v=xtWySQ-ehRw) 14 | * [20-minute presentation at FPL'20](https://www.youtube.com/watch?v=qCyK5v84jpI) 15 | 16 | 17 | ## Prerequisites 18 | 19 | - [Python\>=3](https://www.python.org/) 20 | - [PyTorch](https://pytorch.org/) 21 | - [Brevitas](https://github.com/Xilinx/brevitas) 22 | - [Verilator](https://www.veripool.org/wiki/verilator) 23 | - [PyVerilator](https://github.com/csail-csg/pyverilator) 24 | - [oh-my-xilinx](https://bitbucket.org/maltanar/oh-my-xilinx/) 25 | - [Vivado Design 26 | Suite](https://www.xilinx.com/products/design-tools/vivado.html) 27 | 28 | ## Installation 29 | 30 | There are two main ways to install LogicNets: 31 | 32 | 1. Using [docker](https://www.docker.com/) (recommended) 33 | 2. Manually (advanced) 34 | 35 | ### Docker Installation 36 | 37 | Follow the steps below to install LogicNets within a `docker` container: 38 | 39 | 1. Install [Vivado Design Suite](https://www.xilinx.com/products/design-tools/vivado.html) 40 | 1. Install [docker](https://www.docker.com/). 41 | 1. Clone the LogicNets repository using: `git clone git@github.com:Xilinx/logicnets.git` 42 | 1. Create an environment variable `VIVADO_SETTINGS_FILE` which points to the `settings64.sh` in your Vivado installation (e.g., `export VIVADO_SETTINGS_FILE=/path/to/Vivado/settings64.sh`) 43 | 1. (Optional): Set your `XILINXD_LICENSE_FILE` or `LM_LICENSE_FILE` variables to point to a specific license server 44 | 1. Run `./docker/run-docker.sh` from the root directory of the LogicNets repository 45 | 1. (Optional): Copy your Vivado license file over to the running docker container 46 | 47 | You are now ready to run an example, browse to `/workspace/logicnets/examples/jet_substructure/` and start training a LogicNets network! 48 | 49 | Note, a license is not required to run any of the provided examples. 50 | 51 | ### Manual Installation 52 | 53 | A manual installation is not recommended for beginners. 54 | We will only provide limited support for people wishing to pursue a manual installation. 55 | People wanting to pursue a manual installation may want to look at the LogicNets Dockerfile, 56 | as this contains details for a full environmental setup (including the installation of dependencies of dependencies). 57 | 58 | The basic steps for a manual installation are as follows: 59 | 60 | 1. Install [Vivado Design Suite](https://www.xilinx.com/products/design-tools/vivado.html) 61 | 1. Install `oh-my-xilinx` by cloning this [repository](https://bitbucket.org/maltanar/oh-my-xilinx.git) and creating the environment variable `OHMYXILINX` which points to its root directory (e.g., `export OHMYXILINX=/path/to/oh-my-xilinx`) 62 | * `oh-my-xilinx` requires `zsh`, on Ubuntu, this can be installed as follows: `sudo apt-get install zsh` 63 | 1. Install `Nitro-Parts-lib-Xilinx` by cloning this [repository](https://github.com/dirjud/Nitro-Parts-lib-Xilinx.git) and creating the environment variable `NITROPARTSLIB` which points to its root directory (e.g., `export NITROPARTSLIB=/path/to/Nitro-Parts-lib-Xilinx`) 64 | 1. Install `verilator`, on Ubuntu, this can be done as follows: `sudo apt-get install verilator build-essential` 65 | 1. Install `python3`, `pytorch`, `brevitas` and `pyverilator` using `conda` and/or `pip` or your preferred method 66 | 1. Install `logicnets` using: `pip install --upgrade git+https://github.com/Xilinx/logicnets.git` 67 | 68 | Alternatively, instead of the last step you can clone the LogicNets repository and install as follows: 69 | 70 | ```bash 71 | pip install . 72 | ``` 73 | 74 | Note, to install the dependencies for all the examples, you can run the following: 75 | 76 | ```bash 77 | pip install .[example-all] 78 | ``` 79 | 80 | ## Examples 81 | 82 | Currently, we have the following examples, located in `./examples/`: 83 | * [Jet substructure classification](examples/jet_substructure/) 84 | * [Network intrusion detection](examples/cybersecurity/) 85 | 86 | More examples coming soon. 87 | 88 | ## Documentation 89 | 90 | Documentation site coming soon. 91 | 92 | ## Citation 93 | 94 | If you find this work useful for your research, please consider citing 95 | [our paper](https://arxiv.org/abs/2004.03021) from FPL'20 below: 96 | 97 | ```bibtex 98 | @inproceedings{umuroglu2020logicnets, 99 | author = {Umuroglu, Yaman and Akhauri, Yash and Fraser, Nicholas J and Blott, Michaela}, 100 | booktitle = {Proceedings of the International Conference on Field-Programmable Logic and Applications}, 101 | title = {LogicNets: Co-Designed Neural Networks and Circuits for Extreme-Throughput Applications}, 102 | year = {2020}, 103 | pages = {291-297}, 104 | publisher = {IEEE Computer Society}, 105 | address = {Los Alamitos, CA, USA}, 106 | month = {sep} 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /examples/cybersecurity/dataset_dump.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from argparse import ArgumentParser 17 | from functools import reduce 18 | 19 | import torch 20 | from torch.utils.data import DataLoader 21 | 22 | from logicnets.nn import generate_truth_tables, \ 23 | lut_inference, \ 24 | module_list_to_verilog_module 25 | from logicnets.synthesis import synthesize_and_get_resource_counts 26 | 27 | from train import configs, model_config, dataset_config, other_options, test 28 | from dataset import get_preqnt_dataset 29 | from models import UnswNb15NeqModel 30 | 31 | def dump_io(model, data_loader, input_file, output_file): 32 | input_quant = model.module_list[0].input_quant 33 | _, input_bitwidth = input_quant.get_scale_factor_bits() 34 | input_bitwidth = int(input_bitwidth) 35 | total_input_bits = model.module_list[0].in_features*input_bitwidth 36 | input_quant.bin_output() 37 | with open(input_file, 'w') as i_f, open(output_file, 'w') as o_f: 38 | for data, target in data_loader: 39 | x = input_quant(data) 40 | indices = target 41 | for i in range(x.shape[0]): 42 | x_i = x[i,:] 43 | xv_i = list(map(lambda z: input_quant.get_bin_str(z), x_i)) 44 | xvc_i = reduce(lambda a,b: a+b, xv_i[::-1]) 45 | i_f.write(f"{int(xvc_i,2):0{int(total_input_bits)}b}\n") 46 | o_f.write(f"{int(indices[i])}\n") 47 | 48 | if __name__ == "__main__": 49 | parser = ArgumentParser(description="Dump the train and test datasets (after input quantization) into text files") 50 | parser.add_argument('--arch', type=str, choices=configs.keys(), default="nid-s", 51 | help="Specific the neural network model to use (default: %(default)s)") 52 | parser.add_argument('--batch-size', type=int, default=None, metavar='N', 53 | help="Batch size for evaluation (default: %(default)s)") 54 | parser.add_argument('--input-bitwidth', type=int, default=None, 55 | help="Bitwidth to use at the input (default: %(default)s)") 56 | parser.add_argument('--hidden-bitwidth', type=int, default=None, 57 | help="Bitwidth to use for activations in hidden layers (default: %(default)s)") 58 | parser.add_argument('--output-bitwidth', type=int, default=None, 59 | help="Bitwidth to use at the output (default: %(default)s)") 60 | parser.add_argument('--input-fanin', type=int, default=None, 61 | help="Fanin to use at the input (default: %(default)s)") 62 | parser.add_argument('--hidden-fanin', type=int, default=None, 63 | help="Fanin to use for the hidden layers (default: %(default)s)") 64 | parser.add_argument('--output-fanin', type=int, default=None, 65 | help="Fanin to use at the output (default: %(default)s)") 66 | parser.add_argument('--hidden-layers', nargs='+', type=int, default=None, 67 | help="A list of hidden layer neuron sizes (default: %(default)s)") 68 | parser.add_argument('--dataset-file', type=str, default='data/unsw_nb15_binarized.npz', 69 | help="The file to use as the dataset input (default: %(default)s)") 70 | parser.add_argument('--log-dir', type=str, default='./log', 71 | help="A location to store the output I/O text files (default: %(default)s)") 72 | parser.add_argument('--checkpoint', type=str, required=True, 73 | help="The checkpoint file which contains the model weights") 74 | args = parser.parse_args() 75 | defaults = configs[args.arch] 76 | options = vars(args) 77 | del options['arch'] 78 | config = {} 79 | for k in options.keys(): 80 | config[k] = options[k] if options[k] is not None else defaults[k] # Override defaults, if specified. 81 | 82 | if not os.path.exists(config['log_dir']): 83 | os.makedirs(config['log_dir']) 84 | 85 | # Split up configuration options to be more understandable 86 | model_cfg = {} 87 | for k in model_config.keys(): 88 | model_cfg[k] = config[k] 89 | dataset_cfg = {} 90 | for k in dataset_config.keys(): 91 | dataset_cfg[k] = config[k] 92 | options_cfg = {} 93 | for k in other_options.keys(): 94 | if k == 'cuda': 95 | continue 96 | options_cfg[k] = config[k] 97 | 98 | # Fetch the datasets 99 | dataset = {} 100 | dataset['train'] = get_preqnt_dataset(dataset_cfg['dataset_file'], split="train") 101 | dataset['test'] = get_preqnt_dataset(dataset_cfg['dataset_file'], split="test") 102 | train_loader = DataLoader(dataset["train"], batch_size=config['batch_size'], shuffle=False) 103 | test_loader = DataLoader(dataset["test"], batch_size=config['batch_size'], shuffle=False) 104 | 105 | # Instantiate the PyTorch model 106 | x, y = dataset["train"][0] 107 | model_cfg['input_length'] = len(x) 108 | model_cfg['output_length'] = 1 109 | model = UnswNb15NeqModel(model_cfg) 110 | 111 | # Load the model weights 112 | checkpoint = torch.load(options_cfg['checkpoint'], map_location='cpu') 113 | model.load_state_dict(checkpoint['model_dict']) 114 | 115 | # Test the PyTorch model 116 | print("Running inference on baseline model...") 117 | model.eval() 118 | baseline_accuracy = test(model, test_loader, cuda=False) 119 | print("Baseline accuracy: %f" % (baseline_accuracy)) 120 | 121 | # Run preprocessing on training set. 122 | train_input_file = config['log_dir'] + "/train_input.txt" 123 | train_output_file = config['log_dir'] + "/train_output.txt" 124 | test_input_file = config['log_dir'] + "/test_input.txt" 125 | test_output_file = config['log_dir'] + "/test_output.txt" 126 | print(f"Dumping train I/O to {train_input_file} and {train_output_file}") 127 | dump_io(model, train_loader, train_input_file, train_output_file) 128 | print(f"Dumping test I/O to {test_input_file} and {test_output_file}") 129 | dump_io(model, test_loader, test_input_file, test_output_file) 130 | -------------------------------------------------------------------------------- /examples/jet_substructure/dataset_dump.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from argparse import ArgumentParser 17 | from functools import reduce 18 | 19 | import torch 20 | from torch.utils.data import DataLoader 21 | 22 | from logicnets.nn import generate_truth_tables, \ 23 | lut_inference, \ 24 | module_list_to_verilog_module 25 | 26 | from train import configs, model_config, dataset_config, other_options, test 27 | from dataset import JetSubstructureDataset 28 | from models import JetSubstructureNeqModel, JetSubstructureLutModel 29 | from logicnets.synthesis import synthesize_and_get_resource_counts 30 | 31 | def dump_io(model, data_loader, input_file, output_file): 32 | input_quant = model.module_list[0].input_quant 33 | _, input_bitwidth = input_quant.get_scale_factor_bits() 34 | input_bitwidth = int(input_bitwidth) 35 | total_input_bits = model.module_list[0].in_features*input_bitwidth 36 | input_quant.bin_output() 37 | with open(input_file, 'w') as i_f, open(output_file, 'w') as o_f: 38 | for data, target in data_loader: 39 | x = input_quant(data) 40 | indices = torch.argmax(target,dim=1) 41 | for i in range(x.shape[0]): 42 | x_i = x[i,:] 43 | xv_i = list(map(lambda z: input_quant.get_bin_str(z), x_i)) 44 | xvc_i = reduce(lambda a,b: a+b, xv_i[::-1]) 45 | i_f.write(f"{int(xvc_i,2):0{int(total_input_bits)}b}\n") 46 | o_f.write(f"{int(indices[i])}\n") 47 | 48 | if __name__ == "__main__": 49 | parser = ArgumentParser(description="Dump the train and test datasets (after input quantization) into text files") 50 | parser.add_argument('--arch', type=str, choices=configs.keys(), default="jsc-s", 51 | help="Specific the neural network model to use (default: %(default)s)") 52 | parser.add_argument('--batch-size', type=int, default=None, metavar='N', 53 | help="Batch size for evaluation (default: %(default)s)") 54 | parser.add_argument('--input-bitwidth', type=int, default=None, 55 | help="Bitwidth to use at the input (default: %(default)s)") 56 | parser.add_argument('--hidden-bitwidth', type=int, default=None, 57 | help="Bitwidth to use for activations in hidden layers (default: %(default)s)") 58 | parser.add_argument('--output-bitwidth', type=int, default=None, 59 | help="Bitwidth to use at the output (default: %(default)s)") 60 | parser.add_argument('--input-fanin', type=int, default=None, 61 | help="Fanin to use at the input (default: %(default)s)") 62 | parser.add_argument('--hidden-fanin', type=int, default=None, 63 | help="Fanin to use for the hidden layers (default: %(default)s)") 64 | parser.add_argument('--output-fanin', type=int, default=None, 65 | help="Fanin to use at the output (default: %(default)s)") 66 | parser.add_argument('--hidden-layers', nargs='+', type=int, default=None, 67 | help="A list of hidden layer neuron sizes (default: %(default)s)") 68 | parser.add_argument('--dataset-file', type=str, default='data/processed-pythia82-lhc13-all-pt1-50k-r1_h022_e0175_t220_nonu_truth.z', 69 | help="The file to use as the dataset input (default: %(default)s)") 70 | parser.add_argument('--dataset-config', type=str, default='config/yaml_IP_OP_config.yml', 71 | help="The file to use to configure the input dataset (default: %(default)s)") 72 | parser.add_argument('--log-dir', type=str, default='./log', 73 | help="A location to store the output I/O text files (default: %(default)s)") 74 | parser.add_argument('--checkpoint', type=str, required=True, 75 | help="The checkpoint file which contains the model weights") 76 | args = parser.parse_args() 77 | defaults = configs[args.arch] 78 | options = vars(args) 79 | del options['arch'] 80 | config = {} 81 | for k in options.keys(): 82 | config[k] = options[k] if options[k] is not None else defaults[k] # Override defaults, if specified. 83 | 84 | if not os.path.exists(config['log_dir']): 85 | os.makedirs(config['log_dir']) 86 | 87 | # Split up configuration options to be more understandable 88 | model_cfg = {} 89 | for k in model_config.keys(): 90 | model_cfg[k] = config[k] 91 | dataset_cfg = {} 92 | for k in dataset_config.keys(): 93 | dataset_cfg[k] = config[k] 94 | options_cfg = {} 95 | for k in other_options.keys(): 96 | if k == 'cuda': 97 | continue 98 | options_cfg[k] = config[k] 99 | 100 | # Fetch the test set 101 | dataset = {} 102 | dataset["train"] = JetSubstructureDataset(dataset_cfg['dataset_file'], dataset_cfg['dataset_config'], split="train") 103 | dataset["test"] = JetSubstructureDataset(dataset_cfg['dataset_file'], dataset_cfg['dataset_config'], split="test") 104 | train_loader = DataLoader(dataset["train"], batch_size=config['batch_size'], shuffle=False) 105 | test_loader = DataLoader(dataset["test"], batch_size=config['batch_size'], shuffle=False) 106 | 107 | # Instantiate the PyTorch model 108 | x, y = dataset["train"][0] 109 | model_cfg['input_length'] = len(x) 110 | model_cfg['output_length'] = len(y) 111 | model = JetSubstructureNeqModel(model_cfg) 112 | 113 | # Load the model weights 114 | checkpoint = torch.load(options_cfg['checkpoint'], map_location='cpu') 115 | model.load_state_dict(checkpoint['model_dict']) 116 | 117 | # Test the PyTorch model 118 | print("Running inference on baseline model...") 119 | model.eval() 120 | baseline_accuracy, baseline_avg_roc_auc = test(model, test_loader, cuda=False) 121 | print("Baseline accuracy: %f" % (baseline_accuracy)) 122 | print("Baseline AVG ROC AUC: %f" % (baseline_avg_roc_auc)) 123 | 124 | # Run preprocessing on training set. 125 | train_input_file = config['log_dir'] + "/train_input.txt" 126 | train_output_file = config['log_dir'] + "/train_output.txt" 127 | test_input_file = config['log_dir'] + "/test_input.txt" 128 | test_output_file = config['log_dir'] + "/test_output.txt" 129 | print(f"Dumping train I/O to {train_input_file} and {train_output_file}") 130 | dump_io(model, train_loader, train_input_file, train_output_file) 131 | print(f"Dumping test I/O to {test_input_file} and {test_output_file}") 132 | dump_io(model, test_loader, test_input_file, test_output_file) 133 | -------------------------------------------------------------------------------- /src/logicnets/quant.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import torch 16 | import torch.nn as nn 17 | 18 | from brevitas.core.quant import QuantType 19 | from brevitas.core.quant import RescalingIntQuant, ClampedBinaryQuant 20 | from brevitas.core.scaling import ScalingImplType 21 | import brevitas.nn as bnn 22 | 23 | # TODO: Put this inside an abstract base class 24 | def get_int_state_space(bits: int, signed: bool, narrow_range: bool): 25 | start = int(0 if not signed else (-2**(bits-1) + int(narrow_range))) # calculate the minimum value in the range 26 | end = int(start + 2**(bits) - int(narrow_range)) # calculate the maximum of the range 27 | state_space = torch.as_tensor(range(start, end)) 28 | return state_space 29 | 30 | # TODO: Put this inside an abstract base class 31 | def get_float_state_space(bits: int, scale_factor: float, signed: bool, narrow_range: bool, quant_type: QuantType): 32 | if quant_type == QuantType.INT: 33 | bin_state_space = get_int_state_space(bits, signed, narrow_range) 34 | elif quant_type == QuantType.BINARY: 35 | bin_state_space = torch.as_tensor([-1., 1.]) 36 | state_space = scale_factor * bin_state_space 37 | return state_space 38 | 39 | # TODO: Add an abstract class with a specific interface which all brevitas-based classes inherit from? 40 | class QuantBrevitasActivation(nn.Module): 41 | 42 | def __init__(self, brevitas_module, pre_transforms: list = [], post_transforms: list = []): 43 | super(QuantBrevitasActivation, self).__init__() 44 | self.brevitas_module = brevitas_module 45 | self.pre_transforms = nn.ModuleList(pre_transforms) 46 | self.post_transforms = nn.ModuleList(post_transforms) 47 | self.is_bin_output = False 48 | 49 | # TODO: Move to a base class 50 | # TODO: Move the string templates to verilog.py 51 | def get_bin_str(self, x: int): 52 | quant_type = self.get_quant_type() 53 | scale_factor, bits = self.get_scale_factor_bits() 54 | if quant_type == QuantType.INT: 55 | tensor_quant = self.brevitas_module.act_quant_proxy.fused_activation_quant_proxy.tensor_quant 56 | narrow_range = tensor_quant.int_quant.narrow_range 57 | signed = tensor_quant.int_quant.signed 58 | offset = 2**(bits-1) -int(narrow_range) if signed else 0 59 | return f"{int(x+offset):0{int(bits)}b}" 60 | elif quant_type == QuantType.BINARY: 61 | return f"{int(x):0{int(bits)}b}" 62 | else: 63 | raise Exception("Unknown quantization type: {}".format(quant_type)) 64 | 65 | # TODO: Move to a base class 66 | def bin_output(self): 67 | self.is_bin_output = True 68 | 69 | # TODO: Move to a base class 70 | def float_output(self): 71 | self.is_bin_output = False 72 | 73 | def get_quant_type(self): 74 | brevitas_module_type = type(self.brevitas_module.act_quant_proxy.fused_activation_quant_proxy.tensor_quant) 75 | if brevitas_module_type == RescalingIntQuant: 76 | return QuantType.INT 77 | elif brevitas_module_type == ClampedBinaryQuant: 78 | return QuantType.BINARY 79 | else: 80 | raise Exception("Unknown quantization type for tensor_quant: {}".format(brevitas_module_type)) 81 | 82 | # TODO: Allow for different bitwidths / scales per output 83 | def get_scale_factor_bits(self): 84 | # TODO: put guards in this based on quantization type 85 | quant_proxy = self.brevitas_module.act_quant_proxy 86 | current_status = quant_proxy.training 87 | quant_proxy.eval() 88 | _, scale_factor, bits = quant_proxy(quant_proxy.zero_hw_sentinel) 89 | quant_proxy.training = current_status 90 | return scale_factor, bits 91 | 92 | # Return a floating point version of the state space, this return values 93 | # that PyTorch would see at the output of this layer during training. 94 | # TODO: Merge this function with 'get_bin_state_space' and remove duplicated code. 95 | def get_state_space(self): 96 | quant_type = self.get_quant_type() 97 | scale_factor, bits = self.get_scale_factor_bits() 98 | if quant_type == QuantType.INT: 99 | tensor_quant = self.brevitas_module.act_quant_proxy.fused_activation_quant_proxy.tensor_quant 100 | narrow_range = tensor_quant.int_quant.narrow_range 101 | signed = tensor_quant.int_quant.signed 102 | state_space = get_float_state_space(bits, scale_factor, signed, narrow_range, quant_type) 103 | elif quant_type == QuantType.BINARY: 104 | state_space = scale_factor*torch.tensor([-1, 1]) 105 | else: 106 | raise Exception("Unknown quantization type: {}".format(quant_type)) 107 | return self.apply_post_transforms(state_space) 108 | 109 | # Return the underlying binary representation of the values returned by 110 | # 'get_state_space' 111 | def get_bin_state_space(self): 112 | quant_type = self.get_quant_type() 113 | _, bits = self.get_scale_factor_bits() 114 | if quant_type == QuantType.INT: 115 | tensor_quant = self.brevitas_module.act_quant_proxy.fused_activation_quant_proxy.tensor_quant 116 | narrow_range = tensor_quant.int_quant.narrow_range 117 | signed = tensor_quant.int_quant.signed 118 | state_space = get_int_state_space(bits, signed, narrow_range) 119 | elif quant_type == QuantType.BINARY: 120 | state_space = torch.tensor([0, 1]) 121 | else: 122 | raise Exception("Unknown quantization type: {}".format(quant_type)) 123 | return state_space 124 | 125 | def apply_pre_transforms(self, x): 126 | for i in range(len(self.pre_transforms)): 127 | x = self.pre_transforms[i](x) 128 | return x 129 | 130 | def apply_post_transforms(self, x): 131 | for i in range(len(self.post_transforms)): 132 | x = self.post_transforms[i](x) 133 | return x 134 | 135 | def forward(self, x): 136 | if self.is_bin_output: 137 | s, _ = self.get_scale_factor_bits() 138 | x = self.apply_pre_transforms(x) 139 | x = self.brevitas_module(x) 140 | x = torch.round(x/s).type(torch.int64) 141 | else: 142 | x = self.apply_pre_transforms(x) 143 | x = self.brevitas_module(x) 144 | x = self.apply_post_transforms(x) 145 | return x 146 | 147 | -------------------------------------------------------------------------------- /examples/jet_substructure/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from functools import reduce 16 | from os.path import realpath 17 | 18 | import torch 19 | import torch.nn as nn 20 | from torch.nn.parameter import Parameter 21 | from torch.nn import init 22 | 23 | from brevitas.core.quant import QuantType 24 | from brevitas.core.scaling import ScalingImplType 25 | from brevitas.nn import QuantHardTanh, QuantReLU 26 | 27 | from pyverilator import PyVerilator 28 | 29 | from logicnets.quant import QuantBrevitasActivation 30 | from logicnets.nn import SparseLinearNeq, ScalarBiasScale, RandomFixedSparsityMask2D 31 | from logicnets.init import random_restrict_fanin 32 | 33 | class JetSubstructureNeqModel(nn.Module): 34 | def __init__(self, model_config): 35 | super(JetSubstructureNeqModel, self).__init__() 36 | self.model_config = model_config 37 | self.num_neurons = [model_config["input_length"]] + model_config["hidden_layers"] + [model_config["output_length"]] 38 | layer_list = [] 39 | for i in range(1, len(self.num_neurons)): 40 | in_features = self.num_neurons[i-1] 41 | out_features = self.num_neurons[i] 42 | bn = nn.BatchNorm1d(out_features) 43 | if i == 1: 44 | bn_in = nn.BatchNorm1d(in_features) 45 | input_bias = ScalarBiasScale(scale=False, bias_init=-0.25) 46 | input_quant = QuantBrevitasActivation(QuantHardTanh(model_config["input_bitwidth"], max_val=1., narrow_range=False, quant_type=QuantType.INT, scaling_impl_type=ScalingImplType.PARAMETER), pre_transforms=[bn_in, input_bias]) 47 | output_quant = QuantBrevitasActivation(QuantReLU(bit_width=model_config["hidden_bitwidth"], max_val=1.61, quant_type=QuantType.INT, scaling_impl_type=ScalingImplType.PARAMETER), pre_transforms=[bn]) 48 | mask = RandomFixedSparsityMask2D(in_features, out_features, fan_in=model_config["input_fanin"]) 49 | layer = SparseLinearNeq(in_features, out_features, input_quant=input_quant, output_quant=output_quant, sparse_linear_kws={'mask': mask}) 50 | layer_list.append(layer) 51 | elif i == len(self.num_neurons)-1: 52 | output_bias_scale = ScalarBiasScale(bias_init=0.33) 53 | output_quant = QuantBrevitasActivation(QuantHardTanh(bit_width=model_config["output_bitwidth"], max_val=1.33, narrow_range=False, quant_type=QuantType.INT, scaling_impl_type=ScalingImplType.PARAMETER), pre_transforms=[bn], post_transforms=[output_bias_scale]) 54 | mask = RandomFixedSparsityMask2D(in_features, out_features, fan_in=model_config["output_fanin"]) 55 | layer = SparseLinearNeq(in_features, out_features, input_quant=layer_list[-1].output_quant, output_quant=output_quant, sparse_linear_kws={'mask': mask}, apply_input_quant=False) 56 | layer_list.append(layer) 57 | else: 58 | output_quant = QuantBrevitasActivation(QuantReLU(bit_width=model_config["hidden_bitwidth"], max_val=1.61, quant_type=QuantType.INT, scaling_impl_type=ScalingImplType.PARAMETER), pre_transforms=[bn]) 59 | mask = RandomFixedSparsityMask2D(in_features, out_features, fan_in=model_config["hidden_fanin"]) 60 | layer = SparseLinearNeq(in_features, out_features, input_quant=layer_list[-1].output_quant, output_quant=output_quant, sparse_linear_kws={'mask': mask}, apply_input_quant=False) 61 | layer_list.append(layer) 62 | self.module_list = nn.ModuleList(layer_list) 63 | self.is_verilog_inference = False 64 | self.latency = 1 65 | self.verilog_dir = None 66 | self.top_module_filename = None 67 | self.dut = None 68 | self.logfile = None 69 | 70 | def verilog_inference(self, verilog_dir, top_module_filename, logfile: bool = False, add_registers: bool = False): 71 | self.verilog_dir = realpath(verilog_dir) 72 | self.top_module_filename = top_module_filename 73 | self.dut = PyVerilator.build(f"{self.verilog_dir}/{self.top_module_filename}", verilog_path=[self.verilog_dir], build_dir=f"{self.verilog_dir}/verilator") 74 | self.is_verilog_inference = True 75 | self.logfile = logfile 76 | if add_registers: 77 | self.latency = len(self.num_neurons) 78 | 79 | def pytorch_inference(self): 80 | self.is_verilog_inference = False 81 | 82 | def verilog_forward(self, x): 83 | # Get integer output from the first layer 84 | input_quant = self.module_list[0].input_quant 85 | output_quant = self.module_list[-1].output_quant 86 | _, input_bitwidth = self.module_list[0].input_quant.get_scale_factor_bits() 87 | _, output_bitwidth = self.module_list[-1].output_quant.get_scale_factor_bits() 88 | input_bitwidth, output_bitwidth = int(input_bitwidth), int(output_bitwidth) 89 | total_input_bits = self.module_list[0].in_features*input_bitwidth 90 | total_output_bits = self.module_list[-1].out_features*output_bitwidth 91 | num_layers = len(self.module_list) 92 | input_quant.bin_output() 93 | self.module_list[0].apply_input_quant = False 94 | y = torch.zeros(x.shape[0], self.module_list[-1].out_features) 95 | x = input_quant(x) 96 | self.dut.io.rst = 0 97 | self.dut.io.clk = 0 98 | for i in range(x.shape[0]): 99 | x_i = x[i,:] 100 | y_i = self.pytorch_forward(x[i:i+1,:])[0] 101 | xv_i = list(map(lambda z: input_quant.get_bin_str(z), x_i)) 102 | ys_i = list(map(lambda z: output_quant.get_bin_str(z), y_i)) 103 | xvc_i = reduce(lambda a,b: a+b, xv_i[::-1]) 104 | ysc_i = reduce(lambda a,b: a+b, ys_i[::-1]) 105 | self.dut["M0"] = int(xvc_i, 2) 106 | for j in range(self.latency + 1): 107 | #print(self.dut.io.M5) 108 | res = self.dut[f"M{num_layers}"] 109 | result = f"{res:0{int(total_output_bits)}b}" 110 | self.dut.io.clk = 1 111 | self.dut.io.clk = 0 112 | expected = f"{int(ysc_i,2):0{int(total_output_bits)}b}" 113 | result = f"{res:0{int(total_output_bits)}b}" 114 | assert(expected == result) 115 | res_split = [result[i:i+output_bitwidth] for i in range(0, len(result), output_bitwidth)][::-1] 116 | yv_i = torch.Tensor(list(map(lambda z: int(z, 2), res_split))) 117 | y[i,:] = yv_i 118 | # Dump the I/O pairs 119 | if self.logfile is not None: 120 | with open(self.logfile, "a") as f: 121 | f.write(f"{int(xvc_i,2):0{int(total_input_bits)}b}{int(ysc_i,2):0{int(total_output_bits)}b}\n") 122 | return y 123 | 124 | def pytorch_forward(self, x): 125 | for l in self.module_list: 126 | x = l(x) 127 | return x 128 | 129 | def forward(self, x): 130 | if self.is_verilog_inference: 131 | return self.verilog_forward(x) 132 | else: 133 | return self.pytorch_forward(x) 134 | 135 | class JetSubstructureLutModel(JetSubstructureNeqModel): 136 | pass 137 | 138 | class JetSubstructureVerilogModel(JetSubstructureNeqModel): 139 | pass 140 | 141 | -------------------------------------------------------------------------------- /examples/cybersecurity/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from functools import reduce 16 | from os.path import realpath 17 | 18 | import torch 19 | import torch.nn as nn 20 | from torch.nn.parameter import Parameter 21 | from torch.nn import init 22 | 23 | from brevitas.core.quant import QuantType 24 | from brevitas.core.scaling import ScalingImplType 25 | from brevitas.nn import QuantHardTanh, QuantReLU 26 | 27 | from pyverilator import PyVerilator 28 | 29 | from logicnets.quant import QuantBrevitasActivation 30 | from logicnets.nn import SparseLinearNeq, ScalarBiasScale, RandomFixedSparsityMask2D 31 | from logicnets.init import random_restrict_fanin 32 | 33 | class UnswNb15NeqModel(nn.Module): 34 | def __init__(self, model_config): 35 | super(UnswNb15NeqModel, self).__init__() 36 | self.model_config = model_config 37 | self.num_neurons = [model_config["input_length"]] + model_config["hidden_layers"] + [model_config["output_length"]] 38 | layer_list = [] 39 | for i in range(1, len(self.num_neurons)): 40 | in_features = self.num_neurons[i-1] 41 | out_features = self.num_neurons[i] 42 | bn = nn.BatchNorm1d(out_features) 43 | if i == 1: 44 | input_quant = QuantBrevitasActivation(QuantReLU(bit_width=model_config["input_bitwidth"], max_val=1., quant_type=QuantType.INT, scaling_impl_type=ScalingImplType.CONST)) 45 | output_quant = QuantBrevitasActivation(QuantReLU(bit_width=model_config["hidden_bitwidth"], max_val=1.61, quant_type=QuantType.INT, scaling_impl_type=ScalingImplType.PARAMETER), pre_transforms=[bn]) 46 | mask = RandomFixedSparsityMask2D(in_features, out_features, fan_in=model_config["input_fanin"]) 47 | layer = SparseLinearNeq(in_features, out_features, input_quant=input_quant, output_quant=output_quant, sparse_linear_kws={'mask': mask}) 48 | layer_list.append(layer) 49 | elif i == len(self.num_neurons)-1: 50 | output_bias_scale = ScalarBiasScale(bias_init=0.33) 51 | output_quant = QuantBrevitasActivation(QuantHardTanh(bit_width=model_config["output_bitwidth"], max_val=1.33, narrow_range=False, quant_type=QuantType.INT, scaling_impl_type=ScalingImplType.PARAMETER), pre_transforms=[bn], post_transforms=[output_bias_scale]) 52 | mask = RandomFixedSparsityMask2D(in_features, out_features, fan_in=model_config["output_fanin"]) 53 | layer = SparseLinearNeq(in_features, out_features, input_quant=layer_list[-1].output_quant, output_quant=output_quant, sparse_linear_kws={'mask': mask}, apply_input_quant=False) 54 | layer_list.append(layer) 55 | else: 56 | output_quant = QuantBrevitasActivation(QuantReLU(bit_width=model_config["hidden_bitwidth"], max_val=1.61, quant_type=QuantType.INT, scaling_impl_type=ScalingImplType.PARAMETER), pre_transforms=[bn]) 57 | mask = RandomFixedSparsityMask2D(in_features, out_features, fan_in=model_config["hidden_fanin"]) 58 | layer = SparseLinearNeq(in_features, out_features, input_quant=layer_list[-1].output_quant, output_quant=output_quant, sparse_linear_kws={'mask': mask}, apply_input_quant=False) 59 | layer_list.append(layer) 60 | self.module_list = nn.ModuleList(layer_list) 61 | self.is_verilog_inference = False 62 | self.latency = 1 63 | self.verilog_dir = None 64 | self.top_module_filename = None 65 | self.dut = None 66 | self.logfile = None 67 | 68 | def verilog_inference(self, verilog_dir, top_module_filename, logfile: bool = False, add_registers: bool = False): 69 | self.verilog_dir = realpath(verilog_dir) 70 | self.top_module_filename = top_module_filename 71 | self.dut = PyVerilator.build(f"{self.verilog_dir}/{self.top_module_filename}", verilog_path=[self.verilog_dir], build_dir=f"{self.verilog_dir}/verilator") 72 | self.is_verilog_inference = True 73 | self.logfile = logfile 74 | if add_registers: 75 | self.latency = len(self.num_neurons) 76 | 77 | def pytorch_inference(self): 78 | self.is_verilog_inference = False 79 | 80 | def verilog_forward(self, x): 81 | # Get integer output from the first layer 82 | input_quant = self.module_list[0].input_quant 83 | output_quant = self.module_list[-1].output_quant 84 | _, input_bitwidth = self.module_list[0].input_quant.get_scale_factor_bits() 85 | _, output_bitwidth = self.module_list[-1].output_quant.get_scale_factor_bits() 86 | input_bitwidth, output_bitwidth = int(input_bitwidth), int(output_bitwidth) 87 | total_input_bits = self.module_list[0].in_features*input_bitwidth 88 | total_output_bits = self.module_list[-1].out_features*output_bitwidth 89 | num_layers = len(self.module_list) 90 | input_quant.bin_output() 91 | self.module_list[0].apply_input_quant = False 92 | y = torch.zeros(x.shape[0], self.module_list[-1].out_features) 93 | x = input_quant(x) 94 | self.dut.io.rst = 0 95 | self.dut.io.clk = 0 96 | for i in range(x.shape[0]): 97 | x_i = x[i,:] 98 | y_i = self.pytorch_forward(x[i:i+1,:])[0] 99 | xv_i = list(map(lambda z: input_quant.get_bin_str(z), x_i)) 100 | ys_i = list(map(lambda z: output_quant.get_bin_str(z), y_i)) 101 | xvc_i = reduce(lambda a,b: a+b, xv_i[::-1]) 102 | ysc_i = reduce(lambda a,b: a+b, ys_i[::-1]) 103 | self.dut["M0"] = int(xvc_i, 2) 104 | for j in range(self.latency + 1): 105 | #print(self.dut.io.M5) 106 | res = self.dut[f"M{num_layers}"] 107 | result = f"{res:0{int(total_output_bits)}b}" 108 | self.dut.io.clk = 1 109 | self.dut.io.clk = 0 110 | expected = f"{int(ysc_i,2):0{int(total_output_bits)}b}" 111 | result = f"{res:0{int(total_output_bits)}b}" 112 | assert(expected == result) 113 | res_split = [result[i:i+output_bitwidth] for i in range(0, len(result), output_bitwidth)][::-1] 114 | yv_i = torch.Tensor(list(map(lambda z: int(z, 2), res_split))) 115 | y[i,:] = yv_i 116 | # Dump the I/O pairs 117 | if self.logfile is not None: 118 | with open(self.logfile, "a") as f: 119 | f.write(f"{int(xvc_i,2):0{int(total_input_bits)}b}{int(ysc_i,2):0{int(total_output_bits)}b}\n") 120 | return y 121 | 122 | def pytorch_forward(self, x): 123 | for l in self.module_list: 124 | x = l(x) 125 | return x 126 | 127 | def forward(self, x): 128 | if self.is_verilog_inference: 129 | x = self.verilog_forward(x) 130 | output_scale, output_bits = self.module_list[-1].output_quant.get_scale_factor_bits() 131 | x = self.module_list[-1].output_quant.apply_post_transforms((x - 2**(output_bits-1)) * output_scale) 132 | else: 133 | x = self.pytorch_forward(x) 134 | # Scale output, if necessary 135 | if self.module_list[-1].is_lut_inference: 136 | output_scale, output_bits = self.module_list[-1].output_quant.get_scale_factor_bits() 137 | x = self.module_list[-1].output_quant.apply_post_transforms(x * output_scale) 138 | return x 139 | 140 | class UnswNb15LutModel(UnswNb15NeqModel): 141 | pass 142 | 143 | class UnswNb15VerilogModel(UnswNb15NeqModel): 144 | pass 145 | 146 | -------------------------------------------------------------------------------- /examples/cybersecurity/neq2lut.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from argparse import ArgumentParser 17 | 18 | import torch 19 | from torch.utils.data import DataLoader 20 | 21 | from logicnets.nn import generate_truth_tables, \ 22 | lut_inference, \ 23 | module_list_to_verilog_module 24 | from logicnets.synthesis import synthesize_and_get_resource_counts 25 | from logicnets.util import proc_postsynth_file 26 | 27 | from train import configs, model_config, dataset_config, test 28 | from dataset import get_preqnt_dataset 29 | from models import UnswNb15NeqModel, UnswNb15LutModel 30 | 31 | other_options = { 32 | "cuda": None, 33 | "log_dir": None, 34 | "checkpoint": None, 35 | "generate_bench": False, 36 | "add_registers": False, 37 | "simulate_pre_synthesis_verilog": False, 38 | "simulate_post_synthesis_verilog": False, 39 | } 40 | 41 | if __name__ == "__main__": 42 | parser = ArgumentParser(description="Synthesize convert a PyTorch trained model into verilog") 43 | parser.add_argument('--arch', type=str, choices=configs.keys(), default="nid-s", 44 | help="Specific the neural network model to use (default: %(default)s)") 45 | parser.add_argument('--batch-size', type=int, default=None, metavar='N', 46 | help="Batch size for evaluation (default: %(default)s)") 47 | parser.add_argument('--input-bitwidth', type=int, default=None, 48 | help="Bitwidth to use at the input (default: %(default)s)") 49 | parser.add_argument('--hidden-bitwidth', type=int, default=None, 50 | help="Bitwidth to use for activations in hidden layers (default: %(default)s)") 51 | parser.add_argument('--output-bitwidth', type=int, default=None, 52 | help="Bitwidth to use at the output (default: %(default)s)") 53 | parser.add_argument('--input-fanin', type=int, default=None, 54 | help="Fanin to use at the input (default: %(default)s)") 55 | parser.add_argument('--hidden-fanin', type=int, default=None, 56 | help="Fanin to use for the hidden layers (default: %(default)s)") 57 | parser.add_argument('--output-fanin', type=int, default=None, 58 | help="Fanin to use at the output (default: %(default)s)") 59 | parser.add_argument('--hidden-layers', nargs='+', type=int, default=None, 60 | help="A list of hidden layer neuron sizes (default: %(default)s)") 61 | parser.add_argument('--clock-period', type=float, default=1.0, 62 | help="Target clock frequency to use during Vivado synthesis (default: %(default)s)") 63 | parser.add_argument('--dataset-split', type=str, default='test', choices=['train', 'test'], 64 | help="Dataset to use for evaluation (default: %(default)s)") 65 | parser.add_argument('--dataset-file', type=str, default='data/unsw_nb15_binarized.npz', 66 | help="The file to use as the dataset input (default: %(default)s)") 67 | parser.add_argument('--log-dir', type=str, default='./log', 68 | help="A location to store the log output of the training run and the output model (default: %(default)s)") 69 | parser.add_argument('--checkpoint', type=str, required=True, 70 | help="The checkpoint file which contains the model weights") 71 | parser.add_argument('--generate-bench', action='store_true', default=False, 72 | help="Generate the truth table in BENCH format as well as verilog (default: %(default)s)") 73 | parser.add_argument('--dump-io', action='store_true', default=False, 74 | help="Dump I/O to the verilog LUT to a text file in the log directory (default: %(default)s)") 75 | parser.add_argument('--add-registers', action='store_true', default=False, 76 | help="Add registers between each layer in generated verilog (default: %(default)s)") 77 | parser.add_argument('--simulate-pre-synthesis-verilog', action='store_true', default=False, 78 | help="Simulate the verilog generated by LogicNets (default: %(default)s)") 79 | parser.add_argument('--simulate-post-synthesis-verilog', action='store_true', default=False, 80 | help="Simulate the post-synthesis verilog produced by vivado (default: %(default)s)") 81 | parser.add_argument('--fpga-part', type=str, default="xcu280-fsvh2892-2L-e", 82 | help="Part to use for Vivado project (default: %(default)s)") 83 | args = parser.parse_args() 84 | defaults = configs[args.arch] 85 | options = vars(args) 86 | del options['arch'] 87 | config = {} 88 | for k in options.keys(): 89 | config[k] = options[k] if options[k] is not None else defaults[k] # Override defaults, if specified. 90 | 91 | if not os.path.exists(config['log_dir']): 92 | os.makedirs(config['log_dir']) 93 | 94 | # Split up configuration options to be more understandable 95 | model_cfg = {} 96 | for k in model_config.keys(): 97 | model_cfg[k] = config[k] 98 | dataset_cfg = {} 99 | for k in dataset_config.keys(): 100 | dataset_cfg[k] = config[k] 101 | options_cfg = {} 102 | for k in other_options.keys(): 103 | if k == 'cuda': 104 | continue 105 | options_cfg[k] = config[k] 106 | 107 | # Fetch the test set 108 | dataset = {} 109 | dataset[args.dataset_split] = get_preqnt_dataset(dataset_cfg['dataset_file'], split=args.dataset_split) 110 | test_loader = DataLoader(dataset[args.dataset_split], batch_size=config['batch_size'], shuffle=False) 111 | 112 | # Instantiate the PyTorch model 113 | x, y = dataset[args.dataset_split][0] 114 | model_cfg['input_length'] = len(x) 115 | model_cfg['output_length'] = 1 116 | model = UnswNb15NeqModel(model_cfg) 117 | 118 | # Load the model weights 119 | checkpoint = torch.load(options_cfg['checkpoint'], map_location='cpu') 120 | model.load_state_dict(checkpoint['model_dict']) 121 | 122 | # Test the PyTorch model 123 | print("Running inference on baseline model...") 124 | model.eval() 125 | baseline_accuracy = test(model, test_loader, cuda=False) 126 | print("Baseline accuracy: %f" % (baseline_accuracy)) 127 | 128 | # Instantiate LUT-based model 129 | lut_model = UnswNb15LutModel(model_cfg) 130 | lut_model.load_state_dict(checkpoint['model_dict']) 131 | 132 | # Generate the truth tables in the LUT module 133 | print("Converting to NEQs to LUTs...") 134 | generate_truth_tables(lut_model, verbose=True) 135 | 136 | # Test the LUT-based model 137 | print("Running inference on LUT-based model...") 138 | lut_inference(lut_model) 139 | lut_model.eval() 140 | lut_accuracy = test(lut_model, test_loader, cuda=False) 141 | print("LUT-Based Model accuracy: %f" % (lut_accuracy)) 142 | modelSave = { 'model_dict': lut_model.state_dict(), 143 | 'test_accuracy': lut_accuracy} 144 | 145 | torch.save(modelSave, options_cfg["log_dir"] + "/lut_based_model.pth") 146 | 147 | print("Generating verilog in %s..." % (options_cfg["log_dir"])) 148 | module_list_to_verilog_module(lut_model.module_list, "logicnet", options_cfg["log_dir"], generate_bench=options_cfg["generate_bench"], add_registers=options_cfg["add_registers"]) 149 | print("Top level entity stored at: %s/logicnet.v ..." % (options_cfg["log_dir"])) 150 | 151 | if args.dump_io: 152 | io_filename = options_cfg["log_dir"] + f"io_{args.dataset_split}.txt" 153 | with open(io_filename, 'w') as f: 154 | pass # Create an empty file. 155 | print(f"Dumping verilog I/O to {io_filename}...") 156 | else: 157 | io_filename = None 158 | 159 | if args.simulate_pre_synthesis_verilog: 160 | print("Running inference simulation of Verilog-based model...") 161 | lut_model.verilog_inference(options_cfg["log_dir"], "logicnet.v", logfile=io_filename, add_registers=options_cfg["add_registers"]) 162 | verilog_accuracy = test(lut_model, test_loader, cuda=False) 163 | print("Verilog-Based Model accuracy: %f" % (verilog_accuracy)) 164 | 165 | print("Running out-of-context synthesis") 166 | ret = synthesize_and_get_resource_counts(options_cfg["log_dir"], "logicnet", fpga_part=args.fpga_part, clk_period_ns=args.clock_period, post_synthesis = 1) 167 | 168 | if args.simulate_post_synthesis_verilog: 169 | print("Running post-synthesis inference simulation of Verilog-based model...") 170 | proc_postsynth_file(options_cfg["log_dir"]) 171 | lut_model.verilog_inference(options_cfg["log_dir"]+"/post_synth", "logicnet_post_synth.v", io_filename, add_registers=options_cfg["add_registers"]) 172 | post_synth_accuracy = test(lut_model, test_loader, cuda=False) 173 | print("Post-synthesis Verilog-Based Model accuracy: %f" % (post_synth_accuracy)) 174 | 175 | -------------------------------------------------------------------------------- /examples/jet_substructure/neq2lut.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from argparse import ArgumentParser 17 | 18 | import torch 19 | from torch.utils.data import DataLoader 20 | 21 | from logicnets.nn import generate_truth_tables, \ 22 | lut_inference, \ 23 | module_list_to_verilog_module 24 | 25 | from train import configs, model_config, dataset_config, test 26 | from dataset import JetSubstructureDataset 27 | from models import JetSubstructureNeqModel, JetSubstructureLutModel 28 | from logicnets.synthesis import synthesize_and_get_resource_counts 29 | from logicnets.util import proc_postsynth_file 30 | 31 | other_options = { 32 | "cuda": None, 33 | "log_dir": None, 34 | "checkpoint": None, 35 | "generate_bench": False, 36 | "add_registers": False, 37 | "simulate_pre_synthesis_verilog": False, 38 | "simulate_post_synthesis_verilog": False, 39 | } 40 | 41 | if __name__ == "__main__": 42 | parser = ArgumentParser(description="Synthesize convert a PyTorch trained model into verilog") 43 | parser.add_argument('--arch', type=str, choices=configs.keys(), default="jsc-s", 44 | help="Specific the neural network model to use (default: %(default)s)") 45 | parser.add_argument('--batch-size', type=int, default=None, metavar='N', 46 | help="Batch size for evaluation (default: %(default)s)") 47 | parser.add_argument('--input-bitwidth', type=int, default=None, 48 | help="Bitwidth to use at the input (default: %(default)s)") 49 | parser.add_argument('--hidden-bitwidth', type=int, default=None, 50 | help="Bitwidth to use for activations in hidden layers (default: %(default)s)") 51 | parser.add_argument('--output-bitwidth', type=int, default=None, 52 | help="Bitwidth to use at the output (default: %(default)s)") 53 | parser.add_argument('--input-fanin', type=int, default=None, 54 | help="Fanin to use at the input (default: %(default)s)") 55 | parser.add_argument('--hidden-fanin', type=int, default=None, 56 | help="Fanin to use for the hidden layers (default: %(default)s)") 57 | parser.add_argument('--output-fanin', type=int, default=None, 58 | help="Fanin to use at the output (default: %(default)s)") 59 | parser.add_argument('--hidden-layers', nargs='+', type=int, default=None, 60 | help="A list of hidden layer neuron sizes (default: %(default)s)") 61 | parser.add_argument('--dataset-file', type=str, default='data/processed-pythia82-lhc13-all-pt1-50k-r1_h022_e0175_t220_nonu_truth.z', 62 | help="The file to use as the dataset input (default: %(default)s)") 63 | parser.add_argument('--clock-period', type=float, default=1.0, 64 | help="Target clock frequency to use during Vivado synthesis (default: %(default)s)") 65 | parser.add_argument('--dataset-config', type=str, default='config/yaml_IP_OP_config.yml', 66 | help="The file to use to configure the input dataset (default: %(default)s)") 67 | parser.add_argument('--dataset-split', type=str, default='test', choices=['train', 'test'], 68 | help="Dataset to use for evaluation (default: %(default)s)") 69 | parser.add_argument('--log-dir', type=str, default='./log', 70 | help="A location to store the log output of the training run and the output model (default: %(default)s)") 71 | parser.add_argument('--checkpoint', type=str, required=True, 72 | help="The checkpoint file which contains the model weights") 73 | parser.add_argument('--generate-bench', action='store_true', default=False, 74 | help="Generate the truth table in BENCH format as well as verilog (default: %(default)s)") 75 | parser.add_argument('--dump-io', action='store_true', default=False, 76 | help="Dump I/O to the verilog LUT to a text file in the log directory (default: %(default)s)") 77 | parser.add_argument('--add-registers', action='store_true', default=False, 78 | help="Add registers between each layer in generated verilog (default: %(default)s)") 79 | parser.add_argument('--simulate-pre-synthesis-verilog', action='store_true', default=False, 80 | help="Simulate the verilog generated by LogicNets (default: %(default)s)") 81 | parser.add_argument('--simulate-post-synthesis-verilog', action='store_true', default=False, 82 | help="Simulate the post-synthesis verilog produced by vivado (default: %(default)s)") 83 | parser.add_argument('--fpga-part', type=str, default="xcu280-fsvh2892-2L-e", 84 | help="Part to use for Vivado project (default: %(default)s)") 85 | args = parser.parse_args() 86 | defaults = configs[args.arch] 87 | options = vars(args) 88 | del options['arch'] 89 | config = {} 90 | for k in options.keys(): 91 | config[k] = options[k] if options[k] is not None else defaults[k] # Override defaults, if specified. 92 | 93 | if not os.path.exists(config['log_dir']): 94 | os.makedirs(config['log_dir']) 95 | 96 | # Split up configuration options to be more understandable 97 | model_cfg = {} 98 | for k in model_config.keys(): 99 | model_cfg[k] = config[k] 100 | dataset_cfg = {} 101 | for k in dataset_config.keys(): 102 | dataset_cfg[k] = config[k] 103 | options_cfg = {} 104 | for k in other_options.keys(): 105 | if k == 'cuda': 106 | continue 107 | options_cfg[k] = config[k] 108 | 109 | # Fetch the test set 110 | dataset = {} 111 | dataset[args.dataset_split] = JetSubstructureDataset(dataset_cfg['dataset_file'], dataset_cfg['dataset_config'], split=args.dataset_split) 112 | test_loader = DataLoader(dataset[args.dataset_split], batch_size=config['batch_size'], shuffle=False) 113 | 114 | # Instantiate the PyTorch model 115 | x, y = dataset[args.dataset_split][0] 116 | model_cfg['input_length'] = len(x) 117 | model_cfg['output_length'] = len(y) 118 | model = JetSubstructureNeqModel(model_cfg) 119 | 120 | # Load the model weights 121 | checkpoint = torch.load(options_cfg['checkpoint'], map_location='cpu') 122 | model.load_state_dict(checkpoint['model_dict']) 123 | 124 | # Test the PyTorch model 125 | print("Running inference on baseline model...") 126 | model.eval() 127 | baseline_accuracy, baseline_avg_roc_auc = test(model, test_loader, cuda=False) 128 | print("Baseline accuracy: %f" % (baseline_accuracy)) 129 | print("Baseline AVG ROC AUC: %f" % (baseline_avg_roc_auc)) 130 | 131 | # Instantiate LUT-based model 132 | lut_model = JetSubstructureLutModel(model_cfg) 133 | lut_model.load_state_dict(checkpoint['model_dict']) 134 | 135 | # Generate the truth tables in the LUT module 136 | print("Converting to NEQs to LUTs...") 137 | generate_truth_tables(lut_model, verbose=True) 138 | 139 | # Test the LUT-based model 140 | print("Running inference on LUT-based model...") 141 | lut_inference(lut_model) 142 | lut_model.eval() 143 | lut_accuracy, lut_avg_roc_auc = test(lut_model, test_loader, cuda=False) 144 | print("LUT-Based Model accuracy: %f" % (lut_accuracy)) 145 | print("LUT-Based AVG ROC AUC: %f" % (lut_avg_roc_auc)) 146 | modelSave = { 'model_dict': lut_model.state_dict(), 147 | 'test_accuracy': lut_accuracy, 148 | 'test_avg_roc_auc': lut_avg_roc_auc} 149 | 150 | torch.save(modelSave, options_cfg["log_dir"] + "/lut_based_model.pth") 151 | 152 | print("Generating verilog in %s..." % (options_cfg["log_dir"])) 153 | module_list_to_verilog_module(lut_model.module_list, "logicnet", options_cfg["log_dir"], generate_bench=options_cfg["generate_bench"], add_registers=options_cfg["add_registers"]) 154 | print("Top level entity stored at: %s/logicnet.v ..." % (options_cfg["log_dir"])) 155 | 156 | if args.dump_io: 157 | io_filename = options_cfg["log_dir"] + f"io_{args.dataset_split}.txt" 158 | with open(io_filename, 'w') as f: 159 | pass # Create an empty file. 160 | print(f"Dumping verilog I/O to {io_filename}...") 161 | else: 162 | io_filename = None 163 | 164 | if args.simulate_pre_synthesis_verilog: 165 | print("Running inference simulation of Verilog-based model...") 166 | lut_model.verilog_inference(options_cfg["log_dir"], "logicnet.v", logfile=io_filename, add_registers=options_cfg["add_registers"]) 167 | verilog_accuracy, verilog_avg_roc_auc = test(lut_model, test_loader, cuda=False) 168 | print("Verilog-Based Model accuracy: %f" % (verilog_accuracy)) 169 | print("Verilog-Based AVG ROC AUC: %f" % (verilog_avg_roc_auc)) 170 | 171 | print("Running out-of-context synthesis") 172 | ret = synthesize_and_get_resource_counts(options_cfg["log_dir"], "logicnet", fpga_part=args.fpga_part, clk_period_ns=args.clock_period, post_synthesis = 1) 173 | 174 | if args.simulate_post_synthesis_verilog: 175 | print("Running post-synthesis inference simulation of Verilog-based model...") 176 | proc_postsynth_file(options_cfg["log_dir"]) 177 | lut_model.verilog_inference(options_cfg["log_dir"]+"/post_synth", "logicnet_post_synth.v", io_filename, add_registers=options_cfg["add_registers"]) 178 | post_synth_accuracy, post_synth_avg_roc_auc = test(lut_model, test_loader, cuda=False) 179 | print("Post-synthesis Verilog-Based Model accuracy: %f" % (post_synth_accuracy)) 180 | print("Post-synthesis Verilog-Based AVG ROC AUC: %f" % (post_synth_avg_roc_auc)) 181 | 182 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Xilinx 2 | All rights reserved. 3 | 4 | 5 | Apache License 6 | Version 2.0, January 2004 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, 14 | and distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by 17 | the copyright owner that is granting the License. 18 | 19 | "Legal Entity" shall mean the union of the acting entity and all 20 | other entities that control, are controlled by, or are under common 21 | control with that entity. For the purposes of this definition, 22 | "control" means (i) the power, direct or indirect, to cause the 23 | direction or management of such entity, whether by contract or 24 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 25 | outstanding shares, or (iii) beneficial ownership of such entity. 26 | 27 | "You" (or "Your") shall mean an individual or Legal Entity 28 | exercising permissions granted by this License. 29 | 30 | "Source" form shall mean the preferred form for making modifications, 31 | including but not limited to software source code, documentation 32 | source, and configuration files. 33 | 34 | "Object" form shall mean any form resulting from mechanical 35 | transformation or translation of a Source form, including but 36 | not limited to compiled object code, generated documentation, 37 | and conversions to other media types. 38 | 39 | "Work" shall mean the work of authorship, whether in Source or 40 | Object form, made available under the License, as indicated by a 41 | copyright notice that is included in or attached to the work 42 | (an example is provided in the Appendix below). 43 | 44 | "Derivative Works" shall mean any work, whether in Source or Object 45 | form, that is based on (or derived from) the Work and for which the 46 | editorial revisions, annotations, elaborations, or other modifications 47 | represent, as a whole, an original work of authorship. For the purposes 48 | of this License, Derivative Works shall not include works that remain 49 | separable from, or merely link (or bind by name) to the interfaces of, 50 | the Work and Derivative Works thereof. 51 | 52 | "Contribution" shall mean any work of authorship, including 53 | the original version of the Work and any modifications or additions 54 | to that Work or Derivative Works thereof, that is intentionally 55 | submitted to Licensor for inclusion in the Work by the copyright owner 56 | or by an individual or Legal Entity authorized to submit on behalf of 57 | the copyright owner. For the purposes of this definition, "submitted" 58 | means any form of electronic, verbal, or written communication sent 59 | to the Licensor or its representatives, including but not limited to 60 | communication on electronic mailing lists, source code control systems, 61 | and issue tracking systems that are managed by, or on behalf of, the 62 | Licensor for the purpose of discussing and improving the Work, but 63 | excluding communication that is conspicuously marked or otherwise 64 | designated in writing by the copyright owner as "Not a Contribution." 65 | 66 | "Contributor" shall mean Licensor and any individual or Legal Entity 67 | on behalf of whom a Contribution has been received by Licensor and 68 | subsequently incorporated within the Work. 69 | 70 | 2. Grant of Copyright License. Subject to the terms and conditions of 71 | this License, each Contributor hereby grants to You a perpetual, 72 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 73 | copyright license to reproduce, prepare Derivative Works of, 74 | publicly display, publicly perform, sublicense, and distribute the 75 | Work and such Derivative Works in Source or Object form. 76 | 77 | 3. Grant of Patent License. Subject to the terms and conditions of 78 | this License, each Contributor hereby grants to You a perpetual, 79 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 80 | (except as stated in this section) patent license to make, have made, 81 | use, offer to sell, sell, import, and otherwise transfer the Work, 82 | where such license applies only to those patent claims licensable 83 | by such Contributor that are necessarily infringed by their 84 | Contribution(s) alone or by combination of their Contribution(s) 85 | with the Work to which such Contribution(s) was submitted. If You 86 | institute patent litigation against any entity (including a 87 | cross-claim or counterclaim in a lawsuit) alleging that the Work 88 | or a Contribution incorporated within the Work constitutes direct 89 | or contributory patent infringement, then any patent licenses 90 | granted to You under this License for that Work shall terminate 91 | as of the date such litigation is filed. 92 | 93 | 4. Redistribution. You may reproduce and distribute copies of the 94 | Work or Derivative Works thereof in any medium, with or without 95 | modifications, and in Source or Object form, provided that You 96 | meet the following conditions: 97 | 98 | (a) You must give any other recipients of the Work or 99 | Derivative Works a copy of this License; and 100 | 101 | (b) You must cause any modified files to carry prominent notices 102 | stating that You changed the files; and 103 | 104 | (c) You must retain, in the Source form of any Derivative Works 105 | that You distribute, all copyright, patent, trademark, and 106 | attribution notices from the Source form of the Work, 107 | excluding those notices that do not pertain to any part of 108 | the Derivative Works; and 109 | 110 | (d) If the Work includes a "NOTICE" text file as part of its 111 | distribution, then any Derivative Works that You distribute must 112 | include a readable copy of the attribution notices contained 113 | within such NOTICE file, excluding those notices that do not 114 | pertain to any part of the Derivative Works, in at least one 115 | of the following places: within a NOTICE text file distributed 116 | as part of the Derivative Works; within the Source form or 117 | documentation, if provided along with the Derivative Works; or, 118 | within a display generated by the Derivative Works, if and 119 | wherever such third-party notices normally appear. The contents 120 | of the NOTICE file are for informational purposes only and 121 | do not modify the License. You may add Your own attribution 122 | notices within Derivative Works that You distribute, alongside 123 | or as an addendum to the NOTICE text from the Work, provided 124 | that such additional attribution notices cannot be construed 125 | as modifying the License. 126 | 127 | You may add Your own copyright statement to Your modifications and 128 | may provide additional or different license terms and conditions 129 | for use, reproduction, or distribution of Your modifications, or 130 | for any such Derivative Works as a whole, provided Your use, 131 | reproduction, and distribution of the Work otherwise complies with 132 | the conditions stated in this License. 133 | 134 | 5. Submission of Contributions. Unless You explicitly state otherwise, 135 | any Contribution intentionally submitted for inclusion in the Work 136 | by You to the Licensor shall be under the terms and conditions of 137 | this License, without any additional terms or conditions. 138 | Notwithstanding the above, nothing herein shall supersede or modify 139 | the terms of any separate license agreement you may have executed 140 | with Licensor regarding such Contributions. 141 | 142 | 6. Trademarks. This License does not grant permission to use the trade 143 | names, trademarks, service marks, or product names of the Licensor, 144 | except as required for reasonable and customary use in describing the 145 | origin of the Work and reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. Unless required by applicable law or 148 | agreed to in writing, Licensor provides the Work (and each 149 | Contributor provides its Contributions) on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 151 | implied, including, without limitation, any warranties or conditions 152 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 153 | PARTICULAR PURPOSE. You are solely responsible for determining the 154 | appropriateness of using or redistributing the Work and assume any 155 | risks associated with Your exercise of permissions under this License. 156 | 157 | 8. Limitation of Liability. In no event and under no legal theory, 158 | whether in tort (including negligence), contract, or otherwise, 159 | unless required by applicable law (such as deliberate and grossly 160 | negligent acts) or agreed to in writing, shall any Contributor be 161 | liable to You for damages, including any direct, indirect, special, 162 | incidental, or consequential damages of any character arising as a 163 | result of this License or out of the use or inability to use the 164 | Work (including but not limited to damages for loss of goodwill, 165 | work stoppage, computer failure or malfunction, or any and all 166 | other commercial damages or losses), even if such Contributor 167 | has been advised of the possibility of such damages. 168 | 169 | 9. Accepting Warranty or Additional Liability. While redistributing 170 | the Work or Derivative Works thereof, You may choose to offer, 171 | and charge a fee for, acceptance of support, warranty, indemnity, 172 | or other liability obligations and/or rights consistent with this 173 | License. However, in accepting such obligations, You may act only 174 | on Your own behalf and on Your sole responsibility, not on behalf 175 | of any other Contributor, and only if You agree to indemnify, 176 | defend, and hold each Contributor harmless for any liability 177 | incurred by, or claims asserted against, such Contributor by reason 178 | of your accepting any such warranty or additional liability. 179 | 180 | END OF TERMS AND CONDITIONS 181 | 182 | APPENDIX: How to apply the Apache License to your work. 183 | 184 | To apply the Apache License to your work, attach the following 185 | boilerplate notice, with the fields enclosed by brackets "{}" 186 | replaced with your own identifying information. (Don't include 187 | the brackets!) The text should be enclosed in the appropriate 188 | comment syntax for the file format. We also recommend that a 189 | file or class name and description of purpose be included on the 190 | same "printed page" as the copyright notice for easier 191 | identification within third-party archives. 192 | 193 | Copyright {yyyy} {name of copyright owner} 194 | 195 | Licensed under the Apache License, Version 2.0 (the "License"); 196 | you may not use this file except in compliance with the License. 197 | You may obtain a copy of the License at 198 | 199 | http://www.apache.org/licenses/LICENSE-2.0 200 | 201 | Unless required by applicable law or agreed to in writing, software 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | See the License for the specific language governing permissions and 205 | limitations under the License. 206 | -------------------------------------------------------------------------------- /examples/cybersecurity/train.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from argparse import ArgumentParser 17 | from functools import reduce 18 | import random 19 | 20 | import numpy as np 21 | 22 | import torch 23 | import torch.nn as nn 24 | import torch.optim as optim 25 | from torch.utils.data import DataLoader 26 | from torch.utils.tensorboard import SummaryWriter 27 | 28 | from dataset import get_preqnt_dataset 29 | from models import UnswNb15NeqModel 30 | 31 | # TODO: Replace default configs with YAML files. 32 | configs = { 33 | "nid-s": { 34 | "hidden_layers": [593, 100], 35 | "input_bitwidth": 1, 36 | "hidden_bitwidth": 2, 37 | "output_bitwidth": 2, 38 | "input_fanin": 7, 39 | "hidden_fanin": 7, 40 | "output_fanin": 7, 41 | "weight_decay": 0.0, 42 | "batch_size": 1024, 43 | "epochs": 100, 44 | "learning_rate": 1e-1, 45 | "seed": 109, 46 | "checkpoint": None, 47 | }, 48 | "nid-s-comp": { 49 | "hidden_layers": [49, 7], 50 | "input_bitwidth": 1, 51 | "hidden_bitwidth": 2, 52 | "output_bitwidth": 2, 53 | "input_fanin": 7, 54 | "hidden_fanin": 7, 55 | "output_fanin": 7, 56 | "weight_decay": 0.0, 57 | "batch_size": 1024, 58 | "epochs": 100, 59 | "learning_rate": 1e-1, 60 | "seed": 81, 61 | "checkpoint": None, 62 | }, 63 | "nid-m": { 64 | "hidden_layers": [593, 256, 128, 128], 65 | "input_bitwidth": 1, 66 | "hidden_bitwidth": 2, 67 | "output_bitwidth": 2, 68 | "input_fanin": 7, 69 | "hidden_fanin": 7, 70 | "output_fanin": 7, 71 | "weight_decay": 0.0, 72 | "batch_size": 1024, 73 | "epochs": 100, 74 | "learning_rate": 1e-1, 75 | "seed": 196, 76 | "checkpoint": None, 77 | }, 78 | "nid-m-comp": { 79 | "hidden_layers": [593, 256, 49, 7], 80 | "input_bitwidth": 1, 81 | "hidden_bitwidth": 2, 82 | "output_bitwidth": 2, 83 | "input_fanin": 7, 84 | "hidden_fanin": 7, 85 | "output_fanin": 7, 86 | "weight_decay": 0.0, 87 | "batch_size": 1024, 88 | "epochs": 100, 89 | "learning_rate": 1e-1, 90 | "seed": 40, 91 | "checkpoint": None, 92 | }, 93 | "nid-l": { 94 | "hidden_layers": [593, 100, 100, 100], 95 | "input_bitwidth": 1, 96 | "hidden_bitwidth": 3, 97 | "output_bitwidth": 3, 98 | "input_fanin": 7, 99 | "hidden_fanin": 5, 100 | "output_fanin": 5, 101 | "weight_decay": 0.0, 102 | "batch_size": 1024, 103 | "epochs": 100, 104 | "learning_rate": 1e-1, 105 | "seed": 2, 106 | "checkpoint": None, 107 | }, 108 | "nid-l-comp": { 109 | "hidden_layers": [593, 100, 25, 5], 110 | "input_bitwidth": 1, 111 | "hidden_bitwidth": 3, 112 | "output_bitwidth": 3, 113 | "input_fanin": 7, 114 | "hidden_fanin": 5, 115 | "output_fanin": 5, 116 | "weight_decay": 0.0, 117 | "batch_size": 1024, 118 | "epochs": 100, 119 | "learning_rate": 1e-1, 120 | "seed": 83, 121 | "checkpoint": None, 122 | }, 123 | } 124 | 125 | # A dictionary, so we can set some defaults if necessary 126 | model_config = { 127 | "hidden_layers": None, 128 | "input_bitwidth": None, 129 | "hidden_bitwidth": None, 130 | "output_bitwidth": None, 131 | "input_fanin": None, 132 | "hidden_fanin": None, 133 | "output_fanin": None, 134 | } 135 | 136 | training_config = { 137 | "weight_decay": None, 138 | "batch_size": None, 139 | "epochs": None, 140 | "learning_rate": None, 141 | "seed": None, 142 | } 143 | 144 | dataset_config = { 145 | "dataset_file": None, 146 | } 147 | 148 | other_options = { 149 | "cuda": None, 150 | "log_dir": None, 151 | "checkpoint": None, 152 | } 153 | 154 | def train(model, datasets, train_cfg, options): 155 | # Create data loaders for training and inference: 156 | train_loader = DataLoader(datasets["train"], batch_size=train_cfg['batch_size'], shuffle=True) 157 | val_loader = DataLoader(datasets["valid"], batch_size=train_cfg['batch_size'], shuffle=False) 158 | test_loader = DataLoader(datasets["test"], batch_size=train_cfg['batch_size'], shuffle=False) 159 | 160 | # Configure optimizer 161 | weight_decay = train_cfg["weight_decay"] 162 | decay_exclusions = ["bn", "bias", "learned_value"] # Make a list of parameters name fragments which will ignore weight decay TODO: make this list part of the train_cfg 163 | decay_params = [] 164 | no_decay_params = [] 165 | for pname, params in model.named_parameters(): 166 | if params.requires_grad: 167 | if reduce(lambda a,b: a or b, map(lambda x: x in pname, decay_exclusions)): # check if the current label should be excluded from weight decay 168 | #print("Disabling weight decay for %s" % (pname)) 169 | no_decay_params.append(params) 170 | else: 171 | #print("Enabling weight decay for %s" % (pname)) 172 | decay_params.append(params) 173 | #else: 174 | #print("Ignoring %s" % (pname)) 175 | params = [{'params': decay_params, 'weight_decay': weight_decay}, 176 | {'params': no_decay_params, 'weight_decay': 0.0}] 177 | optimizer = optim.AdamW(params, lr=train_cfg['learning_rate'], betas=(0.5, 0.999), weight_decay=weight_decay) 178 | 179 | # Configure scheduler 180 | steps = len(train_loader) 181 | scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=steps*100, T_mult=1) 182 | 183 | # Configure criterion 184 | criterion = nn.BCEWithLogitsLoss() 185 | 186 | # Push the model to the GPU, if necessary 187 | if options["cuda"]: 188 | model.cuda() 189 | 190 | # Setup tensorboard 191 | writer = SummaryWriter(options["log_dir"]) 192 | 193 | # Main training loop 194 | maxAcc = 0.0 195 | num_epochs = train_cfg["epochs"] 196 | for epoch in range(0, num_epochs): 197 | # Train for this epoch 198 | model.train() 199 | accLoss = 0.0 200 | correct = 0 201 | for batch_idx, (data, target) in enumerate(train_loader): 202 | if options["cuda"]: 203 | data, target = data.cuda(), target.cuda() 204 | optimizer.zero_grad() 205 | output = model(data) 206 | loss = criterion(output, target.unsqueeze(1)) 207 | pred = (torch.sigmoid(output.detach()) > 0.75) * 1 208 | curCorrect = pred.eq(target.unsqueeze(1)).long().sum() 209 | curAcc = 100.0*curCorrect / len(data) 210 | correct += curCorrect 211 | accLoss += loss.detach()*len(data) 212 | loss.backward() 213 | optimizer.step() 214 | scheduler.step() 215 | 216 | # Log stats to tensorboard 217 | #writer.add_scalar('train_loss', loss.detach().cpu().numpy(), epoch*steps + batch_idx) 218 | #writer.add_scalar('train_accuracy', curAcc.detach().cpu().numpy(), epoch*steps + batch_idx) 219 | #g = optimizer.param_groups[0] 220 | #writer.add_scalar('LR', g['lr'], epoch*steps + batch_idx) 221 | 222 | accLoss /= len(train_loader.dataset) 223 | accuracy = 100.0*correct / len(train_loader.dataset) 224 | print(f"Epoch: {epoch}/{num_epochs}\tTrain Acc (%): {accuracy.detach().cpu().numpy():.2f}\tTrain Loss: {accLoss.detach().cpu().numpy():.3e}") 225 | #for g in optimizer.param_groups: 226 | # print("LR: {:.6f} ".format(g['lr'])) 227 | # print("LR: {:.6f} ".format(g['weight_decay'])) 228 | writer.add_scalar('avg_train_loss', accLoss.detach().cpu().numpy(), (epoch+1)*steps) 229 | writer.add_scalar('avg_train_accuracy', accuracy.detach().cpu().numpy(), (epoch+1)*steps) 230 | val_accuracy = test(model, val_loader, options["cuda"]) 231 | test_accuracy = test(model, test_loader, options["cuda"]) 232 | modelSave = { 'model_dict': model.state_dict(), 233 | 'optim_dict': optimizer.state_dict(), 234 | 'val_accuracy': val_accuracy, 235 | 'test_accuracy': test_accuracy, 236 | 'epoch': epoch} 237 | torch.save(modelSave, options["log_dir"] + "/checkpoint.pth") 238 | if(maxAcc thresh) * 1 254 | curCorrect = pred.eq(target.unsqueeze(1)).long().sum() 255 | curAcc = 100.0*curCorrect / len(data) 256 | correct += curCorrect 257 | accuracy = 100*float(correct) / len(dataset_loader.dataset) 258 | return accuracy 259 | 260 | if __name__ == "__main__": 261 | parser = ArgumentParser(description="LogicNets Network Intrusion Detection Example") 262 | parser.add_argument('--arch', type=str, choices=configs.keys(), default="nid-s", 263 | help="Specific the neural network model to use (default: %(default)s)") 264 | parser.add_argument('--weight-decay', type=float, default=None, metavar='D', 265 | help="Weight decay (default: %(default)s)") 266 | parser.add_argument('--batch-size', type=int, default=None, metavar='N', 267 | help="Batch size for training (default: %(default)s)") 268 | parser.add_argument('--epochs', type=int, default=None, metavar='N', 269 | help="Number of epochs to train (default: %(default)s)") 270 | parser.add_argument('--learning-rate', type=float, default=None, metavar='LR', 271 | help="Initial learning rate (default: %(default)s)") 272 | parser.add_argument('--cuda', action='store_true', default=False, 273 | help="Train on a GPU (default: %(default)s)") 274 | parser.add_argument('--seed', type=int, default=None, 275 | help="Seed to use for RNG (default: %(default)s)") 276 | parser.add_argument('--input-bitwidth', type=int, default=None, 277 | help="Bitwidth to use at the input (default: %(default)s)") 278 | parser.add_argument('--hidden-bitwidth', type=int, default=None, 279 | help="Bitwidth to use for activations in hidden layers (default: %(default)s)") 280 | parser.add_argument('--output-bitwidth', type=int, default=None, 281 | help="Bitwidth to use at the output (default: %(default)s)") 282 | parser.add_argument('--input-fanin', type=int, default=None, 283 | help="Fanin to use at the input (default: %(default)s)") 284 | parser.add_argument('--hidden-fanin', type=int, default=None, 285 | help="Fanin to use for the hidden layers (default: %(default)s)") 286 | parser.add_argument('--output-fanin', type=int, default=None, 287 | help="Fanin to use at the output (default: %(default)s)") 288 | parser.add_argument('--hidden-layers', nargs='+', type=int, default=None, 289 | help="A list of hidden layer neuron sizes (default: %(default)s)") 290 | parser.add_argument('--log-dir', type=str, default='./log', 291 | help="A location to store the log output of the training run and the output model (default: %(default)s)") 292 | parser.add_argument('--dataset-file', type=str, default='data/unsw_nb15_binarized.npz', 293 | help="The file to use as the dataset input (default: %(default)s)") 294 | parser.add_argument('--checkpoint', type=str, default=None, 295 | help="Retrain the model from a previous checkpoint (default: %(default)s)") 296 | args = parser.parse_args() 297 | defaults = configs[args.arch] 298 | options = vars(args) 299 | del options['arch'] 300 | config = {} 301 | for k in options.keys(): 302 | config[k] = options[k] if options[k] is not None else defaults[k] # Override defaults, if specified. 303 | 304 | if not os.path.exists(config['log_dir']): 305 | os.makedirs(config['log_dir']) 306 | 307 | # Split up configuration options to be more understandable 308 | model_cfg = {} 309 | for k in model_config.keys(): 310 | model_cfg[k] = config[k] 311 | train_cfg = {} 312 | for k in training_config.keys(): 313 | train_cfg[k] = config[k] 314 | dataset_cfg = {} 315 | for k in dataset_config.keys(): 316 | dataset_cfg[k] = config[k] 317 | options_cfg = {} 318 | for k in other_options.keys(): 319 | options_cfg[k] = config[k] 320 | 321 | # Set random seeds 322 | random.seed(train_cfg['seed']) 323 | np.random.seed(train_cfg['seed']) 324 | torch.manual_seed(train_cfg['seed']) 325 | os.environ['PYTHONHASHSEED'] = str(train_cfg['seed']) 326 | if options["cuda"]: 327 | torch.cuda.manual_seed_all(train_cfg['seed']) 328 | torch.backends.cudnn.deterministic = True 329 | 330 | # Fetch the datasets 331 | dataset = {} 332 | dataset['train'] = get_preqnt_dataset(dataset_cfg['dataset_file'], split="train") 333 | dataset['valid'] = get_preqnt_dataset(dataset_cfg['dataset_file'], split="test") # This dataset is so small, we'll just use the test set as the validation set, otherwise we may have too few trainings examples to converge. 334 | dataset['test'] = get_preqnt_dataset(dataset_cfg['dataset_file'], split="test") 335 | 336 | # Instantiate model 337 | x, y = dataset['train'][0] 338 | model_cfg['input_length'] = len(x) 339 | model_cfg['output_length'] = 1 340 | model = UnswNb15NeqModel(model_cfg) 341 | if options_cfg['checkpoint'] is not None: 342 | print(f"Loading pre-trained checkpoint {options_cfg['checkpoint']}") 343 | checkpoint = torch.load(options_cfg['checkpoint'], map_location='cpu') 344 | model.load_state_dict(checkpoint['model_dict']) 345 | 346 | train(model, dataset, train_cfg, options_cfg) 347 | 348 | -------------------------------------------------------------------------------- /examples/jet_substructure/train.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Xilinx, Inc 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from argparse import ArgumentParser 17 | from functools import reduce 18 | import random 19 | 20 | import numpy as np 21 | from sklearn.metrics import roc_auc_score 22 | 23 | import torch 24 | import torch.nn as nn 25 | import torch.optim as optim 26 | import torch.nn.functional as F 27 | from torch.utils.data import DataLoader 28 | from torch.utils.tensorboard import SummaryWriter 29 | 30 | from dataset import JetSubstructureDataset 31 | from models import JetSubstructureNeqModel 32 | 33 | # TODO: Replace default configs with YAML files. 34 | configs = { 35 | "jsc-s": { 36 | "hidden_layers": [64, 32, 32, 32], 37 | "input_bitwidth": 2, 38 | "hidden_bitwidth": 2, 39 | "output_bitwidth": 2, 40 | "input_fanin": 3, 41 | "hidden_fanin": 3, 42 | "output_fanin": 3, 43 | "weight_decay": 1e-3, 44 | "batch_size": 1024, 45 | "epochs": 1000, 46 | "learning_rate": 1e-3, 47 | "seed": 2, 48 | "checkpoint": None, 49 | }, 50 | "jsc-m": { 51 | "hidden_layers": [64, 32, 32, 32], 52 | "input_bitwidth": 3, 53 | "hidden_bitwidth": 3, 54 | "output_bitwidth": 3, 55 | "input_fanin": 4, 56 | "hidden_fanin": 4, 57 | "output_fanin": 4, 58 | "weight_decay": 1e-3, 59 | "batch_size": 1024, 60 | "epochs": 1000, 61 | "learning_rate": 1e-3, 62 | "seed": 3, 63 | "checkpoint": None, 64 | }, 65 | "jsc-l": { 66 | "hidden_layers": [32, 64, 192, 192, 16], 67 | "input_bitwidth": 4, 68 | "hidden_bitwidth": 3, 69 | "output_bitwidth": 7, 70 | "input_fanin": 3, 71 | "hidden_fanin": 4, 72 | "output_fanin": 5, 73 | "weight_decay": 1e-3, 74 | "batch_size": 1024, 75 | "epochs": 1000, 76 | "learning_rate": 1e-3, 77 | "seed": 16, 78 | "checkpoint": None, 79 | }, 80 | } 81 | 82 | # A dictionary, so we can set some defaults if necessary 83 | model_config = { 84 | "hidden_layers": None, 85 | "input_bitwidth": None, 86 | "hidden_bitwidth": None, 87 | "output_bitwidth": None, 88 | "input_fanin": None, 89 | "hidden_fanin": None, 90 | "output_fanin": None, 91 | } 92 | 93 | training_config = { 94 | "weight_decay": None, 95 | "batch_size": None, 96 | "epochs": None, 97 | "learning_rate": None, 98 | "seed": None, 99 | } 100 | 101 | dataset_config = { 102 | "dataset_file": None, 103 | "dataset_config": None, 104 | } 105 | 106 | other_options = { 107 | "cuda": None, 108 | "log_dir": None, 109 | "checkpoint": None, 110 | } 111 | 112 | def train(model, datasets, train_cfg, options): 113 | # Create data loaders for training and inference: 114 | train_loader = DataLoader(datasets["train"], batch_size=train_cfg['batch_size'], shuffle=True) 115 | val_loader = DataLoader(datasets["valid"], batch_size=train_cfg['batch_size'], shuffle=False) 116 | test_loader = DataLoader(datasets["test"], batch_size=train_cfg['batch_size'], shuffle=False) 117 | 118 | # Configure optimizer 119 | weight_decay = train_cfg["weight_decay"] 120 | decay_exclusions = ["bn", "bias", "learned_value"] # Make a list of parameters name fragments which will ignore weight decay TODO: make this list part of the train_cfg 121 | decay_params = [] 122 | no_decay_params = [] 123 | for pname, params in model.named_parameters(): 124 | if params.requires_grad: 125 | if reduce(lambda a,b: a or b, map(lambda x: x in pname, decay_exclusions)): # check if the current label should be excluded from weight decay 126 | #print("Disabling weight decay for %s" % (pname)) 127 | no_decay_params.append(params) 128 | else: 129 | #print("Enabling weight decay for %s" % (pname)) 130 | decay_params.append(params) 131 | #else: 132 | #print("Ignoring %s" % (pname)) 133 | params = [{'params': decay_params, 'weight_decay': weight_decay}, 134 | {'params': no_decay_params, 'weight_decay': 0.0}] 135 | optimizer = optim.AdamW(params, lr=train_cfg['learning_rate'], betas=(0.5, 0.999), weight_decay=weight_decay) 136 | 137 | # Configure scheduler 138 | steps = len(train_loader) 139 | scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=steps*100, T_mult=1) 140 | 141 | # Configure criterion 142 | criterion = nn.CrossEntropyLoss() 143 | 144 | # Push the model to the GPU, if necessary 145 | if options["cuda"]: 146 | model.cuda() 147 | 148 | # Setup tensorboard 149 | writer = SummaryWriter(options["log_dir"]) 150 | 151 | # Main training loop 152 | maxAcc = 0.0 153 | num_epochs = train_cfg["epochs"] 154 | for epoch in range(0, num_epochs): 155 | # Train for this epoch 156 | model.train() 157 | accLoss = 0.0 158 | correct = 0 159 | for batch_idx, (data, target) in enumerate(train_loader): 160 | if options["cuda"]: 161 | data, target = data.cuda(), target.cuda() 162 | optimizer.zero_grad() 163 | output = model(data) 164 | loss = criterion(output, torch.max(target, 1)[1]) 165 | pred = output.detach().max(1, keepdim=True)[1] 166 | target_label = torch.max(target.detach(), 1, keepdim=True)[1] 167 | curCorrect = pred.eq(target_label).long().sum() 168 | curAcc = 100.0*curCorrect / len(data) 169 | correct += curCorrect 170 | accLoss += loss.detach()*len(data) 171 | loss.backward() 172 | optimizer.step() 173 | scheduler.step() 174 | 175 | # Log stats to tensorboard 176 | #writer.add_scalar('train_loss', loss.detach().cpu().numpy(), epoch*steps + batch_idx) 177 | #writer.add_scalar('train_accuracy', curAcc.detach().cpu().numpy(), epoch*steps + batch_idx) 178 | #g = optimizer.param_groups[0] 179 | #writer.add_scalar('LR', g['lr'], epoch*steps + batch_idx) 180 | 181 | accLoss /= len(train_loader.dataset) 182 | accuracy = 100.0*correct / len(train_loader.dataset) 183 | print(f"Epoch: {epoch}/{num_epochs}\tTrain Acc (%): {accuracy.detach().cpu().numpy():.2f}\tTrain Loss: {accLoss.detach().cpu().numpy():.3e}") 184 | #for g in optimizer.param_groups: 185 | # print("LR: {:.6f} ".format(g['lr'])) 186 | # print("LR: {:.6f} ".format(g['weight_decay'])) 187 | writer.add_scalar('avg_train_loss', accLoss.detach().cpu().numpy(), (epoch+1)*steps) 188 | writer.add_scalar('avg_train_accuracy', accuracy.detach().cpu().numpy(), (epoch+1)*steps) 189 | val_accuracy, val_avg_roc_auc = test(model, val_loader, options["cuda"]) 190 | test_accuracy, test_avg_roc_auc = test(model, test_loader, options["cuda"]) 191 | modelSave = { 'model_dict': model.state_dict(), 192 | 'optim_dict': optimizer.state_dict(), 193 | 'val_accuracy': val_accuracy, 194 | 'test_accuracy': test_accuracy, 195 | 'val_avg_roc_auc': val_avg_roc_auc, 196 | 'test_avg_roc_auc': test_avg_roc_auc, 197 | 'epoch': epoch} 198 | torch.save(modelSave, options["log_dir"] + "/checkpoint.pth") 199 | if(maxAcc None: 38 | training = model.training 39 | model.eval() 40 | for name, module in model.named_modules(): 41 | if type(module) == SparseLinearNeq: 42 | if verbose: 43 | print(f"Calculating truth tables for {name}") 44 | module.calculate_truth_tables() 45 | if verbose: 46 | print(f"Truth tables generated for {len(module.neuron_truth_tables)} neurons") 47 | model.training = training 48 | 49 | # TODO: Create a container module which performs this function. 50 | def lut_inference(model: nn.Module) -> None: 51 | for name, module in model.named_modules(): 52 | if type(module) == SparseLinearNeq: 53 | module.lut_inference() 54 | 55 | # TODO: Create a container module which performs this function. 56 | def neq_inference(model: nn.Module) -> None: 57 | for name, module in model.named_modules(): 58 | if type(module) == SparseLinearNeq: 59 | module.neq_inference() 60 | 61 | # TODO: Should this go in with the other verilog functions? 62 | # TODO: Support non-linear topologies 63 | def module_list_to_verilog_module(module_list: nn.ModuleList, module_name: str, output_directory: str, add_registers: bool = True, generate_bench: bool = True): 64 | input_bitwidth = None 65 | output_bitwidth = None 66 | module_contents = "" 67 | for i in range(len(module_list)): 68 | m = module_list[i] 69 | if type(m) == SparseLinearNeq: 70 | module_prefix = f"layer{i}" 71 | module_input_bits, module_output_bits = m.gen_layer_verilog(module_prefix, output_directory, generate_bench=generate_bench) 72 | if i == 0: 73 | input_bitwidth = module_input_bits 74 | if i == len(module_list)-1: 75 | output_bitwidth = module_output_bits 76 | module_contents += layer_connection_verilog( module_prefix, 77 | input_string=f"M{i}", 78 | input_bits=module_input_bits, 79 | output_string=f"M{i+1}", 80 | output_bits=module_output_bits, 81 | output_wire=i!=len(module_list)-1, 82 | register=add_registers) 83 | else: 84 | raise Exception(f"Expect type(module) == SparseLinearNeq, {type(module)} found") 85 | module_list_verilog = generate_logicnets_verilog( module_name=module_name, 86 | input_name="M0", 87 | input_bits=input_bitwidth, 88 | output_name=f"M{len(module_list)}", 89 | output_bits=output_bitwidth, 90 | module_contents=module_contents) 91 | reg_verilog = generate_register_verilog() 92 | with open(f"{output_directory}/myreg.v", "w") as f: 93 | f.write(reg_verilog) 94 | with open(f"{output_directory}/{module_name}.v", "w") as f: 95 | f.write(module_list_verilog) 96 | 97 | class SparseLinear(nn.Linear): 98 | def __init__(self, in_features: int, out_features: int, mask: nn.Module, bias: bool = True) -> None: 99 | super(SparseLinear, self).__init__(in_features=in_features, out_features=out_features, bias=bias) 100 | self.mask = mask 101 | 102 | def forward(self, input: Tensor) -> Tensor: 103 | return F.linear(input, self.weight*self.mask(), self.bias) 104 | 105 | # TODO: Perhaps make this two classes, separating the LUT and NEQ code. 106 | class SparseLinearNeq(nn.Module): 107 | def __init__(self, in_features: int, out_features: int, input_quant, output_quant, sparse_linear_kws={}, apply_input_quant=True, apply_output_quant=True) -> None: 108 | super(SparseLinearNeq, self).__init__() 109 | self.in_features = in_features 110 | self.out_features = out_features 111 | self.input_quant = input_quant 112 | self.fc = SparseLinear(in_features, out_features, **sparse_linear_kws) 113 | self.output_quant = output_quant 114 | self.is_lut_inference = False 115 | self.neuron_truth_tables = None 116 | self.apply_input_quant = apply_input_quant 117 | self.apply_output_quant = apply_output_quant 118 | 119 | def lut_cost(self): 120 | """ 121 | Approximate how many 6:1 LUTs are needed to implement this layer using 122 | LUTCost() as defined in LogicNets paper FPL'20: 123 | LUTCost(X, Y) = (Y / 3) * (2^(X - 4) - (-1)^X) 124 | where: 125 | * X: input fanin bits 126 | * Y: output bits 127 | LUTCost() estimates how many LUTs are needed to implement 1 neuron, so 128 | we then multiply LUTCost() by the number of neurons to get the total 129 | number of LUTs needed. 130 | NOTE: This function (over)estimates how many 6:1 LUTs are needed to implement 131 | this layer b/c it assumes every neuron is connected to the next layer 132 | since we do not have the next layer's sparsity information. 133 | """ 134 | # Compute LUTCost of 1 neuron 135 | _, input_bitwidth = self.input_quant.get_scale_factor_bits() 136 | _, output_bitwidth = self.output_quant.get_scale_factor_bits() 137 | input_bitwidth, output_bitwidth = int(input_bitwidth), int(output_bitwidth) 138 | x = input_bitwidth * self.fc.mask.fan_in # neuron input fanin 139 | y = output_bitwidth 140 | neuron_lut_cost = (y / 3) * ((2 ** (x - 4)) - ((-1) ** x)) 141 | # Compute total LUTCost 142 | return self.out_features * neuron_lut_cost 143 | 144 | # TODO: Move the verilog string templates to elsewhere 145 | # TODO: Move this to another class 146 | # TODO: Update this code to support custom bitwidths per input/output 147 | def gen_layer_verilog(self, module_prefix, directory, generate_bench: bool = True): 148 | _, input_bitwidth = self.input_quant.get_scale_factor_bits() 149 | _, output_bitwidth = self.output_quant.get_scale_factor_bits() 150 | input_bitwidth, output_bitwidth = int(input_bitwidth), int(output_bitwidth) 151 | total_input_bits = self.in_features*input_bitwidth 152 | total_output_bits = self.out_features*output_bitwidth 153 | layer_contents = f"module {module_prefix} (input [{total_input_bits-1}:0] M0, output [{total_output_bits-1}:0] M1);\n\n" 154 | output_offset = 0 155 | for index in range(self.out_features): 156 | module_name = f"{module_prefix}_N{index}" 157 | indices, _, _, _ = self.neuron_truth_tables[index] 158 | neuron_verilog = self.gen_neuron_verilog(index, module_name) # Generate the contents of the neuron verilog 159 | with open(f"{directory}/{module_name}.v", "w") as f: 160 | f.write(neuron_verilog) 161 | if generate_bench: 162 | neuron_bench = self.gen_neuron_bench(index, module_name) # Generate the contents of the neuron verilog 163 | with open(f"{directory}/{module_name}.bench", "w") as f: 164 | f.write(neuron_bench) 165 | connection_string = generate_neuron_connection_verilog(indices, input_bitwidth) # Generate the string which connects the synapses to this neuron 166 | wire_name = f"{module_name}_wire" 167 | connection_line = f"wire [{len(indices)*input_bitwidth-1}:0] {wire_name} = {{{connection_string}}};\n" 168 | inst_line = f"{module_name} {module_name}_inst (.M0({wire_name}), .M1(M1[{output_offset+output_bitwidth-1}:{output_offset}]));\n\n" 169 | layer_contents += connection_line + inst_line 170 | output_offset += output_bitwidth 171 | layer_contents += "endmodule" 172 | with open(f"{directory}/{module_prefix}.v", "w") as f: 173 | f.write(layer_contents) 174 | return total_input_bits, total_output_bits 175 | 176 | # TODO: Move the verilog string templates to elsewhere 177 | # TODO: Move this to another class 178 | def gen_neuron_verilog(self, index, module_name): 179 | indices, input_perm_matrix, float_output_states, bin_output_states = self.neuron_truth_tables[index] 180 | _, input_bitwidth = self.input_quant.get_scale_factor_bits() 181 | _, output_bitwidth = self.output_quant.get_scale_factor_bits() 182 | cat_input_bitwidth = len(indices)*input_bitwidth 183 | lut_string = "" 184 | num_entries = input_perm_matrix.shape[0] 185 | for i in range(num_entries): 186 | entry_str = "" 187 | for idx in range(len(indices)): 188 | val = input_perm_matrix[i,idx] 189 | entry_str += self.input_quant.get_bin_str(val) 190 | res_str = self.output_quant.get_bin_str(bin_output_states[i]) 191 | lut_string += f"\t\t\t{int(cat_input_bitwidth)}'b{entry_str}: M1r = {int(output_bitwidth)}'b{res_str};\n" 192 | return generate_lut_verilog(module_name, int(cat_input_bitwidth), int(output_bitwidth), lut_string) 193 | 194 | # TODO: Move the string templates to bench.py 195 | # TODO: Move this to another class 196 | def gen_neuron_bench(self, index, module_name): 197 | indices, input_perm_matrix, float_output_states, bin_output_states = self.neuron_truth_tables[index] 198 | _, input_bitwidth = self.input_quant.get_scale_factor_bits() 199 | _, output_bitwidth = self.output_quant.get_scale_factor_bits() 200 | cat_input_bitwidth = len(indices)*input_bitwidth 201 | lut_string = "" 202 | num_entries = input_perm_matrix.shape[0] 203 | # Sort the input_perm_matrix to match the bench format 204 | input_state_space_bin_str = list(map(lambda y: list(map(lambda z: self.input_quant.get_bin_str(z), y)), input_perm_matrix)) 205 | sorted_bin_output_states = sort_to_bench(input_state_space_bin_str, bin_output_states) 206 | # Generate the LUT for each output 207 | for i in range(int(output_bitwidth)): 208 | lut_string += f"M1[{i}] = LUT 0x" 209 | output_bin_str = reduce(lambda b,c: b+c, map(lambda a: self.output_quant.get_bin_str(a)[int(output_bitwidth)-1-i], sorted_bin_output_states)) 210 | lut_hex_string = f"{int(output_bin_str,2):0{int(num_entries/4)}x} " 211 | lut_string += lut_hex_string 212 | lut_string += generate_lut_input_string(int(cat_input_bitwidth)) 213 | return generate_lut_bench(int(cat_input_bitwidth), int(output_bitwidth), lut_string) 214 | 215 | def lut_inference(self): 216 | self.is_lut_inference = True 217 | self.input_quant.bin_output() 218 | self.output_quant.bin_output() 219 | 220 | def neq_inference(self): 221 | self.is_lut_inference = False 222 | self.input_quant.float_output() 223 | self.output_quant.float_output() 224 | 225 | # TODO: This function might be a useful utility outside of this class.. 226 | def table_lookup(self, connected_input: Tensor, input_perm_matrix: Tensor, bin_output_states: Tensor) -> Tensor: 227 | fan_in_size = connected_input.shape[1] 228 | ci_bcast = connected_input.unsqueeze(2) # Reshape to B x Fan-in x 1 229 | pm_bcast = input_perm_matrix.t().unsqueeze(0) # Reshape to 1 x Fan-in x InputStates 230 | eq = (ci_bcast == pm_bcast).sum(dim=1) == fan_in_size # Create a boolean matrix which matches input vectors to possible input states 231 | matches = eq.sum(dim=1) # Count the number of perfect matches per input vector 232 | if not (matches == torch.ones_like(matches,dtype=matches.dtype)).all(): 233 | raise Exception(f"One or more vectors in the input is not in the possible input state space") 234 | indices = torch.argmax(eq.type(torch.int64),dim=1) 235 | return bin_output_states[indices] 236 | 237 | def lut_forward(self, x: Tensor) -> Tensor: 238 | if self.apply_input_quant: 239 | x = self.input_quant(x) # Use this to fetch the bin output of the input, if the input isn't already in binary format 240 | y = torch.zeros((x.shape[0],self.out_features)) 241 | # Perform table lookup for each neuron output 242 | for i in range(self.out_features): 243 | indices, input_perm_matrix, float_output_states, bin_output_states = self.neuron_truth_tables[i] 244 | connected_input = x[:,indices] 245 | y[:,i] = self.table_lookup(connected_input, input_perm_matrix, bin_output_states) 246 | return y 247 | 248 | def forward(self, x: Tensor) -> Tensor: 249 | if self.is_lut_inference: 250 | x = self.lut_forward(x) 251 | else: 252 | if self.apply_input_quant: 253 | x = self.input_quant(x) 254 | x = self.fc(x) 255 | if self.apply_output_quant: 256 | x = self.output_quant(x) 257 | return x 258 | 259 | # Consider using masked_select instead of fetching the indices 260 | def calculate_truth_tables(self): 261 | with torch.no_grad(): 262 | mask = self.fc.mask() 263 | # Precalculate all of the input value permutations 264 | input_state_space = list() # TODO: is a list the right data-structure here? 265 | bin_state_space = list() 266 | for m in range(self.in_features): 267 | neuron_state_space = self.input_quant.get_state_space() # TODO: this call should include the index of the element of interest 268 | bin_space = self.input_quant.get_bin_state_space() # TODO: this call should include the index of the element of interest 269 | input_state_space.append(neuron_state_space) 270 | bin_state_space.append(bin_space) 271 | 272 | neuron_truth_tables = list() 273 | for n in range(self.out_features): 274 | # Determine the fan-in as number of synapse connections 275 | input_mask = mask[n,:] 276 | fan_in = torch.sum(input_mask) 277 | indices = fetch_mask_indices(input_mask) 278 | # Retrieve the possible state space of the current neuron 279 | connected_state_space = [input_state_space[i] for i in indices] 280 | bin_connected_state_space = [bin_state_space[i] for i in indices] 281 | # Generate a matrix containing all possible input states 282 | input_permutation_matrix = generate_permutation_matrix(connected_state_space) 283 | bin_input_permutation_matrix = generate_permutation_matrix(bin_connected_state_space) 284 | num_permutations = input_permutation_matrix.shape[0] 285 | padded_perm_matrix = torch.zeros((num_permutations, self.in_features)) 286 | padded_perm_matrix[:,indices] = input_permutation_matrix 287 | 288 | # TODO: Update this block to just run inference on the fc layer, once BN has been moved to output_quant 289 | apply_input_quant, apply_output_quant = self.apply_input_quant, self.apply_output_quant 290 | self.apply_input_quant, self.apply_output_quant = False, False 291 | is_bin_output = self.output_quant.is_bin_output 292 | self.output_quant.float_output() 293 | output_states = self.output_quant(self.forward(padded_perm_matrix))[:,n] # Calculate float for the current input 294 | self.output_quant.bin_output() 295 | bin_output_states = self.output_quant(self.forward(padded_perm_matrix))[:,n] # Calculate bin for the current input 296 | self.output_quant.is_bin_output = is_bin_output 297 | self.apply_input_quant, self.apply_output_quant = apply_input_quant, apply_output_quant 298 | 299 | # Append the connectivity, input permutations and output permutations to the neuron truth tables 300 | neuron_truth_tables.append((indices, bin_input_permutation_matrix, output_states, bin_output_states)) # Change this to be the binary output states 301 | self.neuron_truth_tables = neuron_truth_tables 302 | 303 | class DenseMask2D(nn.Module): 304 | def __init__(self, in_features: int, out_features: int) -> None: 305 | super(DenseMask2D, self).__init__() 306 | self.in_features = in_features 307 | self.out_features = out_features 308 | self.mask = Parameter(torch.Tensor(out_features, in_features), requires_grad=False) 309 | self.reset_parameters() 310 | 311 | def reset_parameters(self) -> None: 312 | init.constant_(self.mask, 1.0) 313 | 314 | def forward(self): 315 | return self.mask 316 | 317 | class RandomFixedSparsityMask2D(nn.Module): 318 | def __init__(self, in_features: int, out_features: int, fan_in: int) -> None: 319 | super(RandomFixedSparsityMask2D, self).__init__() 320 | self.in_features = in_features 321 | self.out_features = out_features 322 | self.fan_in = fan_in 323 | self.mask = Parameter(torch.Tensor(out_features, in_features), requires_grad=False) 324 | self.reset_parameters() 325 | 326 | def reset_parameters(self) -> None: 327 | init.constant_(self.mask, 0.0) 328 | for i in range(self.out_features): 329 | x = torch.randperm(self.in_features)[:self.fan_in] 330 | self.mask[i][x] = 1 331 | 332 | def forward(self): 333 | return self.mask 334 | 335 | class ScalarScaleBias(nn.Module): 336 | def __init__(self, scale=True, scale_init=1.0, bias=True, bias_init=0.0) -> None: 337 | super(ScalarScaleBias, self).__init__() 338 | if scale: 339 | self.weight = Parameter(torch.Tensor(1)) 340 | else: 341 | self.register_parameter('weight', None) 342 | if bias: 343 | self.bias = Parameter(torch.Tensor(1)) 344 | else: 345 | self.register_parameter('bias', None) 346 | self.weight_init = scale_init 347 | self.bias_init = bias_init 348 | self.reset_parameters() 349 | 350 | # Change the default initialisation for BatchNorm 351 | def reset_parameters(self) -> None: 352 | if self.weight is not None: 353 | init.constant_(self.weight, self.weight_init) 354 | if self.bias is not None: 355 | init.constant_(self.bias, self.bias_init) 356 | 357 | def forward(self, x): 358 | if self.weight is not None: 359 | x = x*self.weight 360 | if self.bias is not None: 361 | x = x + self.bias 362 | return x 363 | 364 | class ScalarBiasScale(ScalarScaleBias): 365 | def forward(self, x): 366 | if self.bias is not None: 367 | x = x + self.bias 368 | if self.weight is not None: 369 | x = x*self.weight 370 | return x 371 | 372 | --------------------------------------------------------------------------------