#!/usr/bin/env python # SPDX-License-Identifier: Apache-2.0 # # Copyright (C) 2017, ARM Limited, Google, 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 __future__ import division import os import re import json import glob import argparse import pandas as pd import numpy as np from power_average import PowerAverage # This script computes the cluster power cost and cpu power costs at each # frequency for each cluster. The output can be used in power models or power # profiles. def average(values): return sum(values) / len(values) class SampleReader: def __init__(self, results_dir, column): self.results_dir = results_dir self.column = column def get(self, filename): files = glob.glob(os.path.join(self.results_dir, filename)) if len(files) != 1: raise ValueError('Multiple files match pattern') return PowerAverage.get(files[0], self.column) class Cpu: def __init__(self, platform_file, sample_reader): self.platform_file = platform_file self.sample_reader = sample_reader # This is the additional cost when any cluster is on. It is seperate from # the cluster cost because it is not duplicated when a second cluster # turns on. self.active_cost = -1.0 # Read in the cluster and frequency information from the plaform.json with open(platform_file, 'r') as f: platform = json.load(f) self.clusters = {i : Cluster(self.sample_reader, i, platform["clusters"][i], platform["freqs"][i]) for i in sorted(platform["clusters"])} if len(self.clusters) != 2: raise ValueError('Only cpus with 2 clusters are supported') self.compute_costs() def compute_costs(self): # Compute initial core costs by freq. These are necessary for computing the # cluster and active costs. However, since the cluster and active costs are computed # using averages across all cores and frequencies, we will need to adjust the # core cost at the end. # # For example: The total cpu cost of core 0 on cluster 0 running at # a given frequency is 25. We initally compute the core cost as 10. # However the active and cluster averages end up as 9 and 3. 10 + 9 + 3 is # 22 not 25. We can adjust the core cost 13 to cover this error. for cluster in self.clusters: self.clusters[cluster].compute_initial_core_costs() # Compute the cluster costs cluster0 = self.clusters.values()[0] cluster1 = self.clusters.values()[1] cluster0.compute_cluster_cost(cluster1) cluster1.compute_cluster_cost(cluster0) # Compute the active cost as an average of computed active costs by cluster self.active_cost = average([self.clusters[cluster].compute_active_cost() for cluster in self.clusters]) # Compute final core costs. This will help correct for any errors introduced # by the averaging of the cluster and active costs. for cluster in self.clusters: self.clusters[cluster].compute_final_core_costs(self.active_cost) def get_clusters(self): with open(self.platform_file, 'r') as f: platform = json.load(f) return platform["clusters"] def get_active_cost(self): return self.active_cost def get_cluster_cost(self, cluster): return self.clusters[cluster].get_cluster_cost() def get_cores(self, cluster): return self.clusters[cluster].get_cores() def get_core_freqs(self, cluster): return self.clusters[cluster].get_freqs() def get_core_cost(self, cluster, freq): return self.clusters[cluster].get_core_cost(freq) def dump(self): print 'Active cost: {}'.format(self.active_cost) for cluster in self.clusters: self.clusters[cluster].dump() class Cluster: def __init__(self, sample_reader, handle, cores, freqs): self.sample_reader = sample_reader self.handle = handle self.cores = cores self.cluster_cost = -1.0 self.core_costs = {freq:-1.0 for freq in freqs} def compute_initial_core_costs(self): # For every frequency, freq for freq, _ in self.core_costs.iteritems(): total_costs = [] core_costs = [] # Store the total cost for turning on 1 to len(cores) on the # cluster at freq for cnt in range(1, len(self.cores)+1): total_costs.append(self.get_sample_avg(cnt, freq)) # Compute the additional power cost of turning on another core at freq. for i in range(len(total_costs)-1): core_costs.append(total_costs[i+1] - total_costs[i]) # The initial core cost is the average of the additional power to add # a core at freq self.core_costs[freq] = average(core_costs) def compute_final_core_costs(self, active_cost): # For every frequency, freq for freq, _ in self.core_costs.iteritems(): total_costs = [] core_costs = [] # Store the total cost for turning on 1 to len(cores) on the # cluster at freq for core_cnt in range(1, len(self.cores)+1): total_costs.append(self.get_sample_avg(core_cnt, freq)) # Recompute the core cost as the sample average minus the cluster and # active costs divided by the number of cores on. This will help # correct for any error introduced by averaging the cluster and # active costs. for i, total_cost in enumerate(total_costs): core_cnt = i + 1 core_costs.append((total_cost - self.cluster_cost - active_cost) / (core_cnt)) # The final core cost is the average of the core costs at freq self.core_costs[freq] = average(core_costs) def compute_cluster_cost(self, other_cluster=None): # Create a template for the file name. For each frequency we will be able # to easily substitute it into the file name. template = '{}_samples.csv'.format('_'.join(sorted( ['cluster{}-cores?-freq{{}}'.format(self.handle), 'cluster{}-cores?-freq{}'.format(other_cluster.get_handle(), other_cluster.get_min_freq())]))) # Get the cost of running a single cpu at min frequency on the other cluster cluster_costs = [] other_cluster_total_cost = other_cluster.get_sample_avg(1, other_cluster.get_min_freq()) # For every frequency for freq, core_cost in self.core_costs.iteritems(): # Get the cost of running a single core on this cluster at freq and # a single core on the other cluster at min frequency total_cost = self.sample_reader.get(template.format(freq)) # Get the cluster cost by subtracting all the other costs from the # total cost so that the only cost that remains is the cluster cost # of this cluster cluster_costs.append(total_cost - core_cost - other_cluster_total_cost) # Return the average calculated cluster cost self.cluster_cost = average(cluster_costs) def compute_active_cost(self): active_costs = [] # For every frequency for freq, core_cost in self.core_costs.iteritems(): # For every core for i, core in enumerate(self.cores): core_cnt = i + 1 # Subtract the core and cluster costs from each total cost. # The remaining cost is the active cost active_costs.append(self.get_sample_avg(core_cnt, freq) - core_cost*core_cnt - self.cluster_cost) # Return the average active cost return average(active_costs) def get_handle(self): return self.handle def get_min_freq(self): return min(self.core_costs, key=self.core_costs.get) def get_sample_avg(self, core_cnt, freq): core_str = ''.join('{}-'.format(self.cores[i]) for i in range(core_cnt)) filename = 'cluster{}-cores{}freq{}_samples.csv'.format(self.handle, core_str, freq) return self.sample_reader.get(filename) def get_cluster_cost(self): return self.cluster_cost def get_cores(self): return self.cores def get_freqs(self): return self.core_costs.keys() def get_core_cost(self, freq): return self.core_costs[freq] def dump(self): print 'Cluster {} cost: {}'.format(self.handle, self.cluster_cost) for freq in sorted(self.core_costs): print '\tfreq {} cost: {}'.format(freq, self.core_costs[freq]) class CpuFrequencyPowerAverage: @staticmethod def get(results_dir, platform_file, column): sample_reader = SampleReader(results_dir, column) cpu = Cpu(platform_file, sample_reader) return cpu parser = argparse.ArgumentParser( description="Get the cluster cost and cpu cost per frequency. Optionally" " specify a time interval over which to calculate the sample.") parser.add_argument("--column", "-c", type=str, required=True, help="The name of the column in the samples.csv's that" " contain the power values to average.") parser.add_argument("--results_dir", "-d", type=str, default=os.path.join(os.environ["LISA_HOME"], "results/CpuFrequency_default"), help="The results directory to read from. (default" " LISA_HOME/results/CpuFrequency_default)") parser.add_argument("--platform_file", "-p", type=str, default=os.path.join(os.environ["LISA_HOME"], "results/CpuFrequency/platform.json"), help="The results directory to read from. (default" " LISA_HOME/results/CpuFrequency/platform.json)") if __name__ == "__main__": args = parser.parse_args() cpu = CpuFrequencyPowerAverage.get(args.results_dir, args.platform_file, args.column) cpu.dump()