From 1be5fb23609e3d4ec0d72a8a58f0670b6812037f Mon Sep 17 00:00:00 2001
From: Yifan Zhao <yifanz16@illinois.edu>
Date: Tue, 13 Jul 2021 05:53:22 -0500
Subject: [PATCH] Bring miniera model up to date

---
 .../miniera/{miniera.pth => miniera.ckpt}     | Bin 1908903 -> 1909486 bytes
 hpvm/test/epoch_dnn/miniera.py                |  62 +++++++++++++
 hpvm/test/epoch_dnn/torch_dnn/__init__.py     |   1 +
 hpvm/test/epoch_dnn/torch_dnn/_utils.py       |  47 ++++++++++
 .../epoch_dnn/torch_dnn/miniera/__init__.py   |   3 +-
 .../test/epoch_dnn/torch_dnn/miniera/model.py |  84 ++++++++++++++----
 hpvm/test/epoch_dnn/yolo.py                   |  70 ++-------------
 7 files changed, 184 insertions(+), 83 deletions(-)
 rename hpvm/test/epoch_dnn/assets/miniera/{miniera.pth => miniera.ckpt} (99%)
 create mode 100644 hpvm/test/epoch_dnn/miniera.py
 create mode 100644 hpvm/test/epoch_dnn/torch_dnn/_utils.py

diff --git a/hpvm/test/epoch_dnn/assets/miniera/miniera.pth b/hpvm/test/epoch_dnn/assets/miniera/miniera.ckpt
similarity index 99%
rename from hpvm/test/epoch_dnn/assets/miniera/miniera.pth
rename to hpvm/test/epoch_dnn/assets/miniera/miniera.ckpt
index 6796117d7821bc9574ffbb7194f21a03dc559e73..7bf0e767bd063e42c16aa10f6eca4649f553efc9 100644
GIT binary patch
delta 2590
zcmZ`*TXz#x6i(WvX#!~rlv@h60%}l+llw%JMo~n@R2jUCmm1RP(1}em`7$TmbQ~00
zig+pBD&F7x0k~FQT%rqo;z7hme*$aypvxy-+%uE337sbETWe<TGvD6dx$J$u|Aqbi
z8=Jo%8}LLVpU2}-s+z3AD|n?f^fp=6#*5{G&Xg)T$hMVi)l*EX$?BeRX;NjCQf0Dm
zT&d|Lt%6RQAr?(Ud2~rZ8mqG+QwkF$nZbAQLU?EQZV~>7w2535g`Lo9Q&Cx~9M_{<
z^n_yZq37byJIp{NBKhslwOVmfK~T0s1`m|AB0ECQWDH`B)JtQs$z&{^%EXf~E*s~u
zR_Y|9T&*LNKiWCU(0f?$M1*0X)A}>?MT9{iK+j%={`0Dp&^m_oA~r|^&8*Cpw8l$C
z9Rn(5qz2XYhzXvfA2zBXVVDAKqCmp%3^ohi8EkQ=w=#r947v!8HwAB7$!(Gxp3Cq_
z4%^jG-m{C??`X)dg)j_7^26Syj$tR*oeVof>|EBdBqJWRW$aoRDwWPAGBKXxaaTje
z-G>F!)^U%eBjQl+X1G_xeJ(muO&#~UkZ0lvK9S;+*kj2!NHU_fjGVI`3=g!*;G9@7
zW+E;k;i4nu=t#PSie(Zpj&!6*M~)(;t>wtgAY;kk9qKH@gCh32$jCHhJVe~)%C+Wc
zmnD;U*wQgi?E4xza?Y+AiR5z~&E?2B!5(3FRK$K485u{$V{S6yJfAi>p2On}9S0gZ
zp0IQrbf}+XI3z-7qdq3LoT8^(sk0oHOvI5_Lx()MZV$;ACGxz-o6mL6;%UK4kF$8@
zylEec!wjN`Bhvcjv7qhb1hF^Mv#uc$d^%0r_Bm^wycFPhC%_C|u+o>D^j~CnNyN)5
z(r=y#6R)_EXH)4I9q}=X+(+cET2te*lP(Be!8ePePJklAxCnVg>Wy<S;l`eb#WJZx
z8j2b+1&jTd%|3%kD|6K$E-}0&VybSSECpy@3eyTJP83-Y)ApQIA`)3W&<s@V3f3%*
z5WKo=Xp}wF4CtzD4Yka#T?}C|19ivQce^vn;kbblt9fHMY2egq-Z)Mhczrc*0&f^F
z$ZJxrD+4F-rU7%ck5f2f;H}lXX}oRV>}uW&-Z3z<>^0ka1CLn)=a${=-Yn(}oOird
z%W8UQtKwY)7aULPB-@)9?-_XC-4`?Qfq@U*opH<?xajy==SbI%vFYMTd}QEb+gsPi
z?JXh~%jNN+Jf-6k1E0!`?pavsp3$WN(P?c$DHmkbROG3uR;sXI-m>ghm)`N2fd!{G
zPHUACbwYeTD)p1>aYbhZhd`ILnu1HC_8PX>t3{^b3ssY>$&l-{no?l}XYiM!_U4=@
z)vQ4#z8bCTHmz1=rRmaXrABuEYUMh<9>X^_L77als!Y_&)cx`87{04V&vcAg_lV`T
zywqL1Eyhf<RClx*sa4qRv{ULeDZgf0Z#0mu+lJaz#@A+STmZV;jEy$xZ#O!{yr<pB
zr>(LZAmhH}@QsogY#Z9BLv6Q#X8BmFL>K=0Jv_Jg?;qhF?}B_iJa_i$(Bjo0Z(#WH
zkAwcM#i0$`=R%8rj)Z&W&)vEaCJZj#y0NV%w|Mh9eRA&Fh=mS9C!verBXkq|gdReG
z5G1T2tR;j9y@WnOKVcnVJz)c3fUuFUiLjZlg|Kzu+DJHX`3Jv$V4HR4vwl-M{@dHt
U8@ck+KR5SvUFyGb=-1rH|4}0OX#fBK

delta 2002
zcmZ{l+jA3T7{+&VPMU^N%Bd|HDryjn$!3%6ia=CE?3yZzhj`$ShO}Fu4Nvp6;9*?^
z(-yd?(+kJHzzgs2A~RmdIOvTt{3XtC;fyz4`F{H)CE1C|JTp5_cAodQ@4nxg$A7RN
ze`A~XzI!}AAssd$rz#0al4^~m)n+=AzFuFLw-|g|N8~BTpGsB2^kb7XmaFr11Zs3q
zvtp;OHmdBLFk=Y10eVhX3u-Q->N!O(AXM@5)N3mUmoNEe7$Rp4DP<%LKmRgBQ^tf5
zrE8fXcE{o*Lkw{f!<Bd!$045Cg=!NCi%6`1<*B3<i>itpR@g{Vfe|WTBwN^N$SsVD
z$XyI$CU*Bcq0)WAJ%cmVLM~UpUMpOd4mp&%uY>J^Gwe^5lX91PTojpLm^5+V33pl%
z*iQ{Ek<aQ>;^_`{vV(n)V^4|1XBZBdc-HCR&Ud+=8yKo+SyfT-Jm)?`*eMsgB>K&8
zxEK2cQRE22Q4=rrVCMyPx?4fi-+r;0meW+cMBF8+km1~=7P6dM5s^6t)kMAryV%7p
zbVK`xMbk4y1x1d%NugQ?yW|du13gtP`MT|XSrj?O@QR7!J={gXed2#gXbN@aWCy#{
z!G4uvza|o2XE<fz4L5PF_T&^eI6XL0(RC$*H?8orR0>JNeTGuYl3Xr@*6@}g)8!hb
zMYzH6wuy2jMpG@q-I1Odn5-1_LP5nlydUz6!|<*MZs9EFHAUn(hWAXozs<Wn(;o~D
zE$A6d!-qUHNU#;&f}qRzks%wwHGC{`KVkUP#QAMhr!&VF2F9v-HkVa!ks8ZWFS)5L
zR5|FZ2&^$&Hc?-(F;|Jw^Iu%7v+Ct4t76_g@m8uru7m{}pSg=>wZ7b3Xe{B1AvY^Q
zhkkSS60X`<v|N*~qt33vl8uJf&ElF3cwGg{Hkw{H2WDf%>#A6_ah<zP(}fb{^0;B+
zbMMUsd|~5DuUo`dHf*n};cFX?*VS>;#y7&{E0bmfw`|-d$Kg#7Ryu=kZM4K~T`!9@
z8|(d^f(;vYgy*jA#vEI$-oRZO_gr_SIh(%eI}b_h{qE^j#E{6n$J5Dl=F!*dW;(mZ
z-)E}gOa%H(u}wW|TkOA3|D9r&hx^P6s^*^ENS{k4-$PN4=`NC3zbPhk$YZ+yPw{%X
z{b%ApWUM`YRE`90kH*`haXFe?f0zi6f}{{BOp1`Aq!?+46ekUn5~LlZ5z<c5C}|gI
zjI^7yhqRZpkF=jOPMRQ1e*cgPw|_b=hc~zWjc#n+9olIBG8~A=Ki`T)<n{ZHxP0yV
okNlC+B!Aibb7ktEV}V5K!LNV+bt3S?$b;$MODEe?8F^#)Kl6kUQUCw|

diff --git a/hpvm/test/epoch_dnn/miniera.py b/hpvm/test/epoch_dnn/miniera.py
new file mode 100644
index 0000000000..398731d1ed
--- /dev/null
+++ b/hpvm/test/epoch_dnn/miniera.py
@@ -0,0 +1,62 @@
+import site
+import sys
+from os import makedirs
+from pathlib import Path
+
+import numpy as np
+from torch2hpvm import ModelExporter
+
+self_folder = Path(__file__).parent.absolute()
+site.addsitedir(self_folder.as_posix())
+
+from torch_dnn import BUFFER_NAME, quantize, run_test_over_ssh, split_and_scp
+from torch_dnn.miniera import MiniEraPL
+
+# Local configs
+ASSET_DIR = self_folder / "assets/miniera"
+QUANT_STRAT = "NONE"  # Quantization method
+WORKING_DIR = Path("/tmp/miniera")
+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"
+SCP_HOST = "root@espgate.cs.columbia.edu"
+SCP_PWD = "openesp"
+SCP_DST = "~/NV_NVDLA"
+
+if len(sys.argv) > 1:
+    WORKING_DIR = Path(sys.argv[1])
+
+# Reproducibility
+np.random.seed(42)
+
+# Create working directory
+makedirs(WORKING_DIR, exist_ok=True)
+
+# Calculate quantization scales
+ckpt = (ASSET_DIR / "miniera.ckpt").as_posix()
+model: MiniEraPL = MiniEraPL.load_from_checkpoint(ckpt, dataset_path=ASSET_DIR)
+scale_output = quantize(model, QUANT_STRAT, WORKING_DIR)
+
+# Code generation (into /tmp/miniera/hpvm-mod.nvdla)
+nvdla_buffer = WORKING_DIR / BUFFER_NAME
+print(f"Generating NVDLA buffer into {nvdla_buffer}")
+# You may replace scale_output (below) with ASSET_DIR / "scales/calib_NONE.txt"
+# to use precomputed quantization scale
+dataset = model.test_dataloader().dataset
+exporter = ModelExporter(model, 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
+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
+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, 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/__init__.py b/hpvm/test/epoch_dnn/torch_dnn/__init__.py
index 89b65549ba..af88db3e23 100644
--- a/hpvm/test/epoch_dnn/torch_dnn/__init__.py
+++ b/hpvm/test/epoch_dnn/torch_dnn/__init__.py
@@ -1 +1,2 @@
 from .quantizer import quantize
+from ._utils import split_and_scp, run_test_over_ssh, BUFFER_NAME
diff --git a/hpvm/test/epoch_dnn/torch_dnn/_utils.py b/hpvm/test/epoch_dnn/torch_dnn/_utils.py
new file mode 100644
index 0000000000..630100bb46
--- /dev/null
+++ b/hpvm/test/epoch_dnn/torch_dnn/_utils.py
@@ -0,0 +1,47 @@
+from pathlib import Path
+import pexpect
+
+# This is hardcoded in the NVDLA pass and will not change any time soon
+BUFFER_NAME = "hpvm-mod.nvdla"
+
+
+def split_and_scp(
+    local_srcs: list, host: str, remote_dst: str, password: str, options: str
+):
+    print(f"Copying files to remote host {host}...")
+    args = options.split(" ")
+    local_srcs = [str(s) for s in local_srcs]
+    args += ["-r", *local_srcs, f"{host}:{remote_dst}"]
+    child = pexpect.spawn("scp", args, timeout=None)
+    child.expect(r"password:")
+    child.sendline(password)
+    # A rough approach to at least print something when scp is alive
+    for line in child:
+        print(line.decode(), end="")
+
+
+def run_test_over_ssh(
+    host: str, password: str, working_dir: str, image_dir: Path, options: str
+):
+    print(f"Running test on remote host {host}...")
+    args = options.split(" ") + [host]
+    child = pexpect.spawn("ssh", args, timeout=None)
+    child.expect(r"password:")
+    child.sendline(password)
+    child.expect("# ")  # The bash prompt
+    child.sendline(f"cd {working_dir}")
+    child.expect("# ")
+    child.delimiter = "# "
+    for image in image_dir.glob("*"):
+        remote_path = f"{image_dir.name}/{image.name}"
+        print(f"Sending {image.name} to run")
+        child.sendline(
+            f"./nvdla_runtime --loadable {BUFFER_NAME} --image {remote_path} --rawdump"
+        )
+        child.expect("# ")
+        child.sendline("cat output.dimg")
+        child.expect("# ")
+        result_lines = child.before.decode().splitlines()
+        # Should have 2 lines. First line is the command we keyed in.
+        output = [int(s) for s in result_lines[1].strip().split()]
+        yield image, output
diff --git a/hpvm/test/epoch_dnn/torch_dnn/miniera/__init__.py b/hpvm/test/epoch_dnn/torch_dnn/miniera/__init__.py
index d135d1bd40..5ed605176e 100644
--- a/hpvm/test/epoch_dnn/torch_dnn/miniera/__init__.py
+++ b/hpvm/test/epoch_dnn/torch_dnn/miniera/__init__.py
@@ -1,2 +1 @@
-from .dataset import CIFAR
-from .model import MiniERA
+from .model import MiniEraPL
diff --git a/hpvm/test/epoch_dnn/torch_dnn/miniera/model.py b/hpvm/test/epoch_dnn/torch_dnn/miniera/model.py
index 0f6fe455d8..b03df902de 100644
--- a/hpvm/test/epoch_dnn/torch_dnn/miniera/model.py
+++ b/hpvm/test/epoch_dnn/torch_dnn/miniera/model.py
@@ -2,27 +2,33 @@ from pathlib import Path
 from typing import Union
 
 import numpy as np
+import pytorch_lightning as pl
 import torch
-from torch.nn import Conv2d, Linear, MaxPool2d, Module, ReLU, Sequential, Softmax
+from torch import nn
+from torch.utils.data import DataLoader
 
+from .dataset import CIFAR
 
-class MiniERA(Module):
+PathLike = Union[Path, str]
+
+
+class MiniERA(nn.Module):
     def __init__(self):
         super().__init__()
-        self.convs = Sequential(
-            Conv2d(3, 32, 3),
-            ReLU(),
-            Conv2d(32, 32, 3),
-            ReLU(),
-            MaxPool2d(2, 2),
-            Conv2d(32, 64, 3),
-            ReLU(),
-            Conv2d(64, 64, 3),
-            ReLU(),
-            MaxPool2d(2, 2),
+        self.convs = nn.Sequential(
+            nn.Conv2d(3, 32, 3),
+            nn.ReLU(),
+            nn.Conv2d(32, 32, 3),
+            nn.ReLU(),
+            nn.MaxPool2d(2, 2),
+            nn.Conv2d(32, 64, 3),
+            nn.ReLU(),
+            nn.Conv2d(64, 64, 3),
+            nn.ReLU(),
+            nn.MaxPool2d(2, 2),
         )
-        self.fcs = Sequential(Linear(1600, 256), ReLU(), Linear(256, 5))
-        self.softmax = Softmax(1)
+        self.fcs = nn.Sequential(nn.Linear(1600, 256), nn.ReLU(), nn.Linear(256, 5))
+        self.softmax = nn.Softmax(1)
 
     def forward(self, input):
         outputs = self.convs(input)
@@ -34,7 +40,7 @@ class MiniERA(Module):
         # Load in model convolution weights
         count = 0
         for conv in self.convs:
-            if not isinstance(conv, Conv2d):
+            if not isinstance(conv, nn.Conv2d):
                 continue
             weight_np = np.fromfile(
                 prefix / f"conv2d_{count+1}_w.bin", dtype=np.float32
@@ -46,7 +52,7 @@ class MiniERA(Module):
         # Load in model fc weights
         count = 0
         for linear in self.fcs:
-            if not isinstance(linear, Linear):
+            if not isinstance(linear, nn.Linear):
                 continue
             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)
@@ -55,3 +61,47 @@ class MiniERA(Module):
             linear.bias.data = torch.tensor(bias_np).reshape(linear.bias.shape)
             count += 1
         return self
+
+
+class MiniEraPL(pl.LightningModule):
+    def __init__(self, dataset_path: PathLike):
+        super().__init__()
+        self.network = MiniERA()
+        self.dataset_path = Path(dataset_path)
+
+    def forward(self, image):
+        prediction = self.network(image)
+        return prediction
+
+    @staticmethod
+    def _get_loss(output, targets):
+        from torch.nn.functional import cross_entropy
+
+        return cross_entropy(output, targets)
+
+    @staticmethod
+    def _get_metric(output, targets):
+        predicted = torch.argmax(output, 1)
+        return (predicted == targets).sum().item() / len(targets)
+
+    def validation_step(self, val_batch, batch_idx):
+        images, target = val_batch
+        prediction = self(images)
+        loss = self._get_loss(prediction, target)
+        accuracy = self._get_metric(prediction, target)
+        self.log("val_loss", loss)
+        self.log("val_acc", accuracy)
+        return accuracy
+
+    def test_step(self, test_batch, batch_idx):
+        images, target = test_batch
+        prediction = self(images)
+        accuracy = self._get_metric(prediction, target)
+        self.log("test_acc", accuracy)
+        return accuracy
+
+    def test_dataloader(self):
+        dataset = CIFAR.from_file(
+            self.dataset_path / "input.bin", self.dataset_path / "labels.bin"
+        )
+        return DataLoader(dataset, batch_size=128)
diff --git a/hpvm/test/epoch_dnn/yolo.py b/hpvm/test/epoch_dnn/yolo.py
index aa1ac34210..16c01fd287 100644
--- a/hpvm/test/epoch_dnn/yolo.py
+++ b/hpvm/test/epoch_dnn/yolo.py
@@ -9,59 +9,9 @@ from torch2hpvm import ModelExporter
 self_folder = Path(__file__).parent.absolute()
 site.addsitedir(self_folder.as_posix())
 
-from torch_dnn import quantize
+from torch_dnn import BUFFER_NAME, quantize, run_test_over_ssh, split_and_scp
 from torch_dnn.yolo import TinyYoloPL
 
-# Consts (don't change)
-BUFFER_NAME = "hpvm-mod.nvdla"
-
-
-def split_and_scp(
-    local_srcs: list, host: str, remote_dst: str, password: str, options: str
-):
-    import pexpect
-
-    print(f"Copying files to remote host {host}...")
-    args = options.split(" ")
-    local_srcs = [str(s) for s in local_srcs]
-    args += ["-r", *local_srcs, f"{host}:{remote_dst}"]
-    child = pexpect.spawn("scp", args, timeout=None)
-    child.expect(r"password:")
-    child.sendline(password)
-    # A rough approach to at least print something when scp is alive
-    for line in child:
-        print(line.decode(), end="")
-
-
-def run_test_over_ssh(
-    host: str, password: str, working_dir: str, image_dir: Path, options: str
-):
-    import pexpect
-
-    print(f"Running test on remote host {host}...")
-    args = options.split(" ") + [host]
-    child = pexpect.spawn("ssh", args, timeout=None)
-    child.expect(r"password:")
-    child.sendline(password)
-    child.expect("# ")  # The bash prompt
-    child.sendline(f"cd {working_dir}")
-    child.expect("# ")
-    child.delimiter = "# "
-    for image in image_dir.glob("*"):
-        remote_path = f"{image_dir.name}/{image.name}"
-        print(f"Sending {image.name} to run")
-        child.sendline(
-            f"./nvdla_runtime --loadable {BUFFER_NAME} --image {remote_path} --rawdump"
-        )
-        child.expect("# ")
-        child.sendline("cat output.dimg")
-        child.expect("# ")
-        result_lines = child.before.decode().splitlines()
-        # Should have 2 lines. First line is the command we keyed in.
-        output = [int(s) for s in result_lines[1].strip().split()]
-        yield image, output
-
-
 # Local configs
 ASSET_DIR = self_folder / "assets/yolo"
 QUANT_STRAT = "NONE"  # Quantization method
@@ -84,17 +34,16 @@ np.random.seed(42)
 makedirs(WORKING_DIR, exist_ok=True)
 
 # Calculate quantization scales
-# model = TinyYoloPL(num_classes=12)
 ckpt = (ASSET_DIR / "yolo_atrv2.ckpt").as_posix()
 model = TinyYoloPL.load_from_checkpoint(
     ckpt, num_classes=12, dataset_path=ASSET_DIR / "atr_dataset.tar.gz"
 )
-dataset = model.test_dataloader().dataset
 scale_output = quantize(model, QUANT_STRAT, WORKING_DIR, gpus=1)
 
 # Code generation (into /tmp/miniera/hpvm-mod.nvdla)
 nvdla_buffer = WORKING_DIR / BUFFER_NAME
 print(f"Generating NVDLA buffer into {nvdla_buffer}")
+dataset = model.test_dataloader().dataset
 exporter = ModelExporter(model, dataset, WORKING_DIR, scale_output)
 exporter.generate(n_images=N_IMAGES).compile(WORKING_DIR / "miniera", WORKING_DIR)
 
@@ -103,14 +52,7 @@ 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
-# n_total, correct = 0, 0
-# image_path, output = next(
-#     run_test_over_ssh(SCP_HOST, SCP_PWD, SCP_DST, input_images, SSH_OPTS)
-# )
-# for image_path, output in run_test_over_ssh(SCP_HOST, SCP_PWD, SCP_DST, input_images, SSH_OPTS):
-#     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)")
+n_total, correct = 0, 0
+image_path, output = next(
+    run_test_over_ssh(SCP_HOST, SCP_PWD, SCP_DST, input_images, SSH_OPTS)
+)
-- 
GitLab