From add87927e161b72c09a073fe0d89372e39b520ea Mon Sep 17 00:00:00 2001 From: Yifan Zhao <yifanz16@illinois.edu> Date: Sun, 24 Jan 2021 01:40:53 -0600 Subject: [PATCH] Implemented model P2 --- predtuner/approxapp.py | 12 ++- predtuner/modeledapp.py | 162 ++++++++++++++++++++++++++++++++++++++-- predtuner/torchapp.py | 5 +- test/__init__.py | 0 4 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 test/__init__.py diff --git a/predtuner/approxapp.py b/predtuner/approxapp.py index c7e9415..2d74068 100644 --- a/predtuner/approxapp.py +++ b/predtuner/approxapp.py @@ -6,8 +6,7 @@ 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.search.manipulator import (ConfigurationManipulator, - EnumParameter) +from opentuner.search.manipulator import ConfigurationManipulator, EnumParameter from ._logging import override_opentuner_config from ._pareto import is_pareto_efficient @@ -58,6 +57,15 @@ class ApproxApp(abc.ABC): the user should try to make it unique.""" return "" + @property + def ops(self) -> List[str]: + return list(self.op_knobs) + + @property + def knobs(self) -> List[ApproxKnob]: + knob_sets = [set(knobs) for knobs in self.op_knobs.values()] + return list(set.union(*knob_sets)) + class Config: def __init__( diff --git a/predtuner/modeledapp.py b/predtuner/modeledapp.py index d5f3873..618e2c9 100644 --- a/predtuner/modeledapp.py +++ b/predtuner/modeledapp.py @@ -1,12 +1,18 @@ import abc +import json import logging -from typing import Callable, Dict, List, Optional, Tuple, Type, Union +import pickle +from pathlib import Path +from typing import Callable, Dict, Iterator, List, Optional, Tuple, Type, Union +import numpy as np +import pandas as pd import torch -from .approxapp import ApproxApp, ApproxTuner, Config, KnobsT +from .approxapp import ApproxApp, ApproxKnob, ApproxTuner, Config, KnobsT msg_logger = logging.getLogger(__name__) +PathLike = Union[Path, str] class ModeledApp(ApproxApp, abc.ABC): @@ -20,6 +26,9 @@ class ModeledApp(ApproxApp, abc.ABC): def __init__(self) -> None: super().__init__() models = self.get_models() + self._name_to_model = {m.name: m for m in models} + if len(self._name_to_model) != len(models): + raise ValueError("Name conflict in models") self._perf_models = { model.name: model for model in models if isinstance(model, IPerfModel) } @@ -83,10 +92,16 @@ class ModeledApp(ApproxApp, abc.ABC): def get_tuner(self) -> "ApproxModeledTuner": return ApproxModeledTuner(self) + def _init_model(self, model_name: str): + self._name_to_model[model_name]._init() + class IPerfModel(abc.ABC): """Abstract base class for models that provide performance prediction.""" + def __init__(self) -> None: + self._inited = False + @property @abc.abstractmethod def name(self) -> str: @@ -95,13 +110,20 @@ class IPerfModel(abc.ABC): @abc.abstractmethod def measure_perf(self, with_approxes: KnobsT) -> float: - """We implement this using a weighted linear performance model.""" + """Predict the performance of application.""" pass + def _init(self): + """Initialize the model before the first prediction task (profiling, etc.)""" + self._inited = True + class IQoSModel(abc.ABC): """Abstract base class for models that provide QoS prediction.""" + def __init__(self) -> None: + self._inited = False + @property @abc.abstractmethod def name(self) -> str: @@ -110,9 +132,13 @@ class IQoSModel(abc.ABC): @abc.abstractmethod def measure_qos(self, with_approxes: KnobsT) -> float: - """We implement this using a weighted linear performance model.""" + """Predict the qos of application.""" pass + def _init(self): + """Initialize the model before the first prediction task (profiling, etc.)""" + self._inited = True + class LinearPerfModel(IPerfModel): """Weighted linear performance predictor based on cost of each operator.""" @@ -156,12 +182,20 @@ class QoSModelP1(IQoSModel): def __init__( self, + app: ModeledApp, tensor_output_getter: Callable[[KnobsT], torch.Tensor], qos_metric: Callable[[torch.Tensor], float], + storage: PathLike = None, ) -> None: super().__init__() + self.app = app self.output_f = tensor_output_getter self.qos_metric = qos_metric + self.storage = Path(storage) if storage else None + self.delta_tensors = { + op: {k.name: None for k in self.app.knobs} for op in self.app.ops + } + self.baseline_tensor = self.output_f({}) @property def name(self) -> str: @@ -169,15 +203,58 @@ class QoSModelP1(IQoSModel): def measure_qos(self, with_approxes: KnobsT) -> float: """Implementation of model.""" - return 0.0 + assert self.baseline_tensor is not None + delta_tensors = np.array( + [self.delta_tensors[op][knob] for op, knob in with_approxes.items()] + ) + ret = delta_tensors.sum() + self.baseline_tensor + return self.qos_metric(ret) + + def _init(self): + dt = self.delta_tensors + btensor = self.baseline_tensor + if self.storage and self.storage.is_file(): + for op, knob, delta_tensor in self._load(self.storage): + dt[op][knob] = delta_tensor + for op, knob in barred_ravel_knobs(self.app): + if dt[op][knob] is not None: + continue + delta_tensor = self.output_f({op: knob}) - btensor + dt[op][knob] = delta_tensor + self._try_append_save(self.storage, op, knob, delta_tensor) + super()._init() + + @staticmethod + def _load(path: Path) -> Iterator[Tuple[str, str, torch.Tensor]]: + msg_logger.info(f"Found pickle at {path}") + with path.open("rb") as f: + while True: + try: + op_name, knob_name, tensor = pickle.load(f) + yield op_name, knob_name, tensor + except EOFError: + return + + @staticmethod + def _try_append_save( + path: Optional[Path], op_name: str, knob_name: str, tensor: torch.Tensor + ): + if not path: + return + path.touch(exist_ok=True) + with path.open("ab") as f: + pickle.dump((op_name, knob_name, tensor), f) class QoSModelP2(IQoSModel): """QoS model `P2` in ApproxTuner.""" - def __init__(self, app: ModeledApp) -> None: + def __init__(self, app: ModeledApp, storage: PathLike = None) -> None: super().__init__() self.app = app + self.storage = Path(storage) if storage else None + self.qos_df = None + self.baseline_qos = None @property def name(self) -> str: @@ -194,8 +271,56 @@ class QoSModelP2(IQoSModel): return qos def measure_qos(self, with_approxes: KnobsT) -> float: - """Implementation of model.""" - return 0.0 + assert self.baseline_qos is not None and self.qos_df is not None + delta_qoses = np.array( + [self.qos_df.loc[kv] for kv in with_approxes.items()] + ) - self.baseline_qos + ret = delta_qoses.sum() + self.baseline_qos + assert not np.isnan(ret) + return ret + + def _init(self): + if self.storage and self.storage.is_file(): + self.qos_df, self.baseline_qos = self._load(self.storage) + else: + knob_names = [k.name for k in self.app.knobs] + self.qos_df = pd.DataFrame(index=self.app.ops, columns=knob_names) + self.baseline_qos = self._empirical_measure_qos({}) + df = self.qos_df + for op, knob in barred_ravel_knobs(self.app): + if not np.isnan(df.loc[op, knob]): + continue + df.loc[op, knob] = self._empirical_measure_qos({op: knob}) + if self.storage and not self.storage.is_file(): + self._save(self.storage) + super()._init() + + def _load(self, path: Path) -> Tuple[pd.DataFrame, float]: + with path.open() as f: + data = json.load(f) + df = pd.DataFrame(data["df"]) + baseline_qos = float(data["bqos"]) + if "app_name" in data: + name = data["app_name"] + if self.app.name != name: + msg_logger.error( + f'Profile at {path} belongs to app "{name}" ' + f"while our app is {self.app.name}" + ) + else: + msg_logger.warning("Loaded profile does not have app name identifier") + return df, baseline_qos + + def _save(self, path: Path): + with path.open("w") as f: + json.dump( + { + "app_name": self.app.name, + "df": self.qos_df.to_dict(), + "bqos": self.baseline_qos, + }, + f, + ) class ValConfig(Config): @@ -212,6 +337,8 @@ class ValConfig(Config): class ApproxModeledTuner(ApproxTuner): + app: ModeledApp + def tune( self, max_iter: int, @@ -224,6 +351,10 @@ class ApproxModeledTuner(ApproxTuner): perf_model: str = "none", qos_model: str = "none", ) -> List[ValConfig]: + if qos_model != "none": + self.app._init_model(qos_model) + if perf_model != "none": + self.app._init_model(perf_model) ret = super().tune( max_iter=max_iter, qos_tuner_threshold=qos_tuner_threshold, @@ -256,3 +387,18 @@ class ApproxModeledTuner(ApproxTuner): @classmethod def _get_config_class(cls) -> Type[Config]: return ValConfig + + +def barred_ravel_knobs(app: ApproxApp) -> Iterator[Tuple[str, str]]: + """Flattens op_knobs of app to a list of (layer, knob) pairs while showing 2 levels of + progress bar.""" + + from tqdm import tqdm + + bar1 = tqdm(app.op_knobs.items(), leave=None) + for op_name, knobs in bar1: + bar1.set_postfix(op=op_name) + bar2 = tqdm(knobs, leave=None) + for knob in bar2: + bar2.set_postfix(knob=knob.name) + yield op_name, knob.name diff --git a/predtuner/torchapp.py b/predtuner/torchapp.py index 052a378..b056c57 100644 --- a/predtuner/torchapp.py +++ b/predtuner/torchapp.py @@ -108,13 +108,14 @@ class TorchApp(ModeledApp, abc.ABC): qoses = [] for _, target in self.val_loader: end = begin + len(target) + target = move_to_device_recursively(target, self.device) qos = self.tensor_to_qos(tensor_output[begin:end], target) qoses.append(qos) return self.combine_qos(np.array(qoses)) return [ LinearPerfModel(self._op_costs, self._knob_speedups), - QoSModelP1(self._get_raw_output_valset, batched_valset_qos), + QoSModelP1(self, self._get_raw_output_valset, batched_valset_qos), QoSModelP2(self), ] @@ -154,7 +155,7 @@ class TorchApp(ModeledApp, abc.ABC): inputs = move_to_device_recursively(inputs, self.device) outputs = approxed(inputs) all_outputs.append(outputs) - return torch.stack(all_outputs) + return torch.cat(all_outputs, dim=0) @staticmethod def _check_baseline_knob(knobs: Set[TorchApproxKnob]) -> Set[TorchApproxKnob]: diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 -- GitLab