• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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