diff --git a/hpvm/projects/hpvm-profiler/hpvm_profiler/__init__.py b/hpvm/projects/hpvm-profiler/hpvm_profiler/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e007ca9277f9e584708488ee57fd08c693a00279
--- /dev/null
+++ b/hpvm/projects/hpvm-profiler/hpvm_profiler/__init__.py
@@ -0,0 +1,175 @@
+from pathlib import Path
+from typing import Iterable, List, Tuple, Union
+from dataclasses import dataclass
+
+PathLike = Union[Path, str]
+
+
+def profile_configs(
+    binary_path: PathLike,
+    config_path: PathLike,
+    output_config_path: PathLike,
+    profile_filename: str = "profile_info.txt",
+    qos_filename: str = "final_accuracy",
+):
+    """
+    Profile an HPVM configuration file with an HPVM binary.
+    The configuration file must have the baseline as the first configuration.
+
+    binary_path: Union[Path, str]
+        Path to binary to be executed in profiling.
+    config_path: Union[Path, str]
+        Path to config file (HPVM configuration format)
+        with configs to enumerate for profiling.
+    output_config_path: Union[Path, str]
+        Path where the output configs are written.
+        The output config file has the same configs as the input `config_path` file,
+        but the performance and energy readings are updated.
+    profile_filename: str
+        Name of profile file generated by the binary (in current directory).
+        This defaults to "profile_info.txt" and should not be changed for HPVM binaries.
+    qos_filename: str
+        Name of QoS file generated by the binary (in current directory).
+        It contains a single float number as the QoS of this run.
+        This defaults to "final_accuracy" and should not be changed for HPVM binaries.
+    """
+
+    from subprocess import check_call
+    from tempfile import NamedTemporaryFile
+
+    # Read first line ("the float") and configs in config file
+    header, configs = read_hpvm_configs(Path(config_path))
+    if not configs:
+        raise ValueError("Config file with no configs is unsupported.")
+    temp_file = NamedTemporaryFile("w")
+    baseline_time, baseline_acc = None, None
+    for idx, config in enumerate(configs):
+        # Write config to temp config file
+        write_hpvm_config(header, [config], Path(temp_file.name))
+        # Run binary_path binary,
+        # which generates `profile_filename` and `qos_filename` file in cwd.
+        check_call(str(binary_path))
+        # Read these two files for time and QoS info.
+        time = _read_profile_file(Path(profile_filename))
+        acc = _read_qos_file(Path(qos_filename))
+        if idx == 0:
+            baseline_time, baseline_acc = time, acc
+            continue
+        assert baseline_time is not None and baseline_acc is not None
+        speedup = baseline_time / time
+        config.update_profile_results(speedup, acc, baseline_acc)
+    write_hpvm_config(header, configs, Path(output_config_path))
+    temp_file.close()
+
+
+def plot_hpvm_configs(
+    config_path: PathLike,
+    save_to: PathLike = None,
+    show_qos_loss: bool = True,
+    **fig_kwargs,
+):
+    """
+    Plot the QoS-speedup information in an HPVM configuration file.
+    It is recommended to profile the config file first (using `profile_configs`)
+    to obtain real speedup numbers.
+    This function creates a `matplotlib.pyplot.Figure`, plots on it, and returns it.
+
+    config_path: Union[Path, str]
+        Path to the config file (HPVM configuration format).
+    save_to: Union[Path, str]
+        File to save figure into. Default is None: don't save figure (just return it).
+    show_qos_loss: bool
+        Show the loss of QoS on x axis of the figure. Defaults to True.
+        If False, will use (absolute) QoS instead of QoS loss.
+    fig_kwargs:
+        Arguments to pass to `plt.subplots`.
+    """
+
+    import numpy as np
+    import matplotlib.pyplot as plt
+
+    _, configs = read_hpvm_configs(config_path)
+    get_qos = lambda c: c.qos_loss if show_qos_loss else c.qos
+    qos_speedup = np.array([(get_qos(c), c.speedup) for c in configs])
+    qoses, speedups = qos_speedup.T
+    fig, ax = plt.subplots(**fig_kwargs)
+    ax.scatter(qoses, speedups)
+    ax.xlabel("QoS Loss")
+    ax.ylabel("Speedup (X)")
+    if save_to:
+        fig.savefig(save_to, dpi=300)
+    return fig
+
+@dataclass
+class Config:
+    conf_name: str
+    speedup: float
+    energy: float
+    qos: float
+    qos_loss: float
+    # We don't care about the information in this part, and we don't parse this.
+    config_body: List[str]
+
+    def update_profile_results(self, speedup: float, qos: float, base_qos: float):
+        recorded_base_qos = self.qos + self.qos_loss
+        if abs(recorded_base_qos - base_qos) > 1e-3:
+            raise ValueError(
+                f"Baseline QoS mismatch. Original: {recorded_base_qos}, measured: {base_qos}"
+            )
+        self.speedup = speedup
+        self.qos = qos
+        self.qos_loss = base_qos - qos
+
+    def __repr__(self) -> str:
+        header_fields = [
+            self.conf_name,
+            self.speedup,
+            self.energy,
+            self.qos,
+            self.qos_loss,
+        ]
+        header = " ".join(str(field) for field in header_fields)
+        return f"{header}\n{self.config_body}"
+
+    __str__ = __repr__
+
+
+def read_hpvm_configs(config_file: PathLike) -> Tuple[str, List[Config]]:
+    # def read_hpvm_configs(config_file, config_num, temp_file):
+    ret_configs = []
+    with open(config_file) as f:
+        text = f.read()
+    opening, closing = "+++++", "-----"
+    # There's 1 float sitting on the first line of config file.
+    # We don't use it, but want to keep that intact.
+    header, *configs = text.split(opening)
+    header = header.strip()
+    for config_text in configs:
+        config_text = config_text.replace(closing, "").strip()
+        config_header, *config_body = config_text.splitlines()
+        conf_name, *number_fields = config_header.split(" ")
+        speedup, energy, qos, qos_drop = [float(s) for s in number_fields]
+        ret_configs.append(
+            Config(conf_name, speedup, energy, qos, qos_drop, config_body)
+        )
+    return header, ret_configs
+
+
+def write_hpvm_config(header: str, configs: Iterable[Config], to_file: PathLike):
+    text_segs = [header] + [str(config) for config in configs]
+    with open(to_file, "w") as f:
+        f.write("\n".join(text_segs))
+
+
+def _read_profile_file(profile_file_path: Path):
+    with profile_file_path.open() as f:
+        target_lines = [line.strip() for line in f if "Total Time" in line]
+    if len(target_lines) != 1:
+        raise RuntimeError(f"Profile {profile_file_path} malformed")
+    (target_line,) = target_lines
+    return float(target_line.split()[3])
+
+
+def _read_qos_file(qos_file_path: Path):
+    with qos_file_path.open() as f:
+        return float(f.read().strip())
diff --git a/hpvm/projects/hpvm-profiler/setup.py b/hpvm/projects/hpvm-profiler/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..f7b3771a2ee3a67569092d9b956e67c309f9d08e
--- /dev/null
+++ b/hpvm/projects/hpvm-profiler/setup.py
@@ -0,0 +1,11 @@
+import setuptools
+
+setuptools.setup(
+    name="hpvm_profiler",
+    version="0.1",
+    author="Akash Kothari, Yifan Zhao",
+    author_email="akashk4@illinois.edu, yifanz16@illinois.edu",
+    description="A package for profiling of HPVM approximation configurations",
+    packages=["hpvm_profiler"],
+    install_requires=["numpy>=1.19", "matplotlib>=3"],
+)
diff --git a/hpvm/scripts/hpvm_installer.py b/hpvm/scripts/hpvm_installer.py
index cd6ef3bae08b0cd6c912a40b6a45e3ca37257c99..e83d5f3e727044dcba942e2533597bc779c9b816 100755
--- a/hpvm/scripts/hpvm_installer.py
+++ b/hpvm/scripts/hpvm_installer.py
@@ -35,6 +35,7 @@ MAKE_TEST_TARGETS = ["check-hpvm-dnn", "check-hpvm-pass"]
 
 # Relative to project root which is __file__.parent.parent
 PY_PACKAGES = [
+    "projects/hpvm-profiler",
     "projects/predtuner",
     "projects/torch2hpvm",
     "projects/keras",
diff --git a/hpvm/test/dnn_benchmarks/hpvm-c/scripts/run_dnn.py b/hpvm/test/dnn_benchmarks/hpvm-c/scripts/run_dnn.py
deleted file mode 100644
index 10eaf5699f5549d449fab12bd8a5ed9e8a5d2d6e..0000000000000000000000000000000000000000
--- a/hpvm/test/dnn_benchmarks/hpvm-c/scripts/run_dnn.py
+++ /dev/null
@@ -1,210 +0,0 @@
-import os.path
-from os import path
-import sys
-import matplotlib.pyplot as plt 
-
-
-binary_dir = "../../../build/tools/hpvm/test/dnn_benchmarks/"
-tradeoff_curves_dir = "../../../docs/tradeoff-curves"
-
-
-accuracy_file = "final_accuracy"
-profile_file = "profile_data.txt"
-profile_file_prefix = "profile_info_"
-
-temp_file_name = "temp.txt"
-pred_binary_prefix = ""
-pred_binary_suffix = "" 
-
-rt_binary_suffix = "_rt_pred"
-max_num_runs = 20
-
-
-def max_num_configs (config_file):
-    num_configs = 0
-    with open(config_file, "r") as f:
-        for line in f:
-            if "conf" in line:
-                num_configs = num_configs + 1
-    return (num_configs + 1)
-
-
-def read_and_write_config (config_file, config_num, temp_file):
-    config = ""
-    print("--CONFIG FILE: " + config_file)
-    print("--CONFIG NUM: " + str(config_num))
-    print("--TEMP FILE: " + temp_file)
-    with open(config_file, "r") as f:
-        conf = "conf" + str(config_num)
-        read_config = False
-        read_first_line = False
-        for line in f:
-            if read_first_line == False:
-                config = config + line
-                read_first_line = True
-                continue
-            if "-----" in line and read_config == True:
-                read_config = False
-                config = config + line
-                break
-            if read_config == True:
-                config = config + line
-                continue
-            if conf in line:
-                read_config = True
-                config = config + "+++++\n"
-                config = config + line
-    print("config: ")
-    print(config)
-    with open(temp_file, "w") as f:
-        f.write(config)
-
-
-def get_avg_exec_time(profile_file_path, config_num):
-    prof_file = profile_file_path + profile_file_prefix + str(config_num) + ".txt"
-    print("PROFILE FILE: " + prof_file)
-    with open(prof_file, "r") as f:
-            for line in f:
-                if "Total Time" in line:
-                    print("LINE: " + line)
-                    time = line.strip().split() [3]
-                    print("TIME: " + time)
-                    return float(time)
-    print("ERROR")
-    sys.exit()
-    return float(-1)
-
-def get_exec_time(config_file):
-    print("CONFIG FILE: " + config_file)
-    with open(config_file, "r") as f:
-        for line in f:
-            if "conf" in line:
-                print("LINE: " + line)
-                time = line.strip().split() [1]
-                print("TIME: " + time)
-                return float(time)
-    print("ERROR")
-    sys.exit()
-    return float(-1)
-
-def get_avg_exec_accuracy(file_name):
-    with open(file_name, "r") as f:
-        for line in f:
-            accuracy = line.strip().split() [0]
-            print("ACCURACY: " + accuracy)
-            return float(accuracy)
-    print("ERROR")
-    sys.exit()
-    return float(-1)
-
-def get_exec_accuracy(config_file):
-    with open(config_file, "r") as f:
-        for line in f:
-            if "conf" in line:
-                print("LINE: " + line)
-                acc = line.strip().split() [4]
-                print("ACCURACY: " + acc)
-                return float(acc)
-    print("ERROR")
-    sys.exit()
-    return float(-1)
-
-def predictive_tuning_exec(dnn_name):
-    dnn_dir = "../benchmarks/" + dnn_name
-    binary_name = binary_dir + pred_binary_prefix + dnn_name + pred_binary_suffix
-    pred_dir = dnn_dir + "/predictive/"
-    config_file = pred_dir + dnn_name + ".txt"
-    temp_file = pred_dir + temp_file_name
-    print("dnn_dir: " + dnn_dir)
-    print("binary name: " + binary_name)
-    print("pred_dir: " + pred_dir)
-    print("config_file: " + config_file)
-    print("temp_file: " + temp_file)
-    exec_command = "rm " + temp_file + " " + accuracy_file + " " + profile_file + " " + pred_dir + "profile*"
-    print(exec_command)
-    os.system(exec_command)
-    config_num = 1
-    max_configs = max_num_configs(config_file)
-    baseline_time = 0
-    baseline_acc = 0
-    print("MAX CONFIGS: " + str(max_configs))
-    perf_list = list()
-    acc_list = list()
-    while config_num < max_configs:
-        read_and_write_config(config_file, config_num, temp_file)
-        exec_command = binary_name
-        print(exec_command) 
-        os.system(exec_command)
-        time = get_avg_exec_time(pred_dir, config_num - 1)
-        acc = get_avg_exec_accuracy(accuracy_file)
-        config_time = get_exec_time(temp_file)
-        config_acc = get_exec_accuracy(temp_file)
-        if config_num == 1:
-            baseline_time = time
-            baseline_acc = acc 
-        else:
-            print("SPEEDUP: ")
-            print(baseline_time/time)
-            perf_list.append(baseline_time/time)
-            print("CONFIG TIME: ")
-            print(config_time)
-            print("ACC LOSS: ")
-            print(baseline_acc - acc)
-            acc_list.append(baseline_acc - acc)
-            print("CONFIG ACC: ")
-            print(config_acc)
-        config_num = config_num + 1
-    exec_command = "rm " + temp_file + " " + accuracy_file + " " + profile_file + " " + pred_dir + "profile*"
-    print(exec_command)
-    os.system(exec_command)
-    plt.scatter(acc_list, perf_list)
-    plt.ylabel("Speedup (X)")
-    plt.xlabel("Accurancy loss (%)")
-    xticks = ['-1', '0', '1', '2', '3', '4']
-    yticks = ['1', '1.5', '2', '2.5', '3']
-    plt.xlim(-1, 4)
-    plt.ylim(1, 3)
-    plt.title(dnn_name)
-    plt.savefig(tradeoff_curves_dir + dnn_name + "_tradeoff.pdf")
-    perf_list.clear()
-    acc_list.clear()
-
-
-def runtime_tuning_exec():
-    num_args = len(sys.argv)
-    binary_files = list()
-    arg = 2
-    while arg < num_args:
-        binary_files.append(sys.argv[arg])
-        arg = arg + 1
-
-    for dnn_name in binary_files:
-        binary_dir = "../benchmarks/" + dnn_name
-        binary_name = binary_dir + rt_binary_suffix
-        conf_dir = binary_dir + "/data"
-        print("binary_dir: " + binary_dir)
-        print("binary name: " + binary_name)
-        run = 0
-        while run < max_num_runs:
-            exec_command = binary_name
-            print(exec_command)
-            os.system(exec_command)
-            exec_command = "/home/nvidia/poll 13"
-            print(exec_command)
-            os.system(exec_command)
-            exec_command = "mv " + conf_dir + "/profile_info_0.txt " + conf_dir + "/profile_info_out-run-" + str(run) + ".txt"
-            print(exec_command)
-            os.system(exec_command)
-            run = run + 1
-        exec_command = "rm -rf " + conf_dir +  "/run_data"
-        print(exec_command)
-        os.system(exec_command)
-        exec_command = "mkdir " + conf_dir + "/run_data"  
-        print(exec_command)
-        os.system(exec_command)  
-            
-
-
-if __name__ == "__main__":
-    predictive_tuning_exec(sys.argv[1])
-
diff --git a/hpvm/test/dnn_benchmarks/hpvm-c/scripts/run_dnns.py b/hpvm/test/dnn_benchmarks/hpvm-c/scripts/run_dnns.py
deleted file mode 100644
index 4c9ab6ec12741d05d5af6b414a90ad26c5d1bfe0..0000000000000000000000000000000000000000
--- a/hpvm/test/dnn_benchmarks/hpvm-c/scripts/run_dnns.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import os
-import sys
-
-dnns = ["alexnet", "alexnet2", "vgg16_cifar10", "vgg16_cifar100", "resnet18", "mobilenet_cifar10", "alexnet_imagenet", "resnet50_imagenet", "vgg16_imagenet", "lenet_mnist"]
-
-
-if __name__ == "__main__":
-    for dnn in dnns:
-        exec_command = "python3 run_dnn.py " + dnn 
-        print(exec_command)
-        os.system(exec_command)
-
diff --git a/hpvm/test/dnn_benchmarks/hpvm-c/scripts/README.md b/hpvm/test/dnn_benchmarks/profiling/README.md
similarity index 100%
rename from hpvm/test/dnn_benchmarks/hpvm-c/scripts/README.md
rename to hpvm/test/dnn_benchmarks/profiling/README.md
diff --git a/hpvm/test/dnn_benchmarks/hpvm-c/scripts/jetson_clocks.sh b/hpvm/test/dnn_benchmarks/profiling/jetson_clocks.sh
similarity index 100%
rename from hpvm/test/dnn_benchmarks/hpvm-c/scripts/jetson_clocks.sh
rename to hpvm/test/dnn_benchmarks/profiling/jetson_clocks.sh