diff --git a/ipynb/energy/EnergyModel_SanityCheck.ipynb b/ipynb/energy/EnergyModel_SanityCheck.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..8b201099c1eeca375b5a61898ec60e9d20f6139b --- /dev/null +++ b/ipynb/energy/EnergyModel_SanityCheck.ipynb @@ -0,0 +1,355 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Energy Model sanity checks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Energy Models (EM) of real platforms rarely match the theoretical models. This notebook is an attempt to centralize a series of checks on the EM of a live target in order to detect abnormalities.\n", + "\n", + "The presence of undesired properties in an EM doesn't necessarily mean that something needs fixing, especially if the this is a real hardware issue. The goal of this notebook is mainly to be informative and to summarize the level of brokeness of a new platform quickly. What shall be done with this information (if it is possible to do something) is totally target and usecase-dependent." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2018-07-24 17:05:34,090 INFO : root : Using LISA logging configuration:\n", + "2018-07-24 17:05:34,090 INFO : root : /home/queper01/dev/lisa/logging.conf\n", + "2018-07-24 17:05:34,124 INFO : root : Generating grammar tables from /usr/lib/python2.7/lib2to3/Grammar.txt\n", + "2018-07-24 17:05:34,140 INFO : root : Generating grammar tables from /usr/lib/python2.7/lib2to3/PatternGrammar.txt\n" + ] + } + ], + "source": [ + "import logging\n", + "from conf import LisaLogging\n", + "LisaLogging.setup()\n", + "from env import TestEnv\n", + "from energy_model import EnergyModel\n", + "import libs.utils.energy_model as em" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Global variables\n", + "target = None\n", + "nrg_model = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create EM from a live target" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2018-07-24 17:05:35,062 INFO : TestEnv : Using base path: /data/work/dev/lisa\n", + "2018-07-24 17:05:35,063 INFO : TestEnv : Loading custom (inline) target configuration\n", + "2018-07-24 17:05:35,064 INFO : TestEnv : Devlib modules to load: ['cpuidle', 'cpufreq']\n", + "2018-07-24 17:05:35,065 INFO : TestEnv : Connecting linux target:\n", + "2018-07-24 17:05:35,065 INFO : TestEnv : username : linaro\n", + "2018-07-24 17:05:35,066 INFO : TestEnv : host : 10.4.13.128\n", + "2018-07-24 17:05:35,067 INFO : TestEnv : password : linaro\n", + "2018-07-24 17:05:35,067 INFO : TestEnv : Connection settings:\n", + "2018-07-24 17:05:35,068 INFO : TestEnv : {'username': 'linaro', 'host': '10.4.13.128', 'password': 'linaro'}\n", + "2018-07-24 17:05:42,790 INFO : TestEnv : Initializing target workdir:\n", + "2018-07-24 17:05:42,791 INFO : TestEnv : /home/linaro/devlib-target\n", + "2018-07-24 17:05:43,559 INFO : TestEnv : Topology:\n", + "2018-07-24 17:05:43,560 INFO : TestEnv : [[0, 3, 4, 5], [1, 2]]\n", + "2018-07-24 17:05:43,916 INFO : TestEnv : Loading default EM:\n", + "2018-07-24 17:05:43,917 INFO : TestEnv : /data/work/dev/lisa/libs/utils/platforms/juno.json\n", + "2018-07-24 17:05:44,093 INFO : EnergyMeter : HWMON module not enabled\n", + "2018-07-24 17:05:44,093 WARNING : EnergyMeter : Energy sampling disabled by configuration\n", + "2018-07-24 17:05:44,094 INFO : TestEnv : Set results folder to:\n", + "2018-07-24 17:05:44,095 INFO : TestEnv : /data/work/dev/lisa/results/20180724_170535\n", + "2018-07-24 17:05:44,095 INFO : TestEnv : Experiment results available also in:\n", + "2018-07-24 17:05:44,096 INFO : TestEnv : /data/work/dev/lisa/results_latest\n", + "2018-07-24 17:05:44,272 INFO : EMReader : Attempting to load EM using from_simplifiedEM_target\n" + ] + } + ], + "source": [ + "# Minimal configuration for a Juno\n", + "my_conf = {\n", + " \n", + " # Target platform and board\n", + " \"platform\" : \"linux\",\n", + " \"board\" : \"juno\",\n", + " \"host\" : \"10.4.13.128\",\n", + " \"username\" : \"linaro\",\n", + " \"password\" : \"linaro\",\n", + " \n", + " \"modules\" : [\"cpufreq\", \"cpuidle\"],\n", + " \"exclude_modules\" : [\"bl\", \"hwmon\"],\n", + "}\n", + "\n", + "target = TestEnv(target_conf=my_conf, force_new=True).target\n", + "nrg_model = EnergyModel.from_target(target)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create EM from a static energy model" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "#from platforms.hikey_energy import hikey_energy as nrg_model\n", + "#from platforms.hikey960_energy import hikey960_energy as nrg_model\n", + "#from platforms.juno_r0_energy import juno_r0_energy as nrg_model\n", + "#from platforms.pixel_energy import pixel_energy as nrg_model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Energy efficiency" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "On sane hardware, OPPs at high frequencies should be less energy-efficient than OPPs at low frequencies. If that is not true, the heuristics used in schedutil and EAS are sub-optimal since they always assume the smallest possible frequency request for any given utilization level.\n", + "\n", + "The following check computes the 'capacity / power' ratio of all OPPs of the Energy Model, and asserts that this ratio is monotonically decreasing." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OK: The power efficiency of all CPUs decreases monotonically with growing frequencies\n" + ] + } + ], + "source": [ + "passed, msg = em.is_efficiency_decreasing(nrg_model)\n", + "if not passed:\n", + " print('ERROR: ' + msg)\n", + "else:\n", + " print('OK: The power efficiency of all CPUs decreases monotonically with growing frequencies')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "## Number of active states" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the wake-up path, EAS tries to predict what will be the frequency request given a certain utilization level. The (private) tables used by EAS are supposed to match the OPPs of the target, hence enabling a quick look-up. However, it can be hard on some platform (QCOM SoCs, for example) to know the exact number of OPPs prior to boot, so reporting a static Energy Model in DT which is synchronized with the real set of OPPs isn't easy.\n", + "\n", + "The following check compares the number of entries in the Energy Model tables with the actual available frequencies as reported by CPUFreq in order to detect any mismatch between the two." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OK: the EM and CPUFreq both report the same number of OPPs\n" + ] + } + ], + "source": [ + "if not target:\n", + " raise RuntimeError(\"Impossible to check what CPUFreq reports whitout a target\")\n", + "\n", + "freqs = []\n", + "for fd in nrg_model.freq_domains:\n", + " cpu = fd[0]\n", + " freqs.append(len(target.cpufreq.list_frequencies(cpu)))\n", + "passed, msg = em.check_active_states_nb(nrg_model, freqs)\n", + "\n", + "if not passed:\n", + " print('ERROR: ' + msg)\n", + "else:\n", + " print('OK: the EM and CPUFreq both report the same number of OPPs')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Over-utilized band" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When no CPU is over-utilized, EAS will try to avoid over-utilization as much as possible to avoid loosing the 'control' on the system. As such, the OPPs that are only available in the over-utilized band (between 80% and 100% of the CPU capacity) are generally used very infrequently.\n", + "\n", + "The following hunk reports the OPPs in the over-utilized band." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The OPPs in the overutilized band are:\n", + "\tGroup 0: [367, 406, 446]\n", + "\tGroup 1: [884, 1024]\n", + "\n" + ] + } + ], + "source": [ + "opp_overutilized = em.get_opp_overutilized(nrg_model)\n", + "msg = \"The OPPs in the overutilized band are:\\n\"\n", + "for i, opp in enumerate(opp_overutilized):\n", + " msg += \"\\tGroup {}: {}\\n\".format(i, opp[1])\n", + "print(msg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The fact that some OPPs are mostly unused because of the over-utilized mechanism can lead to sub-optimal task placements. Indeed, the overutilized OPPs of the little CPUs won't be used much, even if they actually are more energy efficient than the low OPPs of big CPUs.\n", + "\n", + "The over-utilized threshold is a fairly strong design choice for EAS, so it is not obvious if anything needs fixing. However, some OPPs of the little CPUs might be wasted because of this. The following hunk checks if that is the case." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ERROR: It is more energy efficient to run for the utilization [367, 406, 446, 356.8, 418] on the little cpu \n", + "\t\tbut it is run on the big cpu due to the overutilization zone\n" + ] + } + ], + "source": [ + "passed, msg = em.compare_big_little_opp(nrg_model)\n", + "if not passed:\n", + " print('ERROR: ' + msg)\n", + "else:\n", + " print ('OK: the overutilized OPPs of the little CPUs are less efficient that the low OPPs of bigs')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ideal placement" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is not desired to have a lot of overlap in the power / perf curves of different types of CPUs. As an attempt to detect completely bogus Energy Models, the following hunk checks that a task using exactly the average capacity of a certain type of CPU would indeed be ran on this type of CPU, if EAS was optimal." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OK: Average-capacity tasks end up where expected\n" + ] + } + ], + "source": [ + "passed, msg = em.ideal_placements(nrg_model)\n", + "if not passed:\n", + " print('ERROR: ' + msg)\n", + "else:\n", + " print('OK: Average-capacity tasks end up where expected')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/libs/utils/energy_model.py b/libs/utils/energy_model.py index d01149ee70cda68fd7a60cd9ccff3f41a07a21a8..4e3e4d7ca1f433ee5398e91a044aa4e823fd0312 100644 --- a/libs/utils/energy_model.py +++ b/libs/utils/energy_model.py @@ -19,6 +19,7 @@ from collections import namedtuple, OrderedDict from itertools import product import logging import operator +import itertools import re import pandas as pd @@ -1152,3 +1153,323 @@ class EnergyModel(object): return ret return inputs.apply(f, axis=1) + +########################################### +# Sanity Checks helpers for Energy Models # +########################################### + +# The utilization rate limit to switch to the overutilization mode +OVERUTILIZED_RATE = 0.8 + +def _get_powers_list(node): + """ + Get the list of powers for a given node. + :param node: A node of the energy model. It can be a cluster or cpu. + :type node: EnergyModelNode + + :returns: The list of powers associates with the given node. + """ + return [state.power for state in node.active_states.values()] + +def _get_capacities_list(node): + """ + Get the list of capacities for a given node. + :param node: A node of the energy model. + :type node: EnergyModelNode + + :returns: The list of capacities associates with the given node. + """ + return [state.capacity for state in node.active_states.values()] + +def is_efficiency_decreasing(energy_model): + """ + Check that for all the cpus of the energy model the energy efficiency is + decreasing. + :params energy_model: The energy model to control + :type energy_model: EnergyModel + + :returns: A tuple that contains True if the energy_model respects + the previous assumption, False otherwise; and an error message. + """ + msg = '' + powers = [] + cap = [] + energy = [] + failed_nodes = [] + for cpu in energy_model.cpu_nodes: + powers = _get_powers_list(cpu) + cap = _get_capacities_list(cpu) + energy = [float(powers[i])/cap[i] for i in range(len(powers))] + # Check that power / cap is strickly increasing which is equivalent + # to check that the cap / power (energy efficiency) is decreasing + if energy != sorted(energy): + failed_nodes.append(cpu.name) + + if failed_nodes: + msg = ('The energy efficiency is not decreasing ' + 'for {}'.format(failed_nodes)) + return (False, msg) + + return (True, msg) + +def check_active_states_nb(energy_model, freqs_nb): + """ + Control that the active states number is equal to the number of + frequencies in an energy model. + :params energy_model: The energy model to control + :type energy_model: EnergyModel + + :params freqs_nb: A list that contains the number of frequencies per cluster + :type freqs_nb: [int] + + :returns: A tuple that contains True if the number of active states + correspond to the number of frequencies for each cluster, False + otherwise; and an error message. + """ + msg = '' + succeed = True + failed_nodes = [] + for i, fd in enumerate(energy_model.freq_domains): + for cpu in fd: + cpu_node = energy_model.cpu_nodes[cpu] + active_state_nb = len(cpu_node.active_states.keys()) + if active_state_nb != freqs_nb[i]: + failed_nodes.append(cpu_node.name) + + # Check higher levels of the EM + node = energy_model.cpu_nodes[fd[0]] + while node.parent.active_states: + node = node.parent + active_state_nb = len(node.active_states.keys()) + if active_state_nb != freqs_nb[i]: + failed_nodes.append(node.name) + + if failed_nodes: + msg = ('The number of active states is not equal to the ' + 'frequency number for {}'.format(failed_nodes)) + return (False, msg) + + return (True, msg) + +def get_opp_overutilized(energy_model): + """ + Get the capacity limit after which the cpu enters in the overutilized mode + and indicate all the capacities for the group that are over this limit. + :params energy_model: The energy model to control + :type energy_model: EnergyModel + + :returns: a list containing a tupple per group containing the limit + capacity and an list of the opp that are in the overutilized zone + """ + opp = [] + for group in energy_model.cpu_groups: + # Get the first cpu of the group + cpu = energy_model.cpu_nodes[group[0]] + cap = _get_capacities_list(cpu) + cap_limit = cap[-1] * OVERUTILIZED_RATE + + # Get the elements that are bigger than the capacity limit + cap_overutilized = [x for x in cap if x > cap_limit] + opp.append((cap_limit, cap_overutilized)) + return opp + +def get_avg_cap(energy_model): + """ + Get a task utilization which should fits on a groups of cpus. + + :params energy_model: The energy model to get the information + :type energy_model: EnergyModel + + :returns: a list of average capacities per group of cpus. + """ + avg_cap = [] + max_cap = 0 + min_cap = 0 + for group in energy_model.cpu_groups: + cap = _get_capacities_list(energy_model.cpu_nodes[group[0]]) + + # Choose the min capacity of the task or the max capacity of the + # previous group to be sure that the task cannot fit on the previous + # group + min_cap = cap[0] if cap[0] > max_cap else max_cap + max_cap = cap[-1] + avg_cap.append((min_cap + max_cap * OVERUTILIZED_RATE) / 2) + return avg_cap + +def are_placements_equal(energy_model, expected, placement): + """ + Given an optimal placement and the placement simulated on the energy + model controls if both placements are equal per group. + :params energy_model: The energy model from which the placement is tested + :type energy_model: EnergyModel + + :params expected: a list of group containing a list of util per cpus. For + instance a placement could be : [[0, 0, 0, 100], [0,0]] + for an energy model containing 4 little cpus and 2 big + cpus and an util of 100 is expected on one of the little + cpus. + :type expected: [[int]] + + :params placement: an exhaustive list of placements obtaining by the + function get_optimal_placements from the EnergyModel. + :type placement: [(int)] + + :returns: True if both placements are equal, False otherwise. + """ + for i, group in enumerate(energy_model.cpu_groups): + # Generate all the placements possible for this group of cpus + s1 = set(itertools.permutations(expected[i])) + # Extract the placement for the given group + s2 = set([tuple(l[g] for g in group) for l in placement]) + if s1 != s2: + return False + return True + +def get_expected_placement(energy_model, group_util): + """ + Create the expected placement given an utilization per group. + :params energy_model: The energy model for which the expected placement is + computed + :type energy_model: EnergyModel + + :params group_util: an utilization value for each group of cpus + :type group_util: [int] + + :returns: a list for each group that contain a list of utilization per cpu. + For instance for a model that has 4 little cpus and 2 big + cpus it returns the following data: + [[group_util[0], 0, 0, 0], [group_util[1], 0]] + """ + expected = [] + for i, group in enumerate(energy_model.cpu_groups): + cpus_nb = len(group) + cpus_util = [0] * cpus_nb + cpus_util[0] = group_util[i] + expected.append(cpus_util) + return expected + +def ideal_placements(energy_model): + """ + For each group, generates a workload that should run only on it and + controls that obtained placement is equal to the expected placement + :params energy_model: The energy model for which these workloads are + generated + :type energy_model: EnergyModel + + :returns: True if the expected placement is equal to the obtained one, + False otherwise + """ + msg = '' + avg_cap = get_avg_cap(energy_model) + nb_groups = len(energy_model.cpu_groups) + for i in range(nb_groups): + # Task with a capacity that should fit on the group + one_task = {'t0': avg_cap[i]} + + # Get the placement obtained for this task on the model + placement = energy_model.get_optimal_placements(one_task) + + # Create the expected placement + exp_cap = [0] * nb_groups + exp_cap[i] = avg_cap[i] + expected = get_expected_placement(energy_model, exp_cap) + + # Compare the expected placement and the obtained one + if not are_placements_equal(energy_model, expected, placement): + msg = ('The expected placement for the task {} is {} but the ' + 'obtained placement \n\t\t' + 'was {}'.format(one_task, expected, placement)) + return (False, msg) + + return (True, msg) + +def _get_efficiency(util, cpu): + """ + For a given utilization, computes the energy efficiency (capacity / power) + to run at this utilization. If it does not correspond to an opp for the + cpu, the efficiency is computed by selecting the opp above the + utilization and the power is computed proportionally to it. + :params util: targeted utilization + :type util: int + + :params cpu: The cpu for which the energy efficiency is computed. + :type cpu: EnergyModelNode + + :returns: The ratio between the capacity and the power for the targeted opp + """ + + # Get the list of capacities and powers for the cpu + capacities = _get_capacities_list(cpu) + powers = _get_powers_list(cpu) + + # Compute the efficiency for the first capacity larger or equal than the + # requiered utilization + for cap in capacities: + if cap >= util: + # The power used for a specific utilization is proportional to + # the capacity used for this utilization. + power = powers[capacities.index(cap)] * (float(util) / cap) + return float(cap) / power + + raise ValueError('The utilization {} is larger than the possible ' + 'opp for {}'.format(util, cpu.name)) + +def compare_big_little_opp(energy_model): + """ + Compare the opp between the little and big cpus. The big cpus should not + have any opp that are more power-efficient than the little in the + non-overutilization zone. + And the little cpus should not have any opp that is more power-efficient + that the big opp but are skipped due to the overutilization zone. + + :params energy_model: the energy model that contains the information + for the cpus + :type energy_model: EnergyModel + + :returns: A tupple containing True if the conditions are respected, False + otherwise; and an error message. + """ + msg = '' + overutilized_failed_opp = [] + non_overutilized_failed_opp = [] + succeed = True + groups = energy_model.cpu_groups + + # This test can only be run on big.LITTLE architecture + if len(groups) < 2: + return (True, msg) + + # Get the first little and big cpus + little_cpu = energy_model.cpu_nodes[groups[0][0]] + big_cpu = energy_model.cpu_nodes[groups[1][0]] + + # Get the opp in overutilized mode for the little cpu + (limit_opp, overutilized_opp) = get_opp_overutilized(energy_model)[0] + + little_cap = _get_capacities_list(little_cpu) + # Get the opp capacities of the big cpu that overlaps with the little + # capacities + big_cap = [c for c in _get_capacities_list(big_cpu) if c < little_cap[-1]] + + # Iterates on all the opp and the overutilized point + for opp in little_cap+[limit_opp]+big_cap: + # Compute efficiency for the big and little cpus + little_efficiency = _get_efficiency(opp, little_cpu) + big_efficiency = _get_efficiency(opp, big_cpu) + if opp < limit_opp: + if big_efficiency > little_efficiency: + non_overutilized_failed_opp.append(opp) + succeed = False + else: + if little_efficiency > big_efficiency: + overutilized_failed_opp.append(opp) + succeed = False + + if overutilized_failed_opp: + msg += ('It is more energy efficient to run for the utilization {} ' + 'on the little cpu \n\t\tbut it is run on the big cpu due ' + 'to the overutilization zone'.format(overutilized_failed_opp)) + if non_overutilized_failed_opp: + msg += ('\n\t\tThe utilization {} is more power efficient to run on a ' + 'big cpu'.format(non_overutilized_failed_opp)) + return (succeed, msg) diff --git a/libs/utils/platforms/hikey960_energy.py b/libs/utils/platforms/hikey960_energy.py new file mode 100644 index 0000000000000000000000000000000000000000..382a8398ca4d4a6b00bff4aac2367c0a3aa3af46 --- /dev/null +++ b/libs/utils/platforms/hikey960_energy.py @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2018, Arm Limited and contributors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from collections import OrderedDict + +from energy_model import (ActiveState, EnergyModelNode, EnergyModelRoot, + PowerDomain, EnergyModel) + +a53_cluster_active_states = OrderedDict([ + (533000, ActiveState(power=12)), + (999000, ActiveState(power=22)), + (1402000, ActiveState(power=36)), + (1709000, ActiveState(power=67)), + (1844000, ActiveState(power=144)), +]) + +# TODO warn if any of the idle states aren't represented by power domains +a53_cluster_idle_states = OrderedDict([ + ("WFI", 12), + ("cpu-sleep-0", 12), + ("cluster-sleep-0", 0), +]) + +a53_cpu_active_states = OrderedDict([ + (533000, ActiveState(capacity=133, power=87)), + (999000, ActiveState(capacity=250, power=164)), + (1402000, ActiveState(capacity=351, power=265)), + (1709000, ActiveState(capacity=429, power=388)), + (1844000, ActiveState(capacity=462, power=502)), +]) + +a53_cpu_idle_states = OrderedDict([ + ("WFI", 5), + ("cpu-sleep-0", 0), + ("cluster-sleep-0", 0), +]) + +a53s = [0, 1, 2, 3] + +def a53_cpu_node(cpu): + return EnergyModelNode(cpu=cpu, + active_states=a53_cpu_active_states, + idle_states=a53_cpu_idle_states) + +a73_cluster_active_states = OrderedDict([ + (903000, ActiveState(power=102)), + (1421000, ActiveState(power=124)), + (1805000, ActiveState(power=221)), + (2112000, ActiveState(power=330)), + (2362000, ActiveState(power=433)), +]) + +a73_cluster_idle_states = OrderedDict([ + ("WFI", 102), + ("cpu-sleep-0", 102), + ("cluster-sleep-0", 0), +]) + +a73_cpu_active_states = OrderedDict([ + (903000, ActiveState(capacity=390, power=404)), + (1421000, ActiveState(capacity=615, power=861)), + (1805000, ActiveState(capacity=782, power=1398)), + (2112000, ActiveState(capacity=915, power=2200)), + (2362000, ActiveState(capacity=1024, power=2848)), +]) + +a73_cpu_idle_states = OrderedDict([ + ("WFI", 18), + ("cpu-sleep-0", 0), + ("cluster-sleep-0", 0), +]) + +a73s = [4, 5, 6, 7] + +def a73_cpu_node(cpu): + return EnergyModelNode(cpu=cpu, + active_states=a73_cpu_active_states, + idle_states=a73_cpu_idle_states) + +hikey960_energy = EnergyModel( + root_node=EnergyModelRoot( + children=[ + EnergyModelNode( + name="cluster_a53", + active_states=a53_cluster_active_states, + idle_states=a53_cluster_idle_states, + children=[a53_cpu_node(c) for c in a53s]), + EnergyModelNode( + name="cluster_a73", + active_states=a73_cluster_active_states, + idle_states=a73_cluster_idle_states, + children=[a73_cpu_node(c) for c in a73s])]), + root_power_domain=PowerDomain(idle_states=[], children=[ + PowerDomain( + idle_states=["cluster-sleep-0"], + children=[PowerDomain(idle_states=["WFI", "cpu-sleep-0"], cpu=c) + for c in a73s]), + PowerDomain( + idle_states=["cluster-sleep-0"], + children=[PowerDomain(idle_states=["WFI", "cpu-sleep-0"], cpu=c) + for c in a53s])]), + freq_domains=[a53s, a73s]) diff --git a/tests/lisa/test_energy_model.py b/tests/lisa/test_energy_model.py index 5e4d004e17df16510cb82b0a28e219d795a8976c..98dd018f15dc1e149fdab431e8f8fce9c42e9e58 100644 --- a/tests/lisa/test_energy_model.py +++ b/tests/lisa/test_energy_model.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # - from collections import OrderedDict from unittest import TestCase import os @@ -22,13 +21,18 @@ import shutil from tempfile import mkdtemp from energy_model import (EnergyModel, ActiveState, EnergyModelCapacityError, - EnergyModelNode, EnergyModelRoot, PowerDomain) + EnergyModelNode, EnergyModelRoot, PowerDomain, + is_efficiency_decreasing, check_active_states_nb, + get_opp_overutilized, compare_big_little_opp, + get_expected_placement, are_placements_equal, + get_avg_cap) from trace import Trace # Import these just to test that they can be constructed import libs.utils.platforms.juno_r0_energy import libs.utils.platforms.pixel_energy import libs.utils.platforms.hikey_energy +import libs.utils.platforms.hikey960_energy """ A very basic test suite for the EnergyModel class.""" @@ -62,9 +66,9 @@ little_cpu_idle_states = OrderedDict([ ]) littles=[0, 1] -def little_cpu_node(cpu): +def little_cpu_node(cpu, active_states): return EnergyModelNode(cpu=cpu, - active_states=little_cpu_active_states, + active_states=active_states, idle_states=little_cpu_idle_states) big_cluster_active_states = OrderedDict([ @@ -91,36 +95,41 @@ big_cpu_idle_states = OrderedDict([ bigs=[2, 3] -def big_cpu_node(cpu): +def big_cpu_node(cpu, active_state): return EnergyModelNode(cpu=cpu, - active_states=big_cpu_active_states, + active_states=active_state, idle_states=big_cpu_idle_states) - -em = EnergyModel( - root_node=EnergyModelRoot(children=[ - EnergyModelNode(name='cluster_little', - active_states=little_cluster_active_states, - idle_states=little_cluster_idle_states, - children=[little_cpu_node(0), - little_cpu_node(1)]), - EnergyModelNode(name='cluster_big', - active_states=big_cluster_active_states, - idle_states=big_cluster_idle_states, - children=[big_cpu_node(2), - big_cpu_node(3)]) - ]), - root_power_domain=PowerDomain(idle_states=[], children=[ - PowerDomain( - idle_states=['cluster-sleep-0'], - children=[PowerDomain(idle_states=['WFI', 'cpu-sleep-0'], cpu=c) - for c in littles]), - PowerDomain( - idle_states=['cluster-sleep-0'], - children=[PowerDomain(idle_states=['WFI', 'cpu-sleep-0'], cpu=c) - for c in bigs]), +def createEnergyModel(little_cpu=little_cpu_active_states, + big_cpu=big_cpu_active_states, + little_cluster=little_cluster_active_states, + big_cluster=big_cluster_active_states): + return EnergyModel( + root_node=EnergyModelRoot(children=[ + EnergyModelNode(name='cluster_little', + active_states=little_cluster, + idle_states=little_cluster_idle_states, + children=[little_cpu_node(0, little_cpu), + little_cpu_node(1, little_cpu)]), + EnergyModelNode(name='cluster_big', + active_states=big_cluster, + idle_states=big_cluster_idle_states, + children=[big_cpu_node(2, big_cpu), + big_cpu_node(3, big_cpu)]) ]), - freq_domains=[littles, bigs] -) + root_power_domain=PowerDomain(idle_states=[], children=[ + PowerDomain( + idle_states=['cluster-sleep-0'], + children=[PowerDomain(idle_states=['WFI', 'cpu-sleep-0'], cpu=c) + for c in littles]), + PowerDomain( + idle_states=['cluster-sleep-0'], + children=[PowerDomain(idle_states=['WFI', 'cpu-sleep-0'], cpu=c) + for c in bigs]), + ]), + freq_domains=[littles, bigs] + ) + +em = createEnergyModel() class TestInvalid(TestCase): """Test the sanity checks in EnerygModel setup""" @@ -423,3 +432,121 @@ class TestEstimateFromTrace(TestCase): row = df.iloc[i] self.assertAlmostEqual(row.name, exp_index, places=4) self.assertDictEqual(row.to_dict(), exp_values) + +class TestSanityCheckUtils(TestCase): + """ + The tests proposes some utils to sanity check the energy model. This + test controls the behaviours of these utils functions. + """ + # Good cpu active states + coherent_little_cpu_active_states = OrderedDict([ + (1000, ActiveState(capacity=100, power=100)), + (1500, ActiveState(capacity=150, power=160)), + (2000, ActiveState(capacity=200, power=800)), + ]) + coherent_big_cpu_active_states = OrderedDict([ + (3000, ActiveState(capacity=300, power=950)), + (4000, ActiveState(capacity=400, power=3000)), + ]) + + # Wrong cpu active states + wrong_little_cpu_active_states = OrderedDict([ + (1000, ActiveState(capacity=100, power=100)), + (1500, ActiveState(capacity=150, power=100)), + (2000, ActiveState(capacity=200, power=100)), + ]) + + wrong_big_cpu_active_states = OrderedDict([ + (1000, ActiveState(capacity=300, power=500)), + (1500, ActiveState(capacity=400, power=100)), + (2000, ActiveState(capacity=500, power=100)) + ]) + + # Wrong cluster active states + wrong_cluster_active_states = OrderedDict([ + (1000, ActiveState(power=30)), + (1500, ActiveState(power=30)), + (2000, ActiveState(power=30)), + ]) + + good_em = createEnergyModel(little_cpu=coherent_little_cpu_active_states, + big_cpu=coherent_big_cpu_active_states) + + bad_em = createEnergyModel(little_cpu=wrong_little_cpu_active_states, + big_cpu=wrong_big_cpu_active_states, + little_cluster=wrong_cluster_active_states, + big_cluster=wrong_cluster_active_states) + + def test_is_efficiency_decreasing(self): + """is_efficiency_decreasing correctly reports errors""" + (succeed, msg) = is_efficiency_decreasing(self.good_em) + self.assertTrue(succeed) + self.assertEqual(msg, '') + + (succeed, msg) = is_efficiency_decreasing(self.bad_em) + self.assertFalse(succeed) + self.assertIn("['cpu0', 'cpu1', 'cpu2', 'cpu3']", msg) + + def test_nb_active_states(self): + """check_active_states_nb correctly reports errors""" + (succeed, msg) = check_active_states_nb(self.good_em, [3,2]) + self.assertTrue(succeed) + self.assertEqual(msg, '') + + (succeed, msg) = check_active_states_nb(self.good_em, [5,2]) + self.assertFalse(succeed) + self.assertIn("['cpu0', 'cpu1', 'cluster_little']", msg) + (succeed, msg) = check_active_states_nb(self.good_em, [3,5]) + self.assertFalse(succeed) + self.assertIn("['cpu2', 'cpu3', 'cluster_big']", msg) + + def test_get_opp_overutilized(self): + """get_opp_overutilized correctly reports information""" + opp_overutilized = get_opp_overutilized(self.good_em) + expected_opp = [(160.0, [200]), (320.0, [400])] + self.assertEqual(opp_overutilized, expected_opp) + + def test_get_avg_cap(self): + """get_avg_cap correctly reports information""" + avg_load = get_avg_cap(self.good_em) + expected_load = [130.0, 310.0] + self.assertEqual(avg_load, expected_load) + + def test_compare_big_little_opp(self): + """compare_big_little_opp correctly reports errors""" + (succeed, msg) = compare_big_little_opp(self.good_em) + self.assertTrue(succeed) + self.assertEqual(msg, '') + + (succeed, msg) = compare_big_little_opp(self.bad_em) + self.assertFalse(succeed) + self.assertIn("[200, 160.0]", msg) + self.assertIn("[100]", msg) + + def test_get_expected_placement(self): + """get_expected_placement returns expected value""" + util = [100, 0] + placement = get_expected_placement(self.good_em, util) + self.assertEqual(placement, [[100, 0], [0, 0]]) + + util = [100, 200] + placement = get_expected_placement(self.good_em, util) + self.assertEqual(placement, [[100, 0], [200, 0]]) + + def test_are_placements_equal(self): + """_are_placements_equal returns expected value""" + s1 = [[100, 0], [0, 0]] + s2 = [(100,0,0,0), (0,100,0,0)] + self.assertTrue(are_placements_equal(self.good_em, s1, s2)) + + s2 = [(100,0,0,0)] + self.assertFalse(are_placements_equal(self.good_em, s1, s2)) + + s2 = [(100,0,0,0), (0,100,0,0), (0,0,100,0)] + self.assertFalse(are_placements_equal(self.good_em, s1, s2)) + + s2 = [(0,0,100,0), (0,0,0,100)] + self.assertFalse(are_placements_equal(self.good_em, s1, s2)) + + s2 = [(200,0,0,0), (0,200,0,0)] + self.assertFalse(are_placements_equal(self.good_em, s1, s2))