From 2bede29a20f12477a58b4bb6f1f22d23351b29c1 Mon Sep 17 00:00:00 2001 From: Elieva Pignat Date: Fri, 2 Mar 2018 11:14:51 +0000 Subject: [PATCH 1/4] Energy Model: add sanity check functions This patch adds some sanity check test that can be done one the energy model. Signed-off-by: Elieva Pignat --- tests/utils/__init__.py | 0 tests/utils/em.py | 313 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/em.py diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/utils/em.py b/tests/utils/em.py new file mode 100644 index 000000000..e6258f155 --- /dev/null +++ b/tests/utils/em.py @@ -0,0 +1,313 @@ +import itertools + +# 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. + """ + powers = [] + for freq in node.active_states.keys(): + powers.append(node.active_states[freq].power) + return powers + +def _get_capacities_list(node): + """ + Get the list of capacities 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 capacities associates with the given node. + """ + cap = [] + for freq in node.active_states.keys(): + cap.append(node.active_states[freq].capacity) + return cap + +def _is_list_strickly_increasing(l): + return all(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 cpu. + + :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): + # Extract the placement for the given group + s1 = set([tuple(l[g] for g in group) for l in placement]) + # Generate all the placements possible for this group of cpus + s2 = set(itertools.permutations(expected[i])) + 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 four little cpus and two big + cpus it will return 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) + for i, group in enumerate(energy_model.cpu_groups): + one_task = {'t0': avg_cap[i]} + placement = energy_model.get_optimal_placements(one_task) + exp_cap = [0] * len(energy_model.cpu_groups) + exp_cap[i] = avg_cap[i] + expected = _get_expected_placement(energy_model, exp_cap) + 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 than the + # requiered opp + for cap in capacities: + if cap >= util: + 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 check_overutilized_area(energy_model): + """ + For the overutilized zone of the little cpu controls that it is indeed more + efficient to run a workload on the big cpu. + :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 = '' + groups = energy_model.cpu_groups + 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] + + for opp in [limit_opp]+overutilized_opp: + little_efficiency = _get_efficiency(opp, little_cpu) + big_efficiency = _get_efficiency(opp, big_cpu) + if little_efficiency > big_efficiency: + 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(opp)) + return (False, msg) + return (True, msg) \ No newline at end of file -- GitLab From 8d003a381eba36882888f6f82c49a8007a4d7f87 Mon Sep 17 00:00:00 2001 From: Elieva Pignat Date: Fri, 2 Mar 2018 11:15:48 +0000 Subject: [PATCH 2/4] preliminary.py: add sanity check of the target energy model This patch add uses the sanity check of the energy model function to control that the basic requirements of the energy model given in the target are met. Signed-off-by: Elieva Pignat --- tests/eas/preliminary.py | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/eas/preliminary.py b/tests/eas/preliminary.py index 2aa6d6099..8bfcdda4c 100644 --- a/tests/eas/preliminary.py +++ b/tests/eas/preliminary.py @@ -20,6 +20,8 @@ import time import re import pandas import StringIO +import logging +import tests.utils.em as em from unittest import SkipTest @@ -188,6 +190,66 @@ class TestEnergyModelPresent(BasicCheckTest): '- Kernel built without (CONFIG_SCHED_DEBUG && CONFIG_SYSCTL)\n' '- No energy model in kernel') +class TestEnergyModelSanity(BasicCheckTest): + @classmethod + def setUp(cls): + if not cls.env.nrg_model: + try: + cls.env.nrg_model = EnergyModel.from_target(cls.env.target) + except Exception as e: + raise SkipTest( + 'This test requires an EnergyModel for the platform. ' + 'Either provide one manually or ensure it can be read ' + 'from the filesystem: {}'.format(e)) + + def test_is_active_state_coherent(self): + """Test coherency of the active states""" + (succeed, msg) = em.is_power_increasing(self.env.nrg_model) + self.assertTrue(succeed, msg) + + (succeed, msg) = em.is_efficiency_decreasing(self.env.nrg_model) + self.assertTrue(succeed, msg) + + def test_nb_active_states(self): + """Test the number of active states for each group of cpus""" + freqs = [] + for cluster in self.env.nrg_model.root.children: + cpu = cluster.children[0] + freqs.append(len(self.target.cpufreq.list_frequencies(cpu.cpus[0]))) + (succeed, msg) = em.check_active_states_nb(self.env.nrg_model, freqs) + self.assertTrue(succeed, msg) + + def test_get_opp_in_overutilized(self): + """Get opp that are in overutilized zone""" + opp_overutilized = em.get_opp_overutilized(self.env.nrg_model) + msg = "\n" + for i, opp in enumerate(opp_overutilized): + msg += "\tGroup {}: {}\n".format(i, opp[1]) + logging.info(msg) + + def test_get_avg_opp_per_group(self): + """ + Get average workload that can be run on each cpus group + """ + avg_load = em.get_avg_cap(self.env.nrg_model) + msg = "\n" + for i, load in enumerate(avg_load): + msg += "\tGroup {}: {}\n".format(i, load) + logging.info(msg) + + def test_check_overutilized_area(self): + """ + Compare the opp in overutilized zone of the little cpu to the + corresponding opp of the big cpus + """ + (succeed, msg) = em.check_overutilized_area(self.env.nrg_model) + self.assertTrue(succeed, msg) + + def test_ideal_placements(self): + """Test placement of simple workloads""" + (succeed, msg) = em.ideal_placements(self.env.nrg_model) + self.assertTrue(succeed, msg) + class TestSchedutilTunables(BasicCheckTest): MAX_RATE_LIMIT_US = 20 * 1e3 -- GitLab From aadd994a9125cc87947d32b028b57eef13409948 Mon Sep 17 00:00:00 2001 From: Elieva Pignat Date: Fri, 2 Mar 2018 11:36:08 +0000 Subject: [PATCH 3/4] Add platform energy model for hikey960 Signed-off-by: Elieva Pignat --- libs/utils/platforms/hikey960_energy.py | 115 ++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 libs/utils/platforms/hikey960_energy.py diff --git a/libs/utils/platforms/hikey960_energy.py b/libs/utils/platforms/hikey960_energy.py new file mode 100644 index 000000000..9ec2c850d --- /dev/null +++ b/libs/utils/platforms/hikey960_energy.py @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2016, 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) + +a72_cluster_active_states = OrderedDict([ + (903000, ActiveState(power=102)), + (1421000, ActiveState(power=124)), + (1805000, ActiveState(power=221)), + (2112000, ActiveState(power=330)), + (2362000, ActiveState(power=433)), +]) + +a72_cluster_idle_states = OrderedDict([ + ("WFI", 102), + ("cpu-sleep-0", 102), + ("cluster-sleep-0", 0), +]) + +a72_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)), +]) + +a72_cpu_idle_states = OrderedDict([ + ("WFI", 18), + ("cpu-sleep-0", 0), + ("cluster-sleep-0", 0), +]) + +a72s = [4, 5, 6, 7] + +def a72_cpu_node(cpu): + return EnergyModelNode(cpu=cpu, + active_states=a72_cpu_active_states, + idle_states=a72_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_a72", + active_states=a72_cluster_active_states, + idle_states=a72_cluster_idle_states, + children=[a72_cpu_node(c) for c in a72s])]), + 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 a72s]), + PowerDomain( + idle_states=["cluster-sleep-0"], + children=[PowerDomain(idle_states=["WFI", "cpu-sleep-0"], cpu=c) + for c in a53s])]), + freq_domains=[a53s, a72s]) -- GitLab From df8460279643666ad99c816bc37ad1be2491598c Mon Sep 17 00:00:00 2001 From: Elieva Pignat Date: Fri, 2 Mar 2018 11:40:31 +0000 Subject: [PATCH 4/4] Test energy model: static energy model check This patch adds static check of the energy model presents in the utils/platforms directory. Signed-off-by: Elieva Pignat --- tests/lisa/test_energy_model.py | 82 +++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/tests/lisa/test_energy_model.py b/tests/lisa/test_energy_model.py index bfb9c79fa..af382198e 100644 --- a/tests/lisa/test_energy_model.py +++ b/tests/lisa/test_energy_model.py @@ -14,7 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # - +import logging +import tests.utils.em as em_utils from collections import OrderedDict from unittest import TestCase import os @@ -25,10 +26,10 @@ from energy_model import (EnergyModel, ActiveState, EnergyModelCapacityError, EnergyModelNode, EnergyModelRoot, PowerDomain) 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 +from libs.utils.platforms.juno_r0_energy import juno_r0_energy +from libs.utils.platforms.pixel_energy import pixel_energy +from libs.utils.platforms.hikey_energy import hikey_energy +from libs.utils.platforms.hikey960_energy import hikey960_energy """ A very basic test suite for the EnergyModel class.""" @@ -423,3 +424,74 @@ class TestEstimateFromTrace(TestCase): row = df.iloc[i] self.assertAlmostEqual(row.name, exp_index, places=4) self.assertDictEqual(row.to_dict(), exp_values) + +class TestEnergyModelSanityCheck(TestCase): + energy_model = [('juno_r0', juno_r0_energy), + ('hikey', hikey_energy), + ('hikey960', hikey960_energy), + ('pixel',pixel_energy)] + def test_is_active_state_coherent(self): + """Test coherency of the active states""" + tests_report = '' + tests_succeed = True + for (name, model) in self.energy_model: + (succeed, msg) = em_utils.is_power_increasing(model) + if not succeed: + tests_succeed = False + tests_report += 'In platform {}: {}\n\n\t\t'.format(name, msg) + + (succeed, msg) = em_utils.is_efficiency_decreasing(model) + if not succeed: + tests_succeed = False + tests_report += 'In platform {}: {}\n\n\t\t'.format(name, msg) + + self.assertTrue(tests_succeed, tests_report) + + + def test_get_opp_in_overutilized(self): + """Get opp that are in overutilized zone""" + for (name, model) in self.energy_model: + opp_overutilized = em_utils.get_opp_overutilized(model) + msg = "\n" + for i, opp in enumerate(opp_overutilized): + msg += "\tGroup {}: {}\n".format(i, opp[1]) + logging.info('\nFor platform {}: {}'.format(name, msg)) + + def test_get_avg_opp_per_group(self): + """ + Get average workload that can be run on each cpus group + """ + for (name, model) in self.energy_model: + avg_load = em_utils.get_avg_cap(model) + msg = "\n" + for i, load in enumerate(avg_load): + msg += "\tGroup {}: {}\n".format(i, load) + logging.info('\nFor platform {}: {}'.format(name, msg)) + + def test_check_overutilized_area(self): + """ + Compare the opp in overutilized zone of the little cpu to the + corresponding opp of the big cpus + """ + tests_report = '' + tests_succeed = True + for (name, model) in self.energy_model: + (succeed, msg) = em_utils.check_overutilized_area(model) + if not succeed: + tests_succeed = False + tests_report += 'In platform {}: {}\n\n\t\t'.format(name, msg) + + self.assertTrue(tests_succeed, tests_report) + + def test_ideal_placements(self): + """Test placement of simple workloads""" + tests_report = '' + tests_succeed = True + for (name, model) in self.energy_model: + (succeed, msg) = em_utils.ideal_placements(model) + if not succeed: + tests_succeed = False + tests_report += 'In platform {}: {}\n\n\t\t'.format(name, msg) + + self.assertTrue(tests_succeed, tests_report) + -- GitLab