diff --git a/libs/utils/platforms/hikey960_energy.py b/libs/utils/platforms/hikey960_energy.py new file mode 100644 index 0000000000000000000000000000000000000000..9ec2c850d05b05cb554365af92b6e40be2d1ca50 --- /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]) diff --git a/tests/eas/preliminary.py b/tests/eas/preliminary.py index 2aa6d60994d98a6cba92ee4ba7829c09515a4212..8bfcdda4c06f717c026701648e08eb469b852a61 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 diff --git a/tests/lisa/test_energy_model.py b/tests/lisa/test_energy_model.py index bfb9c79fa6de216b83b56953ffe56454fbed93c0..af382198e5a9f4c872c4e77f31c7587b030f5fab 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) + diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/utils/em.py b/tests/utils/em.py new file mode 100644 index 0000000000000000000000000000000000000000..e6258f1552e2b7b06ae804ce6c854327efcffbc9 --- /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