From fe456002e20bdbe68a065637c70c6992f8b3e5bb Mon Sep 17 00:00:00 2001
From: Yifan Zhao <yifanz16@illinois.edu>
Date: Sat, 23 Jan 2021 23:34:30 -0600
Subject: [PATCH] Put up framework for modeled tuning

---
 predtuner/approxapp.py  | 101 ++++++++++++++++++++++++----------------
 predtuner/modeledapp.py |  78 ++++++++++++++++++++++++++++---
 test/test_torchapp.py   |  33 +++++++++++++
 3 files changed, 167 insertions(+), 45 deletions(-)

diff --git a/predtuner/approxapp.py b/predtuner/approxapp.py
index cd24abc..fbc7eda 100644
--- a/predtuner/approxapp.py
+++ b/predtuner/approxapp.py
@@ -1,13 +1,13 @@
 import abc
 import logging
 from pathlib import Path
-from typing import Dict, Generic, List, NamedTuple, Optional, Tuple, TypeVar, Union
+from typing import Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union
 
 import matplotlib.pyplot as plt
 import numpy as np
 from opentuner.measurement.interface import MeasurementInterface
-from opentuner.resultsdb.models import Configuration, Result
-from opentuner.search.manipulator import ConfigurationManipulator, EnumParameter
+from opentuner.search.manipulator import (ConfigurationManipulator,
+                                          EnumParameter)
 
 from ._logging import override_opentuner_config
 from ._pareto import is_pareto_efficient
@@ -61,32 +61,33 @@ class ApproxApp(abc.ABC):
 
 class Config:
     def __init__(
-        self, qos: float, calib_qos: Optional[float], perf: float, knobs: KnobsT
+        self, qos: float, perf: float, knobs: KnobsT, calib_qos: Optional[float] = None
     ) -> None:
         self.qos = qos
-        self.calib_qos = calib_qos
         self.perf = perf
         self.knobs = knobs
+        self.calib_qos: Optional[float] = calib_qos
 
 
 T = TypeVar("T", bound=Config)
 
 
-# ApproxTuner is generic over the type of the config
+# IOpenTuner is generic over the type of the config
 # So that the user can use custom Config inherited from Config
 # (in which case they need to override `get_all_configs_from_db`).
 class ApproxTuner(Generic[T]):
     def __init__(self, app: ApproxApp) -> None:
         self.app = app
+        self._tuned = False
         self.all_configs = []
         self.kept_configs = []
         self.best_configs = []
+        # The following will be filled after self.tune() is called
         self.keep_threshold = None
-        self._db = None
 
     @property
     def tuned(self) -> bool:
-        return not self._db is None
+        return self._tuned
 
     def tune(
         self,
@@ -95,54 +96,44 @@ class ApproxTuner(Generic[T]):
         qos_keep_threshold: Optional[float] = None,
         is_threshold_relative: bool = False,
         take_best_n: Optional[int] = None,
-        calibrate: bool = True
+        calibrate: bool = True,
+        **kwargs
         # TODO: more parameters + opentuner param forwarding
-    ) -> List[Config]:
-        """Generate an optimal set of approximation configurations for the model."""
+    ) -> List[T]:
         from opentuner.tuningrunmain import TuningRunMain
 
         from ._dbloader import read_opentuner_db
 
-        # By default, keep_threshold == tuner_threshold
         opentuner_args = opentuner_default_args()
-        qos_keep_threshold = qos_keep_threshold or qos_tuner_threshold
-        if is_threshold_relative:
-            baseline_qos, _ = self.app.measure_qos_perf({}, False)
-            qos_tuner_threshold = baseline_qos - qos_tuner_threshold
-            qos_keep_threshold = baseline_qos - qos_keep_threshold
-        opentuner_args.test_limit = max_iter
-        tuner = TunerInterface(
-            opentuner_args, self.app, qos_tuner_threshold, qos_keep_threshold, max_iter,
+        tuner = self._get_tuner_interface(
+            opentuner_args,
+            max_iter,
+            qos_tuner_threshold,
+            qos_keep_threshold,
+            is_threshold_relative,
+            self._get_app_kwargs(**kwargs),
         )
+        assert self.keep_threshold is not None
         trm = TuningRunMain(tuner, opentuner_args)
         # TuningRunMain.__init__ initializes its own logger, so we'll override it and use ours
         override_opentuner_config()
         # This is where opentuner runs
         trm.main()
-
         # Parse and store results
-        self._db = opentuner_args.database
-        self.keep_threshold = qos_keep_threshold
-        self.all_configs = list(
-            self.get_all_configs_from_db(read_opentuner_db(self._db))
-        )
+        self._tuned = True
+        config_ty = self._get_config_class()
+        self.all_configs = [
+            config_ty(result.accuracy, result.time, configuration.data)
+            for result, configuration in read_opentuner_db(opentuner_args.database)
+        ]
         self.kept_configs = [
-            cfg for cfg in self.all_configs if cfg.qos > qos_keep_threshold
+            cfg for cfg in self.all_configs if cfg.qos > self.keep_threshold
         ]
         self.best_configs = self.take_best_configs(self.kept_configs, take_best_n)
         if calibrate:
             self.calibrate_configs_(self.best_configs)
         return self.best_configs
 
-    @classmethod
-    def get_all_configs_from_db(
-        cls, results_configs: List[Tuple[Result, Configuration]]
-    ) -> Tuple[T]:
-        return tuple(
-            Config(result.accuracy, None, result.time, configuration.data)
-            for result, configuration in results_configs
-        )
-
     def calibrate_configs_(self, configs: List[T]):
         from tqdm import tqdm
 
@@ -154,9 +145,7 @@ class ApproxTuner(Generic[T]):
             msg_logger.debug(f"Calibration: {cfg.qos} (mean) -> {cfg.calib_qos} (mean)")
 
     @staticmethod
-    def take_best_configs(
-        configs: List[Config], n: Optional[int] = None
-    ) -> List[Config]:
+    def take_best_configs(configs: List[T], n: Optional[int] = None) -> List[T]:
         points = np.array([[c.perf, c.qos] for c in configs])
         taken_idx = is_pareto_efficient(points, take_n=n)
         return [configs[i] for i in taken_idx]
@@ -193,6 +182,38 @@ class ApproxTuner(Generic[T]):
         ax.set_ylabel("speedup")
         return fig
 
+    def _get_tuner_interface(
+        self,
+        opentuner_args,
+        max_iter: int,
+        qos_tuner_threshold: float,
+        qos_keep_threshold: Optional[float],
+        is_threshold_relative: bool,
+        app_kwargs: dict,
+    ) -> "TunerInterface":
+        # By default, keep_threshold == tuner_threshold
+        self.keep_threshold = qos_keep_threshold or qos_tuner_threshold
+        if is_threshold_relative:
+            baseline_qos, _ = self.app.measure_qos_perf({}, False)
+            qos_tuner_threshold = baseline_qos - qos_tuner_threshold
+            self.keep_threshold = baseline_qos - self.keep_threshold
+        opentuner_args.test_limit = max_iter
+        return TunerInterface(
+            opentuner_args,
+            self.app,
+            qos_tuner_threshold,
+            self.keep_threshold,
+            max_iter,
+            **app_kwargs,
+        )
+
+    def _get_app_kwargs(self, **kwargs):
+        return {}
+
+    @classmethod
+    def _get_config_class(cls) -> Type[Config]:
+        return Config
+
 
 def opentuner_default_args():
     from opentuner import default_argparser
@@ -208,6 +229,7 @@ class TunerInterface(MeasurementInterface):
         tuner_thres: float,
         keep_thres: float,
         test_limit: int,
+        **app_kwargs,
     ):
         from opentuner.measurement.inputmanager import FixedInputManager
         from opentuner.search.objective import ThresholdAccuracyMinimizeTime
@@ -217,6 +239,7 @@ class TunerInterface(MeasurementInterface):
         self.tune_thres = tuner_thres
         self.keep_thres = keep_thres
         self.pbar = tqdm(total=test_limit, leave=False)
+        self.app_kwargs = app_kwargs
 
         objective = ThresholdAccuracyMinimizeTime(tuner_thres)
         input_manager = FixedInputManager(size=len(self.app.op_knobs))
diff --git a/predtuner/modeledapp.py b/predtuner/modeledapp.py
index 6745999..d5f3873 100644
--- a/predtuner/modeledapp.py
+++ b/predtuner/modeledapp.py
@@ -1,9 +1,12 @@
 import abc
-from typing import Callable, Dict, List, Tuple, Union
+import logging
+from typing import Callable, Dict, List, Optional, Tuple, Type, Union
 
 import torch
 
-from .approxapp import ApproxApp, KnobsT
+from .approxapp import ApproxApp, ApproxTuner, Config, KnobsT
+
+msg_logger = logging.getLogger(__name__)
 
 
 class ModeledApp(ApproxApp, abc.ABC):
@@ -65,7 +68,7 @@ class ModeledApp(ApproxApp, abc.ABC):
                     f'"{qos_model}" is an invalid value for qos_model '
                     f"(choose from {list(self._qos_models.keys())})"
                 )
-            qos = self._qos_models[qos_model].measure_qos(with_approxes, is_calibration)
+            qos = self._qos_models[qos_model].measure_qos(with_approxes)
         # Same goes for perf
         if perf_model != "none":
             if perf_model not in self._perf_models:
@@ -73,10 +76,13 @@ class ModeledApp(ApproxApp, abc.ABC):
                     f'"{perf_model}" is an invalid value for perf_model '
                     f"(choose from {list(self._perf_models.keys())})"
                 )
-            perf = self._perf_models[perf_model].measure_perf(with_approxes, is_calibration)
+            perf = self._perf_models[perf_model].measure_perf(with_approxes)
         assert qos is not None and perf is not None
         return qos, perf
 
+    def get_tuner(self) -> "ApproxModeledTuner":
+        return ApproxModeledTuner(self)
+
 
 class IPerfModel(abc.ABC):
     """Abstract base class for models that provide performance prediction."""
@@ -163,7 +169,7 @@ class QoSModelP1(IQoSModel):
 
     def measure_qos(self, with_approxes: KnobsT) -> float:
         """Implementation of model."""
-        pass
+        return 0.0
 
 
 class QoSModelP2(IQoSModel):
@@ -189,4 +195,64 @@ class QoSModelP2(IQoSModel):
 
     def measure_qos(self, with_approxes: KnobsT) -> float:
         """Implementation of model."""
-        pass
+        return 0.0
+
+
+class ValConfig(Config):
+    def __init__(
+        self,
+        qos: float,
+        perf: float,
+        knobs: KnobsT,
+        calib_qos: Optional[float] = None,
+        validated_qos: Optional[float] = None,
+    ) -> None:
+        super().__init__(qos, perf, knobs, calib_qos)
+        self.validated_qos = validated_qos
+
+
+class ApproxModeledTuner(ApproxTuner):
+    def tune(
+        self,
+        max_iter: int,
+        qos_tuner_threshold: float,
+        qos_keep_threshold: Optional[float] = None,
+        is_threshold_relative: bool = False,
+        take_best_n: Optional[int] = None,
+        calibrate: bool = True,
+        validate: Optional[bool] = None,
+        perf_model: str = "none",
+        qos_model: str = "none",
+    ) -> List[ValConfig]:
+        ret = super().tune(
+            max_iter=max_iter,
+            qos_tuner_threshold=qos_tuner_threshold,
+            qos_keep_threshold=qos_keep_threshold,
+            is_threshold_relative=is_threshold_relative,
+            take_best_n=take_best_n,
+            calibrate=calibrate,
+            perf_model=perf_model,
+            qos_model=qos_model,
+        )
+        if validate is None:
+            validate = qos_model != "none"
+        if validate:
+            self.validate_configs_(self.best_configs)
+        return ret
+
+    def validate_configs_(self, configs: List[ValConfig]):
+        from tqdm import tqdm
+
+        for cfg in tqdm(configs, leave=False):
+            cfg: ValConfig
+            if cfg.validated_qos is not None:
+                continue
+            cfg.validated_qos, _ = self.app.measure_qos_perf(cfg.knobs, False)
+            msg_logger.debug(f"Validation: {cfg.qos} (mean) -> {cfg.calib_qos} (mean)")
+
+    def _get_app_kwargs(self, perf_model: str, qos_model: str):
+        return {"perf_model": perf_model, "qos_model": qos_model}
+
+    @classmethod
+    def _get_config_class(cls) -> Type[Config]:
+        return ValConfig
diff --git a/test/test_torchapp.py b/test/test_torchapp.py
index d2752d7..98b941b 100644
--- a/test/test_torchapp.py
+++ b/test/test_torchapp.py
@@ -55,6 +55,12 @@ class TestTorchAppTuning(TorchAppSetUp):
         if len(tuner.kept_configs) >= 10:
             self.assertEqual(len(tuner.best_configs), 10)
 
+    def test_enum_models(self):
+        self.assertSetEqual(
+            set(model.name for model in self.app.get_models()),
+            {"perf_linear", "qos_p1", "qos_p2"},
+        )
+
 
 class TestTorchAppTunerResult(TorchAppSetUp):
     @classmethod
@@ -80,3 +86,30 @@ class TestTorchAppTunerResult(TorchAppSetUp):
         configs = self.tuner.best_configs
         for c in configs:
             self.assertAlmostEqual(c.calib_qos, c.qos)
+
+
+class TestModeledTuning(TorchAppSetUp):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.baseline, _ = cls.app.measure_qos_perf({}, False)
+
+    def test_qos_p1(self):
+        tuner = self.app.get_tuner()
+        tuner.tune(
+            100,
+            3.0,
+            is_threshold_relative=True,
+            perf_model="perf_linear",
+            qos_model="qos_p1",
+        )
+
+    def test_qos_p2(self):
+        tuner = self.app.get_tuner()
+        tuner.tune(
+            100,
+            3.0,
+            is_threshold_relative=True,
+            perf_model="perf_linear",
+            qos_model="qos_p2",
+        )
-- 
GitLab