diff --git a/hpvm/test/epoch_dnn/assets/miniera/calib.txt b/hpvm/test/epoch_dnn/assets/miniera/calib.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4fff404f7e9b7beb3ef3dfc19bf1648c68dbe7be
--- /dev/null
+++ b/hpvm/test/epoch_dnn/assets/miniera/calib.txt
@@ -0,0 +1,21 @@
+input:	0.007874015748031496 
+conv1:	0.041152522047244094
+add1:	0.041152522047244094
+relu1:	0.03673215196850394
+conv2:	0.18990387401574804
+add2:	0.18990387401574804
+relu2:	0.12851757480314963
+pool1:	0.12851757480314963
+conv3:	0.4379456692913386
+add3:	0.4379456692913386
+relu3:	0.13566622047244095
+conv4:	0.3754296456692913
+add4:	0.3754296456692913
+relu4:	0.15238466141732282
+pool2:	0.15238466141732282
+gemm1:	0.27306629921259845
+add5:	0.27306629921259845
+relu5:	0.20020370078740157
+gemm2:	0.6007037007874017
+add6:	0.6007037007874017
+softmax1:	0.007874015748031496
diff --git a/hpvm/test/epoch_dnn/assets/miniera/conv2d_1_b.bin b/hpvm/test/epoch_dnn/assets/miniera/conv2d_1_b.bin
new file mode 100644
index 0000000000000000000000000000000000000000..39c3fbac7f94a6824736f8b21f184b71b3d45a7b
--- /dev/null
+++ b/hpvm/test/epoch_dnn/assets/miniera/conv2d_1_b.bin
@@ -0,0 +1,2 @@
+αÝ>aŸ¾Ì?N?„œQ¿JÙ%½t‰¼ªl©=™&¼½œ ¿¿^L8?د>r¾:õö¾
νóù¼š¶?B Y?–;Uì>ç—=€ëh?rXö½ï
+‹=&ç½Ýˆ™½C#S>¥”½7ü¹>vÉ…>ɇ¿!?
\ No newline at end of file
diff --git a/hpvm/test/epoch_dnn/assets/miniera/conv2d_1_w.bin b/hpvm/test/epoch_dnn/assets/miniera/conv2d_1_w.bin
new file mode 100644
index 0000000000000000000000000000000000000000..d01508286ed5fddf05790e261efa168847699efd
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/conv2d_1_w.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/conv2d_2_b.bin b/hpvm/test/epoch_dnn/assets/miniera/conv2d_2_b.bin
new file mode 100644
index 0000000000000000000000000000000000000000..39489675632774a46e0ea704d7d13807b2e4feb5
--- /dev/null
+++ b/hpvm/test/epoch_dnn/assets/miniera/conv2d_2_b.bin
@@ -0,0 +1,2 @@
+„§Ÿ½æ.î=•·?¾Š¥¿vƒ¿ºS¿Ó놻Þ >
+Qżøæ¼å—8¿ÂVä».I>Æp𼄃;dd=h䈾Ðé¾N½.¾ÓñÍ=/Ú¾ŒÖl¾×;¾ð4¾6ƒ>cTʾR¶	¼ê¿¾ô2Í=c_¨¾­¾ÚZ¾
\ No newline at end of file
diff --git a/hpvm/test/epoch_dnn/assets/miniera/conv2d_2_w.bin b/hpvm/test/epoch_dnn/assets/miniera/conv2d_2_w.bin
new file mode 100644
index 0000000000000000000000000000000000000000..381b72379b85614a79910c9560c6115310da538a
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/conv2d_2_w.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/conv2d_3_b.bin b/hpvm/test/epoch_dnn/assets/miniera/conv2d_3_b.bin
new file mode 100644
index 0000000000000000000000000000000000000000..43fe41a6edcae03a1a531123940e528a71807300
--- /dev/null
+++ b/hpvm/test/epoch_dnn/assets/miniera/conv2d_3_b.bin
@@ -0,0 +1 @@
+˜Ò½Ê+¥½ò?Š$ù¾méÊ>(>¼½hŠ >qÂB½y²*½‚ì>IÒ¸>»Kˆ?@ ¨¼t\¢?æH¾
•=ùÔý>…œ;½_å—>Ÿfœ=;┿®Œû>›jÞ¾DâÓ»×Á‰¾šU>·`†? Éc>ÈŽ?Õª?Ÿ·/<#&?—?ôðš¾Dy<Hbf¾lò:?ÑwS¾M
)>}¾«t ¾Ÿt'?ö¥ú¿´$¾<¥!æ½ œ?Ë¡½c’‹<Za¾>E5“>0Jê>p9J¾†žÁ>ÞWð>nèr>e–'?¹+R¿Gž>kæò;·bÆ¿²àR>˜{ÿ>ãš½
\ No newline at end of file
diff --git a/hpvm/test/epoch_dnn/assets/miniera/conv2d_3_w.bin b/hpvm/test/epoch_dnn/assets/miniera/conv2d_3_w.bin
new file mode 100644
index 0000000000000000000000000000000000000000..a82a9e397918217bf37873c59dd92bee713fa9df
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/conv2d_3_w.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/conv2d_4_b.bin b/hpvm/test/epoch_dnn/assets/miniera/conv2d_4_b.bin
new file mode 100644
index 0000000000000000000000000000000000000000..bd29ee60df1f0f0b3a199f1f65adb810a2649a3c
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/conv2d_4_b.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/conv2d_4_w.bin b/hpvm/test/epoch_dnn/assets/miniera/conv2d_4_w.bin
new file mode 100644
index 0000000000000000000000000000000000000000..7f6211e76617ee18dac06c9b5449c18183a149de
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/conv2d_4_w.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/dense_1_b.bin b/hpvm/test/epoch_dnn/assets/miniera/dense_1_b.bin
new file mode 100644
index 0000000000000000000000000000000000000000..fae8736fa3da691229c66e73962cb4f0131c2961
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/dense_1_b.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/dense_1_w.bin b/hpvm/test/epoch_dnn/assets/miniera/dense_1_w.bin
new file mode 100644
index 0000000000000000000000000000000000000000..034b84905ae34893f5b57be77033fd91b388a80b
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/dense_1_w.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/dense_2_b.bin b/hpvm/test/epoch_dnn/assets/miniera/dense_2_b.bin
new file mode 100644
index 0000000000000000000000000000000000000000..2bb9d3e89f7d35a4e66063ec93baab23ef90b86e
--- /dev/null
+++ b/hpvm/test/epoch_dnn/assets/miniera/dense_2_b.bin
@@ -0,0 +1 @@
+k"õ;(¿¾òë>™¿JÄ@?
\ No newline at end of file
diff --git a/hpvm/test/epoch_dnn/assets/miniera/dense_2_w.bin b/hpvm/test/epoch_dnn/assets/miniera/dense_2_w.bin
new file mode 100644
index 0000000000000000000000000000000000000000..f3bce42e01c37928d0cd54493835bcda70529bf2
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/dense_2_w.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/input.bin b/hpvm/test/epoch_dnn/assets/miniera/input.bin
new file mode 100644
index 0000000000000000000000000000000000000000..0abae55bf84ff5dc8e2d1074c97853331fc5d879
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/input.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/labels.bin b/hpvm/test/epoch_dnn/assets/miniera/labels.bin
new file mode 100644
index 0000000000000000000000000000000000000000..effaef8583b30228039ff7f61d9c6be51c020b49
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/labels.bin differ
diff --git a/hpvm/test/epoch_dnn/assets/miniera/miniera.pth b/hpvm/test/epoch_dnn/assets/miniera/miniera.pth
new file mode 100644
index 0000000000000000000000000000000000000000..f264a4ea67f88c361923e7c7f0208703001a622d
Binary files /dev/null and b/hpvm/test/epoch_dnn/assets/miniera/miniera.pth differ
diff --git a/hpvm/test/epoch_dnn/main.py b/hpvm/test/epoch_dnn/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f9e5795d323359a5a798abb0aa985bc8c91e224
--- /dev/null
+++ b/hpvm/test/epoch_dnn/main.py
@@ -0,0 +1,18 @@
+import site
+from pathlib import Path
+
+import torch
+from torch2hpvm import BinDataset, ModelExporter
+
+self_folder = Path(__file__).parent.absolute()
+site.addsitedir(self_folder)
+
+from torch_dnn import MiniERA
+
+asset_dir = self_folder / "assets/miniera"
+output_dir = Path("/tmp/miniera")
+bin_dataset = BinDataset(asset_dir / "input.bin", asset_dir / "labels.bin", (5000, 3, 32, 32))
+model = MiniERA()
+model.load_state_dict(torch.load(asset_dir / "miniera.pth"))
+exporter = ModelExporter(model, bin_dataset, output_dir, asset_dir / "calib.txt")
+exporter.generate().compile(output_dir / "miniera", output_dir)
diff --git a/hpvm/test/epoch_dnn/make_ckpt.py b/hpvm/test/epoch_dnn/make_ckpt.py
new file mode 100644
index 0000000000000000000000000000000000000000..eda4c2c02a63cd181e1a1a8552865ba7b72d26d2
--- /dev/null
+++ b/hpvm/test/epoch_dnn/make_ckpt.py
@@ -0,0 +1,33 @@
+"""Make PyTorch checkpoint of MiniERA model from legacy HPVM weights."""
+import site
+from pathlib import Path
+
+import torch
+
+self_folder = Path(__file__).parent.absolute()
+site.addsitedir(self_folder)
+
+from torch_dnn import CIFAR, MiniERA
+
+
+@torch.no_grad()
+def main():
+    prefix = self_folder / "assets/miniera"
+    model = MiniERA().load_legacy_hpvm_weights(prefix)
+    # Test mini ERA
+    dataset = CIFAR.from_file(prefix / "input.bin", prefix / "labels.bin")
+    dataloader = torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False)
+    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()
+    print(f"Accuracy of the network on the test images: {100 * correct / total} %")
+    torch.save(model.state_dict(), prefix / "miniera.pth")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hpvm/test/epoch_dnn/torch_dnn/__init__.py b/hpvm/test/epoch_dnn/torch_dnn/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..62f29d0d7e715cceb1679f3dde21bc11e22dc131
--- /dev/null
+++ b/hpvm/test/epoch_dnn/torch_dnn/__init__.py
@@ -0,0 +1,2 @@
+from .datasets import CIFAR
+from .miniera import MiniERA
diff --git a/hpvm/test/epoch_dnn/torch_dnn/datasets.py b/hpvm/test/epoch_dnn/torch_dnn/datasets.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac519bfe26ba48429cf40d6f16fdc3587e606b37
--- /dev/null
+++ b/hpvm/test/epoch_dnn/torch_dnn/datasets.py
@@ -0,0 +1,99 @@
+import logging
+from pathlib import Path
+from typing import Iterator, Tuple, Union
+
+import numpy as np
+import torch
+from torch.utils.data.dataset import Dataset
+
+RetT = Tuple[torch.Tensor, torch.Tensor]
+msg_logger = logging.getLogger(__name__)
+
+PathLike = Union[Path, str]
+
+
+class SingleFileDataset(Dataset):
+    def __init__(self, inputs: torch.Tensor, outputs: torch.Tensor):
+        self.inputs, self.outputs = inputs, outputs
+
+    @classmethod
+    def from_file(cls, *args, **kwargs):
+        pass
+
+    @property
+    def sample_input(self):
+        inputs, outputs = next(iter(self))
+        return inputs
+
+    def __len__(self) -> int:
+        return len(self.inputs)
+
+    def __getitem__(self, idx) -> RetT:
+        return self.inputs[idx], self.outputs[idx]
+
+    def __iter__(self) -> Iterator[RetT]:
+        for i in range(len(self)):
+            yield self[i]
+
+
+class DNNDataset(SingleFileDataset):
+    image_shape = None
+    label_ty = np.int32
+
+    @classmethod
+    def from_file(
+        cls,
+        input_file: PathLike,
+        labels_file: PathLike,
+        count: int = -1,
+        offset: int = 0,
+    ):
+        # NOTE: assuming (N, *) ordering of inputs (such as NCHW, NHWC)
+        channel_size = np.prod(np.array(cls.image_shape))
+        inputs_count_byte = -1 if count == -1 else count * channel_size
+        inputs = read_tensor_from_file(
+            input_file,
+            -1,
+            *cls.image_shape,
+            count=inputs_count_byte,
+            offset=offset * channel_size,
+        )
+        labels = read_tensor_from_file(
+            labels_file,
+            -1,
+            read_ty=cls.label_ty,
+            cast_ty=np.long,
+            count=count,
+            offset=offset,
+        )
+        if inputs.shape[0] != labels.shape[0]:
+            raise ValueError("Input and output have different number of data points")
+        msg_logger.info(f"%d entries loaded from dataset.", inputs.shape[0])
+        return cls(inputs, labels)
+
+
+class MNIST(DNNDataset):
+    image_shape = 1, 28, 28
+
+
+class CIFAR(DNNDataset):
+    image_shape = 3, 32, 32
+
+
+class ImageNet(DNNDataset):
+    image_shape = 3, 224, 224
+
+
+def read_tensor_from_file(
+    filename: Union[str, Path],
+    *shape: int,
+    read_ty=np.float32,
+    cast_ty=np.float32,
+    count: int = -1,
+    offset: int = 0,
+) -> torch.Tensor:
+    offset = offset * read_ty().itemsize
+    mmap = np.memmap(filename, dtype=read_ty, mode="r", offset=offset)
+    n_entries = min(mmap.shape[0], count) if count != -1 else mmap.shape[0]
+    np_array = mmap[:n_entries].reshape(shape).astype(cast_ty)
+    return torch.from_numpy(np_array).clone()
diff --git a/hpvm/test/epoch_dnn/torch_dnn/miniera.py b/hpvm/test/epoch_dnn/torch_dnn/miniera.py
new file mode 100644
index 0000000000000000000000000000000000000000..84c88ac8f882b4b6d1ba98cdb3a88f92cf418075
--- /dev/null
+++ b/hpvm/test/epoch_dnn/torch_dnn/miniera.py
@@ -0,0 +1,55 @@
+from pathlib import Path
+from typing import Union
+
+import numpy as np
+import torch
+from torch.nn import Conv2d, Linear, MaxPool2d, Module, ReLU, Sequential, Softmax
+
+
+class MiniERA(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.fcs = Sequential(Linear(1600, 256), ReLU(), Linear(256, 5))
+        self.softmax = Softmax(1)
+
+    def forward(self, input):
+        outputs = self.convs(input)
+        outputs = self.fcs(outputs.flatten(1, -1))
+        return self.softmax(outputs)
+
+    def load_legacy_hpvm_weights(self, prefix: Union[Path, str]):
+        prefix = Path(prefix)
+        # Load in model convolution weights
+        count = 0
+        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)
+            conv.weight.data = torch.tensor(weight_np).reshape(conv.weight.shape)
+            conv.bias.data = torch.tensor(bias_np).reshape(conv.bias.shape)
+            count += 1
+        # Load in model fc weights
+        count = 0
+        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)
+            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)
+            count += 1
+        return self