diff --git a/hpvm/projects/torch2hpvm/torch2hpvm/compile.py b/hpvm/projects/torch2hpvm/torch2hpvm/compile.py index 06c9a1c297f3bdfd69a2871387a71b91f37aae0e..f76b801ef9e577d1308fa0ca83c7339045ca69d9 100644 --- a/hpvm/projects/torch2hpvm/torch2hpvm/compile.py +++ b/hpvm/projects/torch2hpvm/torch2hpvm/compile.py @@ -62,18 +62,17 @@ class ModelExporter: def export_datasets(self, n_images: Optional[int]): from PIL import Image from math import log10, ceil - - labels = [] - n_images = self.dataset_size if n_images is None else n_images - n_digits = int(ceil(log10(n_images))) - for i in range(n_images): + if n_images: + indices = torch.randperm(self.dataset_size)[:n_images].numpy() + else: + indices = range(self.dataset_size) + n_digits = int(ceil(log10(self.dataset_size))) + for i in indices: image, label = self.dataset[i] image = (image - image.min()) / (image.max() - image.min()) * 255 image = image.transpose((1, 2, 0)).astype(np.uint8) name = str(i).zfill(n_digits) - Image.fromarray(image).save(self.dataset_dir / f"{name}.jpg") - labels.append(label) - np.array(labels).tofile(self.output_dir / self.label_name) + Image.fromarray(image).save(self.dataset_dir / f"{name}_{label}.jpg") return self def compile(self, output_binary: PathLike, working_dir: Optional[PathLike] = None): diff --git a/hpvm/projects/torch2hpvm/torch2hpvm/graph_ir.py b/hpvm/projects/torch2hpvm/torch2hpvm/graph_ir.py index 3ddcb1868576ab9f09444a060a371e1817d1ee3c..06907cd2777838df67e9c9a9a846988cf5ae111c 100644 --- a/hpvm/projects/torch2hpvm/torch2hpvm/graph_ir.py +++ b/hpvm/projects/torch2hpvm/torch2hpvm/graph_ir.py @@ -105,6 +105,9 @@ class WeightTensor(TensorNode): def dump_weight(self, file_name: PathLike, to_fp16: bool = False): data = self.input_data.astype(np.float16) if to_fp16 else self.input_data + if len(data.shape) == 2: + # weight tensor needs to be transposed + data = data.T data.tofile(file_name) def transpose_(self): diff --git a/hpvm/test/epoch_dnn/assets/miniera/miniera.pth b/hpvm/test/epoch_dnn/assets/miniera/miniera.pth index f264a4ea67f88c361923e7c7f0208703001a622d..6796117d7821bc9574ffbb7194f21a03dc559e73 100644 Binary files a/hpvm/test/epoch_dnn/assets/miniera/miniera.pth and b/hpvm/test/epoch_dnn/assets/miniera/miniera.pth differ diff --git a/hpvm/test/epoch_dnn/assets/miniera/scales/calib_NONE.txt b/hpvm/test/epoch_dnn/assets/miniera/scales/calib_NONE.txt index 9bffa5b8d0b5e315820ac3209c4796f1de2c44e5..f99b4434b6f8e6491e738928e80f3ed97bbc1cb0 100644 --- a/hpvm/test/epoch_dnn/assets/miniera/scales/calib_NONE.txt +++ b/hpvm/test/epoch_dnn/assets/miniera/scales/calib_NONE.txt @@ -1,4 +1,4 @@ -nput: 0.007874015748031496 +input: 0.007874015748031496 conv1: 0.041152522047244094 add1: 0.041152522047244094 relu1: 0.03673215196850394 @@ -19,4 +19,3 @@ relu5: 0.20020370078740157 gemm2: 0.6007037007874017 add6: 0.6007037007874017 softmax1: 0.007874015748031496 - diff --git a/hpvm/test/epoch_dnn/main.py b/hpvm/test/epoch_dnn/main.py index fb02a15f40177d01e134fd4afbd8fec77290e167..2a66aae2009739d066aed8aa91a2f8d0fb611869 100644 --- a/hpvm/test/epoch_dnn/main.py +++ b/hpvm/test/epoch_dnn/main.py @@ -9,7 +9,7 @@ from torch2hpvm import BinDataset, ModelExporter self_folder = Path(__file__).parent.absolute() site.addsitedir(self_folder.as_posix()) -from torch_dnn import MiniERA, quantize +from torch_dnn import MiniERA, quantize, CIFAR # Consts (don't change) @@ -38,7 +38,7 @@ def run_test_over_ssh(host: str, password: str, working_dir: str, image_dir: Pat print(f"Running test on remote host {host}...") args = options.split(" ") + [host] - child = pexpect.spawn("ssh", args) + child = pexpect.spawn("ssh", args, timeout=60) child.expect(r"password:") child.sendline(password) child.expect("# ") # The bash prompt @@ -62,7 +62,7 @@ def run_test_over_ssh(host: str, password: str, working_dir: str, image_dir: Pat ASSET_DIR = self_folder / "assets/miniera" QUANT_STRAT = "NONE" # Quantization method WORKING_DIR = Path("/tmp/miniera") -N_IMAGES = 100 +N_IMAGES = 50 # Remote configs SCP_OPTS = "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P 5506" SSH_OPTS = "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 5506" @@ -70,14 +70,17 @@ SCP_HOST = "root@espgate.cs.columbia.edu" SCP_PWD = "openesp" SCP_DST = "~/NV_NVDLA" +# Reproducibility +np.random.seed(42) -makedirs(WORKING_DIR, exist_ok=True) +makedirs(WORKING_DIR, exist_ok=False) # Calculate quantization scales ckpt = (ASSET_DIR / "miniera.pth").as_posix() model = MiniERA() model.load_state_dict(torch.load(ckpt)) -scale_output = quantize(model, ASSET_DIR, QUANT_STRAT, WORKING_DIR) +dataset = CIFAR.from_file(ASSET_DIR / "input.bin", ASSET_DIR / "labels.bin") +scale_output = quantize(model, dataset, QUANT_STRAT, WORKING_DIR) # Code generation (into /tmp/miniera/hpvm-mod.nvdla) nvdla_buffer = WORKING_DIR / BUFFER_NAME @@ -85,7 +88,7 @@ print(f"Generating NVDLA buffer into {nvdla_buffer}") bin_dataset = BinDataset( ASSET_DIR / "input.bin", ASSET_DIR / "labels.bin", (5000, 3, 32, 32) ) -exporter = ModelExporter(model, bin_dataset, WORKING_DIR, scale_output) +exporter = ModelExporter(model, bin_dataset, WORKING_DIR, ASSET_DIR / "scales/calib_NONE.txt") exporter.generate(n_images=N_IMAGES).compile(WORKING_DIR / "miniera", WORKING_DIR) # SCP essential files to remote device @@ -93,8 +96,11 @@ input_images = exporter.dataset_dir split_and_scp([nvdla_buffer, input_images], SCP_HOST, SCP_DST, SCP_PWD, SCP_OPTS) # SSH to run test remotely -labels_file = WORKING_DIR / exporter.label_name -labels = np.fromfile(labels_file, dtype=np.int32) +n_total, correct = 0, 0 for image_path, output in run_test_over_ssh(SCP_HOST, SCP_PWD, SCP_DST, input_images, SSH_OPTS): - idx = int(image_path.stem) - print(idx, np.array(output).argmax(), labels[idx]) + idx, label = [int(s) for s in image_path.stem.split("_")] + output = np.array(output) + print(idx, output.argmax(), label, output) + n_total += 1 + correct += int(output.argmax() == label) +print(f"Accuracy: {correct / n_total * 100}% ({n_total} images)") diff --git a/hpvm/test/epoch_dnn/torch_dnn/miniera.py b/hpvm/test/epoch_dnn/torch_dnn/miniera.py index 84c88ac8f882b4b6d1ba98cdb3a88f92cf418075..3e200673189185b0a5e3c19d037ca47e4ba0154a 100644 --- a/hpvm/test/epoch_dnn/torch_dnn/miniera.py +++ b/hpvm/test/epoch_dnn/torch_dnn/miniera.py @@ -36,8 +36,8 @@ class MiniERA(Module): for conv in self.convs: if not isinstance(conv, Conv2d): continue - weight_np = np.fromfile(prefix / "conv2d_{count+1}_w.bin", dtype=np.float32) - bias_np = np.fromfile(prefix / "conv2d_{count+1}_b.bin", dtype=np.float32) + weight_np = np.fromfile(prefix / f"conv2d_{count+1}_w.bin", dtype=np.float32) + bias_np = np.fromfile(prefix / f"conv2d_{count+1}_b.bin", dtype=np.float32) conv.weight.data = torch.tensor(weight_np).reshape(conv.weight.shape) conv.bias.data = torch.tensor(bias_np).reshape(conv.bias.shape) count += 1 @@ -46,8 +46,8 @@ class MiniERA(Module): for linear in self.fcs: if not isinstance(linear, Linear): continue - weight_np = np.fromfile(prefix / "dense_{count+1}_w.bin", dtype=np.float32) - bias_np = np.fromfile(prefix / "dense_{count+1}_b.bin", dtype=np.float32) + weight_np = np.fromfile(prefix / f"dense_{count+1}_w.bin", dtype=np.float32) + bias_np = np.fromfile(prefix / f"dense_{count+1}_b.bin", dtype=np.float32) cout, cin = linear.weight.shape linear.weight.data = torch.tensor(weight_np).reshape(cin, cout).T linear.bias.data = torch.tensor(bias_np).reshape(linear.bias.shape) diff --git a/hpvm/test/epoch_dnn/torch_dnn/quantizer.py b/hpvm/test/epoch_dnn/torch_dnn/quantizer.py index 8543f80603ab70bade01414664029ade083dcde6..7bbbbda45c3848f7772ea63a0afe875886ed477b 100644 --- a/hpvm/test/epoch_dnn/torch_dnn/quantizer.py +++ b/hpvm/test/epoch_dnn/torch_dnn/quantizer.py @@ -6,6 +6,7 @@ from shutil import move import distiller import torch +from torch.utils.data.dataset import Dataset import yaml from distiller.data_loggers import collect_quant_stats from distiller.quantization import PostTrainLinearQuantizer @@ -13,7 +14,6 @@ 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" @@ -38,15 +38,13 @@ LAYER_DISTILLER_NAME = { def quantize( model: nn.Module, - dataset_path: PathLike, + dataset: Dataset, strat: str = "NONE", working_dir: PathLike = ".", output_name: str = "calib.txt", ): # possible quant strats ['NONE', 'AVG', 'N_STD', 'GAUSS', 'LAPLACE'] print("Quantizing...") - dataset_path = Path(dataset_path) - dataset = CIFAR.from_file(dataset_path / "input.bin", dataset_path / "labels.bin") dataloader = DataLoader(dataset, batch_size=1) # Collect Pre Quantization Stats @@ -57,12 +55,13 @@ def quantize( if not os.path.isfile(stats_file): # generates `stats_file` collect_quant_stats( - model, lambda model: evaluate(model, dataloader), save_dir=working_dir + model, lambda model: get_loss(model, dataloader), save_dir=working_dir ) # Generate Quantized Scales + new_model = deepcopy(model) quantizer = PostTrainLinearQuantizer( - deepcopy(model), + new_model, model_activation_stats=stats_file, mode="SYMMETRIC", bits_activations=8, @@ -77,7 +76,7 @@ def quantize( # We don't need QUANT_AFTER_FILENAME, remove it Path(QUANT_AFTER_FILENAME).unlink() - print("Quantization process finished.") + print(f"Quantization process finished; accuracy {evaluate(new_model, dataloader)}%") # converts .yaml file stats to hpvm standard generate_calib_file(model, working_dir, working_dir / output_name) return working_dir / output_name @@ -96,7 +95,6 @@ def generate_calib_file(model: nn.Module, working_dir: Path, output_file: Path): 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 layer_count = { nn.ReLU: 0, nn.Linear: 0, @@ -131,7 +129,7 @@ def generate_calib_file(model: nn.Module, working_dir: Path, output_file: Path): @torch.no_grad() -def evaluate(model: MiniERA, dataloader: DataLoader): +def get_loss(model: nn.Module, dataloader: DataLoader): from torch.nn import functional as F # Turn on evaluation mode which disables dropout. @@ -142,3 +140,17 @@ def evaluate(model: MiniERA, dataloader: DataLoader): output = model(data) total_loss += len(data) * F.cross_entropy(output, targets) return total_loss / len(dataloader) + + +@torch.no_grad() +def evaluate(model: nn.Module, dataloader: DataLoader): + model.eval() + correct = 0 + total = 0 + for data in dataloader: + images, labels = data[0], data[1] + outputs = model(images) + _, predicted = torch.max(outputs.data, 1) + total += labels.size(0) + correct += (predicted == labels).sum().item() + return 100 * correct / total