From dae8db446c115297f735ce82426f07a105b8cdfc Mon Sep 17 00:00:00 2001 From: Guillaume Fieni Date: Thu, 14 Nov 2019 23:38:02 +0100 Subject: [PATCH] feat: New formula from paper --- smartwatts/formula.py | 131 ++++++++++++++++++++---------------------- smartwatts/handler.py | 14 +++-- 2 files changed, 70 insertions(+), 75 deletions(-) diff --git a/smartwatts/formula.py b/smartwatts/formula.py index 6c547ef..5657619 100644 --- a/smartwatts/formula.py +++ b/smartwatts/formula.py @@ -39,33 +39,40 @@ class PowerModelNotInitializedException(Exception): pass -class ReportWrapper: +class NotEnoughReportsInHistoryException(Exception): """ - This wrapper stores the needed information for a System report and ease its usage when learning a power model. + This exception happens when a user try to learn a power model without having enough reports in history. + """ + pass + + +class History: + """ + This class stores the reports history to use when learning a new power model. """ - def __init__(self, rapl: Dict[str, float], core: Dict[str, int]) -> None: + def __init__(self) -> None: """ - Initialize a new report wrapper. - :param rapl: RAPL event group - :param core: Core event group + Initialize a new reports history container. """ - self.rapl = rapl - self.core = core + self.X: List[List[int]] = [] + self.y: List[float] = [] - def X(self) -> List[int]: + def __len__(self) -> int: """ - Creates and return a list of events value from the Core events group. - :return: List containing the Core events value sorted by event name. + Compute the length of the history. + :return: Length of the history """ - return [v for _, v in sorted(self.core.items())] + return len(self.X) - def y(self) -> List[float]: + def store_report(self, power_reference: float, events_value: List[int]) -> None: """ - Creates and return a list of events value from the RAPL events group. - :return: List containing the RAPL events value. + Append a report to the reports history. + :param events_value: List of raw events value + :param power_reference: Power reference corresponding to the events value """ - return [v for _, v in sorted(self.rapl.items())] + self.X.append(events_value) + self.y.append(power_reference) class PowerModel: @@ -81,78 +88,62 @@ def __init__(self, frequency: int) -> None: self.frequency = frequency self.model: Union[Ridge, None] = None self.hash: str = 'uninitialized' - self.reports: List[ReportWrapper] = [] + self.history: History = History() + self.id = 0 - def _learn(self) -> None: + def learn_power_model(self) -> None: """ Learn a new power model using the stored reports and update the formula hash. + :raise: NotEnoughReportsInHistoryException when trying to learn without enough previous data """ - X = [] - y = [] - for report in self.reports: - X.append(report.X()) - y.append(report.y()) + if len(self.history) < 3: + return - self.model = Ridge().fit(X, y) + self.model = Ridge().fit(self.history.X, self.history.y) self.hash = hashlib.blake2b(pickle.dumps(self.model), digest_size=20).hexdigest() + self.id += 1 - def store(self, rapl: Dict[str, float], global_core: Dict[str, int]) -> None: + @staticmethod + def _extract_events_value(events: Dict[str, int]) -> List[int]: """ - Store the events group into the System reports list and learn a new power model. - :param rapl: RAPL events group - :param global_core: Core events group of all targets + Creates and return a list of events value from the events group. + :param events: Events group + :return: List containing the events value sorted by event name """ - self.reports.append(ReportWrapper(rapl, global_core)) + return [value for _, value in sorted(events.items())] - if len(self.reports) > 3: - self._learn() - - def compute_global_power_estimation(self, rapl: Dict[str, float], global_core: Dict[str, int]) -> float: + def store_report_in_history(self, power_reference: float, events: Dict[str, int]) -> None: """ - Compute the global power estimation using the power model. - :param rapl: RAPL events group - :param global_core: Core events group of all targets - :return: Power estimation of all running targets using the power model + Store the events group into the System reports list and learn a new power model. + :param power_reference: Power reference (in Watt) + :param events: Events value """ - if not self.model: - self.store(rapl, global_core) - raise PowerModelNotInitializedException() + self.history.store_report(power_reference, self._extract_events_value(events)) - report = ReportWrapper(rapl, global_core) - return self.model.predict([report.X()])[0, 0] - - def compute_target_power_estimation(self, rapl: Dict[str, float], global_core: Dict[str, int], target_core: Dict[str, int]) -> (float, float): + def compute_power_estimation(self, events: Dict[str, int]) -> float: """ - Compute a power estimation for the given target. - :param rapl: RAPL events group - :param global_core: Core events group of all targets - :param target_core: Core events group of any target - :return: Power estimation for the given target and ratio of the target on the global power consumption - :raise: PowerModelNotInitializedException when the power model is not initialized + Compute a power estimation from the events value using the power model. + :param events: Events value + :raise: PowerModelNotInitializedException when haven't been initialized + :return: Power estimation for the given events value """ if not self.model: - self.store(rapl, global_core) raise PowerModelNotInitializedException() - ref_power = next(iter(rapl.values())) - system = ReportWrapper(rapl, global_core).X() - target = ReportWrapper(rapl, target_core).X() - - coefs = next(iter(self.model.coef_)) - sum_coefs = sum(coefs) + return self.model.predict([self._extract_events_value(events)])[0] - ratio = 0.0 - for index, coef in enumerate(coefs): - try: - ratio += (coef / sum_coefs) * (target[index] / system[index]) - except ZeroDivisionError: - pass - - target_power = ref_power * ratio - if target_power < 0.0: - return 0.0, 0.0 - - return target_power, ratio + @staticmethod + def cap_power_estimation(power_reference: float, global_power: float, target_power: float) -> (float, float): + """ + Cap the target power estimation to the reference power estimation. + :param power_reference: Reference power estimation (in Watt, usually RAPL) + :param global_power: Global power estimation from the power model (in Watt) + :param target_power: Target power estimation from the power model (in Watt) + :return: Capped power estimation (in Watt) with its ratio over global power consumption + """ + ratio = target_power / global_power if global_power > 0.0 else 0.0 + power = power_reference * ratio + return power, ratio class SmartWattsFormula: @@ -171,7 +162,7 @@ def __init__(self, cpu_topology: CPUTopology) -> None: def _gen_models_dict(self) -> Dict[int, PowerModel]: """ Generate and returns a layered container to store per-frequency power models. - :return: Initialized Ordered dict containing a power model for each frequency layer. + :return: Initialized Ordered dict containing a power model for each frequency layer """ return OrderedDict((freq, PowerModel(freq)) for freq in self.cpu_topology.get_supported_frequencies()) diff --git a/smartwatts/handler.py b/smartwatts/handler.py index 4f764ef..5891735 100644 --- a/smartwatts/handler.py +++ b/smartwatts/handler.py @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - from collections import OrderedDict, defaultdict from datetime import datetime from math import ldexp, fabs @@ -126,7 +125,8 @@ def _gen_formula_report(self, timestamp: datetime, pkg_frequency: float, model: 'socket': self.state.socket, 'layer_frequency': model.frequency, 'pkg_frequency': pkg_frequency, - 'reports': len(model.reports), + 'samples': len(model.history), + 'id': model.id, 'error': error } return FormulaReport(timestamp, self.state.sensor, model.hash, metadata) @@ -163,15 +163,18 @@ def _process_oldest_tick(self) -> (List[PowerReport], List[FormulaReport]): # compute Global target power report try: - system_power = model.compute_global_power_estimation(rapl, global_core) + system_power = model.compute_power_estimation(global_core) power_reports.append(self._gen_power_report(timestamp, 'global', model.hash, system_power, 1.0)) except PowerModelNotInitializedException: + model.store_report_in_history(rapl_power, global_core) + model.learn_power_model() return power_reports, formula_reports # compute per-target power report for target_name, target_report in hwpc_reports.items(): target_core = self._gen_core_events_group(target_report) - target_power, target_ratio = model.compute_target_power_estimation(rapl, global_core, target_core) + target_power = model.compute_power_estimation(target_core) + target_power, target_ratio = model.cap_power_estimation(rapl_power, system_power, target_power) power_reports.append(self._gen_power_report(timestamp, target_name, model.hash, target_power, target_ratio)) # compute power model error from reference @@ -179,7 +182,8 @@ def _process_oldest_tick(self) -> (List[PowerReport], List[FormulaReport]): # store Global report if the power model error exceeds the error threshold if model_error > self.state.config.error_threshold: - model.store(rapl, global_core) + model.store_report_in_history(rapl_power, global_core) + model.learn_power_model() # store information about the power model used for this tick formula_reports.append(self._gen_formula_report(timestamp, pkg_frequency, model, model_error))