1#!/usr/bin/env python 2# SPDX-License-Identifier: Apache-2.0 3# 4# Copyright (C) 2017, ARM Limited, Google, and contributors. 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); you may 7# not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18 19from __future__ import division 20import os 21import re 22import json 23import glob 24import argparse 25import pandas as pd 26import numpy as np 27 28from power_average import PowerAverage 29 30# This script computes the cluster power cost and cpu power costs at each 31# frequency for each cluster. The output can be used in power models or power 32# profiles. 33 34def average(values): 35 return sum(values) / len(values) 36 37class SampleReader: 38 def __init__(self, results_dir, column): 39 self.results_dir = results_dir 40 self.column = column 41 42 def get(self, filename): 43 files = glob.glob(os.path.join(self.results_dir, filename)) 44 if len(files) != 1: 45 raise ValueError('Multiple files match pattern') 46 return PowerAverage.get(files[0], self.column) 47 48class Cpu: 49 def __init__(self, platform_file, sample_reader): 50 self.platform_file = platform_file 51 self.sample_reader = sample_reader 52 # This is the additional cost when any cluster is on. It is seperate from 53 # the cluster cost because it is not duplicated when a second cluster 54 # turns on. 55 self.active_cost = -1.0 56 57 # Read in the cluster and frequency information from the plaform.json 58 with open(platform_file, 'r') as f: 59 platform = json.load(f) 60 self.clusters = {i : Cluster(self.sample_reader, i, platform["clusters"][i], 61 platform["freqs"][i]) for i in sorted(platform["clusters"])} 62 63 if len(self.clusters) != 2: 64 raise ValueError('Only cpus with 2 clusters are supported') 65 66 self.compute_costs() 67 68 def compute_costs(self): 69 # Compute initial core costs by freq. These are necessary for computing the 70 # cluster and active costs. However, since the cluster and active costs are computed 71 # using averages across all cores and frequencies, we will need to adjust the 72 # core cost at the end. 73 # 74 # For example: The total cpu cost of core 0 on cluster 0 running at 75 # a given frequency is 25. We initally compute the core cost as 10. 76 # However the active and cluster averages end up as 9 and 3. 10 + 9 + 3 is 77 # 22 not 25. We can adjust the core cost 13 to cover this error. 78 for cluster in self.clusters: 79 self.clusters[cluster].compute_initial_core_costs() 80 81 # Compute the cluster costs 82 cluster0 = self.clusters.values()[0] 83 cluster1 = self.clusters.values()[1] 84 cluster0.compute_cluster_cost(cluster1) 85 cluster1.compute_cluster_cost(cluster0) 86 87 # Compute the active cost as an average of computed active costs by cluster 88 self.active_cost = average([self.clusters[cluster].compute_active_cost() for cluster in self.clusters]) 89 90 # Compute final core costs. This will help correct for any errors introduced 91 # by the averaging of the cluster and active costs. 92 for cluster in self.clusters: 93 self.clusters[cluster].compute_final_core_costs(self.active_cost) 94 95 def get_clusters(self): 96 with open(self.platform_file, 'r') as f: 97 platform = json.load(f) 98 return platform["clusters"] 99 100 def get_active_cost(self): 101 return self.active_cost 102 103 def get_cluster_cost(self, cluster): 104 return self.clusters[cluster].get_cluster_cost() 105 106 def get_cores(self, cluster): 107 return self.clusters[cluster].get_cores() 108 109 def get_core_freqs(self, cluster): 110 return self.clusters[cluster].get_freqs() 111 112 def get_core_cost(self, cluster, freq): 113 return self.clusters[cluster].get_core_cost(freq) 114 115 def dump(self): 116 print 'Active cost: {}'.format(self.active_cost) 117 for cluster in self.clusters: 118 self.clusters[cluster].dump() 119 120class Cluster: 121 def __init__(self, sample_reader, handle, cores, freqs): 122 self.sample_reader = sample_reader 123 self.handle = handle 124 self.cores = cores 125 self.cluster_cost = -1.0 126 self.core_costs = {freq:-1.0 for freq in freqs} 127 128 def compute_initial_core_costs(self): 129 # For every frequency, freq 130 for freq, _ in self.core_costs.iteritems(): 131 total_costs = [] 132 core_costs = [] 133 134 # Store the total cost for turning on 1 to len(cores) on the 135 # cluster at freq 136 for cnt in range(1, len(self.cores)+1): 137 total_costs.append(self.get_sample_avg(cnt, freq)) 138 139 # Compute the additional power cost of turning on another core at freq. 140 for i in range(len(total_costs)-1): 141 core_costs.append(total_costs[i+1] - total_costs[i]) 142 143 # The initial core cost is the average of the additional power to add 144 # a core at freq 145 self.core_costs[freq] = average(core_costs) 146 147 def compute_final_core_costs(self, active_cost): 148 # For every frequency, freq 149 for freq, _ in self.core_costs.iteritems(): 150 total_costs = [] 151 core_costs = [] 152 153 # Store the total cost for turning on 1 to len(cores) on the 154 # cluster at freq 155 for core_cnt in range(1, len(self.cores)+1): 156 total_costs.append(self.get_sample_avg(core_cnt, freq)) 157 158 # Recompute the core cost as the sample average minus the cluster and 159 # active costs divided by the number of cores on. This will help 160 # correct for any error introduced by averaging the cluster and 161 # active costs. 162 for i, total_cost in enumerate(total_costs): 163 core_cnt = i + 1 164 core_costs.append((total_cost - self.cluster_cost - active_cost) / (core_cnt)) 165 166 # The final core cost is the average of the core costs at freq 167 self.core_costs[freq] = average(core_costs) 168 169 def compute_cluster_cost(self, other_cluster=None): 170 # Create a template for the file name. For each frequency we will be able 171 # to easily substitute it into the file name. 172 template = '{}_samples.csv'.format('_'.join(sorted( 173 ['cluster{}-cores?-freq{{}}'.format(self.handle), 174 'cluster{}-cores?-freq{}'.format(other_cluster.get_handle(), 175 other_cluster.get_min_freq())]))) 176 177 # Get the cost of running a single cpu at min frequency on the other cluster 178 cluster_costs = [] 179 other_cluster_total_cost = other_cluster.get_sample_avg(1, other_cluster.get_min_freq()) 180 181 # For every frequency 182 for freq, core_cost in self.core_costs.iteritems(): 183 # Get the cost of running a single core on this cluster at freq and 184 # a single core on the other cluster at min frequency 185 total_cost = self.sample_reader.get(template.format(freq)) 186 # Get the cluster cost by subtracting all the other costs from the 187 # total cost so that the only cost that remains is the cluster cost 188 # of this cluster 189 cluster_costs.append(total_cost - core_cost - other_cluster_total_cost) 190 191 # Return the average calculated cluster cost 192 self.cluster_cost = average(cluster_costs) 193 194 def compute_active_cost(self): 195 active_costs = [] 196 197 # For every frequency 198 for freq, core_cost in self.core_costs.iteritems(): 199 # For every core 200 for i, core in enumerate(self.cores): 201 core_cnt = i + 1 202 # Subtract the core and cluster costs from each total cost. 203 # The remaining cost is the active cost 204 active_costs.append(self.get_sample_avg(core_cnt, freq) 205 - core_cost*core_cnt - self.cluster_cost) 206 207 # Return the average active cost 208 return average(active_costs) 209 210 def get_handle(self): 211 return self.handle 212 213 def get_min_freq(self): 214 return min(self.core_costs, key=self.core_costs.get) 215 216 def get_sample_avg(self, core_cnt, freq): 217 core_str = ''.join('{}-'.format(self.cores[i]) for i in range(core_cnt)) 218 filename = 'cluster{}-cores{}freq{}_samples.csv'.format(self.handle, core_str, freq) 219 return self.sample_reader.get(filename) 220 221 def get_cluster_cost(self): 222 return self.cluster_cost 223 224 def get_cores(self): 225 return self.cores 226 227 def get_freqs(self): 228 return self.core_costs.keys() 229 230 def get_core_cost(self, freq): 231 return self.core_costs[freq] 232 233 def dump(self): 234 print 'Cluster {} cost: {}'.format(self.handle, self.cluster_cost) 235 for freq in sorted(self.core_costs): 236 print '\tfreq {} cost: {}'.format(freq, self.core_costs[freq]) 237 238class CpuFrequencyPowerAverage: 239 @staticmethod 240 def get(results_dir, platform_file, column): 241 sample_reader = SampleReader(results_dir, column) 242 cpu = Cpu(platform_file, sample_reader) 243 return cpu 244 245 246parser = argparse.ArgumentParser( 247 description="Get the cluster cost and cpu cost per frequency. Optionally" 248 " specify a time interval over which to calculate the sample.") 249 250parser.add_argument("--column", "-c", type=str, required=True, 251 help="The name of the column in the samples.csv's that" 252 " contain the power values to average.") 253 254parser.add_argument("--results_dir", "-d", type=str, 255 default=os.path.join(os.environ["LISA_HOME"], 256 "results/CpuFrequency_default"), 257 help="The results directory to read from. (default" 258 " LISA_HOME/results/CpuFrequency_default)") 259 260parser.add_argument("--platform_file", "-p", type=str, 261 default=os.path.join(os.environ["LISA_HOME"], 262 "results/CpuFrequency/platform.json"), 263 help="The results directory to read from. (default" 264 " LISA_HOME/results/CpuFrequency/platform.json)") 265 266if __name__ == "__main__": 267 args = parser.parse_args() 268 269 cpu = CpuFrequencyPowerAverage.get(args.results_dir, args.platform_file, args.column) 270 cpu.dump() 271