From ade58e30113dcac3edda75c467a2f5a035d77e64 Mon Sep 17 00:00:00 2001 From: Yifan Zhao <yifanz16@illinois.edu> Date: Thu, 8 Jul 2021 01:21:22 -0500 Subject: [PATCH] Added routine for quantizing miniera model: added distiller as a submodule updated env.yaml auto-install distiller in installer --- .gitmodules | 3 + hpvm/env.yaml | 30 ++--- hpvm/projects/distiller | 1 + hpvm/scripts/hpvm_installer.py | 2 +- hpvm/test/epoch_dnn/torch_dnn/__init__.py | 1 + hpvm/test/epoch_dnn/torch_dnn/quantizer.py | 135 +++++++++++++++++++++ 6 files changed, 157 insertions(+), 15 deletions(-) create mode 160000 hpvm/projects/distiller create mode 100644 hpvm/test/epoch_dnn/torch_dnn/quantizer.py diff --git a/.gitmodules b/.gitmodules index c4a0bd02bc..6d6a1a9975 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "hpvm/projects/sw"] path = hpvm/projects/sw url = https://github.com/nvdla/sw.git +[submodule "hpvm/projects/distiller"] + path = hpvm/projects/distiller + url = git@gitlab.engr.illinois.edu:llvm/distiller.git diff --git a/hpvm/env.yaml b/hpvm/env.yaml index 612cc956c1..3e1f6394c7 100644 --- a/hpvm/env.yaml +++ b/hpvm/env.yaml @@ -1,26 +1,28 @@ name: hpvm channels: - pytorch - - defaults + - conda-forge dependencies: + - Cython - jinja2=2.11 - - jsonpickle=2 - - keras==2.1.6 - matplotlib=3.3 - networkx=2.5 + - onnx==1.8.0 - pandas=1.1 - - python==3.6.13 - pip - - pytorch==1.6.0 - - torchvision=0.7 - - tqdm=4.59 - - scipy==1.1.0 - - h5py==2.10.0 + - pydot=1.4 + - python=3.7 + - pytorch==1.5.0 + - PyYAML + - scikit-learn=0.21 + - scipy=1.3 + - tabulate=0.8 + - tensorflow==1.14.0 + - torchvision==0.6 + - tqdm=4.33 + - xlsxwriter=1.2 - pip: - argparse==1.4 - - onnx==1.8 + - graphviz==0.10 - onnx-simplifier==0.3 - - opentuner==0.8.3 - - sqlalchemy==1.3.0 - - tensorflow==1.14.0 - - tensorflow-gpu==1.14.0 + - torchnet==0.0.4 diff --git a/hpvm/projects/distiller b/hpvm/projects/distiller new file mode 160000 index 0000000000..30c50f368d --- /dev/null +++ b/hpvm/projects/distiller @@ -0,0 +1 @@ +Subproject commit 30c50f368d219efa87d927c0ad0a563bcfa29f0a diff --git a/hpvm/scripts/hpvm_installer.py b/hpvm/scripts/hpvm_installer.py index 16e298f90e..1ee808458e 100755 --- a/hpvm/scripts/hpvm_installer.py +++ b/hpvm/scripts/hpvm_installer.py @@ -31,7 +31,7 @@ MAKE_TARGETS = ["hpvm-clang"] MAKE_TEST_TARGETS = ["check-hpvm-dnn", "check-hpvm-pass"] # Relative to project root which is __file__.parent.parent -PY_PACKAGES = ["projects/torch2hpvm"] +PY_PACKAGES = ["projects/torch2hpvm", "projects/distiller"] PYTHON_REQ = (3, 6) # This means >= 3.6 diff --git a/hpvm/test/epoch_dnn/torch_dnn/__init__.py b/hpvm/test/epoch_dnn/torch_dnn/__init__.py index 62f29d0d7e..a6fb99465f 100644 --- a/hpvm/test/epoch_dnn/torch_dnn/__init__.py +++ b/hpvm/test/epoch_dnn/torch_dnn/__init__.py @@ -1,2 +1,3 @@ from .datasets import CIFAR from .miniera import MiniERA +from .quantizer import quantize diff --git a/hpvm/test/epoch_dnn/torch_dnn/quantizer.py b/hpvm/test/epoch_dnn/torch_dnn/quantizer.py new file mode 100644 index 0000000000..1c9d5d77ea --- /dev/null +++ b/hpvm/test/epoch_dnn/torch_dnn/quantizer.py @@ -0,0 +1,135 @@ +import os +from copy import deepcopy +from pathlib import Path +from typing import Union + +import distiller +import torch +import yaml +from distiller.data_loggers import collect_quant_stats +from distiller.quantization import PostTrainLinearQuantizer +from torch import nn +from torch.utils.data import DataLoader + +from .datasets import CIFAR +from .miniera import MiniERA + +PathLike = Union[str, Path] +STATS_FILENAME = "acts_quantization_stats.yaml" +QUANT_FILENAME = "layer_quant_params.yaml" +LAYER_HPVM_NAME = { + nn.ReLU: "relu", + nn.Linear: "gemm", + nn.Conv2d: "conv", + nn.MaxPool2d: "pool", + nn.Softmax: "softmax", + nn.Parameter: "add", +} +LAYER_DISTILLER_NAME = { + nn.Linear: "fcs", + nn.Conv2d: "convs", + nn.Softmax: "softmax", +} + + +def quantize( + dataset_path: PathLike, + model_chkpt: PathLike, + strat: str = "NONE", + output: PathLike = "calib.txt", +): + # possible quant strats ['NONE', 'AVG', 'N_STD', 'GAUSS', 'LAPLACE'] + print("Quantizing...") + dataset_path = Path(dataset_path) + dataset = CIFAR(dataset_path / "input.bin", dataset_path / "labels.bin") + dataloader = DataLoader(dataset, batch_size=1) + + # Load Model + model = MiniERA() + model.load_state_dict(torch.load(model_chkpt)) + + # Collect Pre Quantization Stats + distiller.utils.assign_layer_fq_names(model) + + if not os.path.isfile(STATS_FILENAME): + # generates STATS_FILENAME + collect_quant_stats( + model, lambda model: evaluate(model, dataloader), save_dir="." + ) + + # Generate Quantized Scales + quantizer = PostTrainLinearQuantizer( + deepcopy(model), + model_activation_stats=STATS_FILENAME, + mode="SYMMETRIC", + bits_activations=8, + bits_accum=32, + clip_acts=strat, + ) + dummy_input = torch.rand(1, 3, 32, 32) + quantizer.prepare_model(dummy_input) + quantizer.save_per_layer_parameters() + + print("Quantization process finished.") + # converts .yaml file stats to hpvm standard + generate_calib_file(model, output) + + +def generate_calib_file(model: MiniERA, output: PathLike): + print("Generating calibration file...") + with open(QUANT_FILENAME, "r") as stream: + scale_data = yaml.safe_load(stream) + + lines = [] + # add scales for input + # fmt: off + input_min_max = scale_data["convs.0"]["model_activation_stats"]["convs.0"]["inputs"][0] + # fmt: on + input_scale = max(abs(input_min_max["min"]), abs(input_min_max["max"])) / 127 + lines.append(f"input:\t{input_scale}\n") + + # because of definition of miniera + layers = [*model.convs, *model.fcs, model.softmax] + layer_count = { + nn.ReLU: 0, + nn.Linear: 0, + nn.Conv2d: 0, + nn.MaxPool2d: 0, + nn.Softmax: 0, + nn.Parameter: 0, + } + # add scales for layers + for layer in layers: + hpvm_name = LAYER_HPVM_NAME[type(layer)] + distiller_typename = LAYER_DISTILLER_NAME[type(layer)] + layer_idx = layer_count[type(layer)] + layer_count[type(layer)] += 1 + + scale_key = f"{distiller_typename}.{layer_idx}.output_scale" + layer_scale = 1 / scale_data["linear_quant_params"][scale_key] + lines.append(f"{hpvm_name}{layer_idx + 1}:\t{layer_scale}\n") + + if isinstance(layer, nn.Conv2d) or isinstance(layer, nn.Linear): + # include 'add' scale + add_hpvm_name = LAYER_HPVM_NAME[nn.Parameter] + add_idx = layer_count[nn.Parameter] + layer_count[nn.Parameter] += 1 + lines.append(f"{add_hpvm_name}{add_idx + 1}:\t{layer_scale}\n") + + with open(output, "w+") as f: + f.writelines(lines) + print(f"Calibration file generated to {output}") + + +@torch.no_grad() +def evaluate(model: MiniERA, dataloader: DataLoader): + from torch.nn import functional as F + + # Turn on evaluation mode which disables dropout. + model.eval() + total_loss = 0 + for batch in dataloader: + data, targets = batch + output = model(data) + total_loss += len(data) * F.cross_entropy(output, targets) + return total_loss / len(dataloader) -- GitLab