def test_task_placement(self, energy_est_threshold_pct=5, nrg_model: EnergyModel = None, capacity_margin_pct=20) -> ResultBundle: """ Test that task placement was energy-efficient :param nrg_model: Allow using an alternate EnergyModel instead of ``nrg_model``` :type nrg_model: EnergyModel :param energy_est_threshold_pct: Allowed margin for estimated vs optimal task placement energy cost :type energy_est_threshold_pct: int Compute optimal energy consumption (energy-optimal task placement) and compare to energy consumption estimated from the trace. Check that the estimated energy does not exceed the optimal energy by more than ``energy_est_threshold_pct``` percents. """ nrg_model = nrg_model or self.nrg_model exp_power = self._get_expected_power_df(nrg_model, capacity_margin_pct) est_power = self._get_estimated_power_df(nrg_model) exp_energy = series_integrate(exp_power.sum(axis=1), method='rect') est_energy = series_integrate(est_power.sum(axis=1), method='rect') msg = f'Estimated {est_energy} bogo-Joules to run workload, expected {exp_energy}' threshold = exp_energy * (1 + (energy_est_threshold_pct / 100)) passed = est_energy < threshold res = ResultBundle.from_bool(passed) res.add_metric("estimated energy", est_energy, 'bogo-joules') res.add_metric("energy threshold", threshold, 'bogo-joules') return res
def _compute_energy(df): channels_nrg = {} for site, measure in df: if measure == 'power': channels_nrg[site] = series_integrate(df[site]['power'], method='trapz') return channels_nrg
def get_trace_cpu_util(self): """ Get the per-phase average CPU utilization read from the trace :returns: A dict of the shape {cpu : {phase_id : trace_util}} """ cpu_util = { cpu: {phase_id: 0 for phase_id in range(self.nr_phases)} for cpu in self.cpus } df = self.trace.analysis.load_tracking.df_cpus_signal('util') phase_start = self.trace.start for phase in range(self.nr_phases): # Start looking at signals once they should've converged start = phase_start + UTIL_AVG_CONVERGENCE_TIME_S # Trim the end a bit, otherwise we could have one or two events # from the next phase end = phase_start + self.phases_durations[phase] * .9 phase_df = df[start:end] phase_duration = end - start for cpu in self.cpus: util = phase_df[phase_df.cpu == cpu].util cpu_util[cpu][phase] = series_integrate(util) / ( phase_duration) phase_start += self.phases_durations[phase] return cpu_util
def _get_frequency_residency(self, cpus): """ Get a DataFrame with per cluster frequency residency, i.e. amount of time spent at a given frequency in each cluster. :param cpus: A tuple of CPU IDs :type cpus: tuple(int) :returns: A :class:`pandas.DataFrame` with: * A ``total_time`` column (the total time spent at a frequency) * A ``active_time`` column (the non-idle time spent at a frequency) """ freq_df = self.df_cpus_frequency() # Assumption: all CPUs in a cluster run at the same frequency, i.e. the # frequency is scaled per-cluster not per-CPU. Hence, we can limit the # cluster frequencies data to a single CPU. self._check_freq_domain_coherency(cpus) cluster_freqs = freq_df[freq_df.cpu == cpus[0]] # Compute TOTAL Time cluster_freqs = df_add_delta(cluster_freqs, col="total_time", window=self.trace.window) time_df = cluster_freqs[["total_time", "frequency"]].groupby('frequency', observed=True, sort=False).sum() # Compute ACTIVE Time cluster_active = self.trace.analysis.idle.signal_cluster_active(cpus) # In order to compute the active time spent at each frequency we # multiply 2 square waves: # - cluster_active, a square wave of the form: # cluster_active[t] == 1 if at least one CPU is reported to be # non-idle by CPUFreq at time t # cluster_active[t] == 0 otherwise # - freq_active, square wave of the form: # freq_active[t] == 1 if at time t the frequency is f # freq_active[t] == 0 otherwise available_freqs = sorted(cluster_freqs.frequency.unique()) cluster_freqs = cluster_freqs.join( cluster_active.to_frame(name='active'), how='outer') cluster_freqs.fillna(method='ffill', inplace=True) nonidle_time = [] for freq in available_freqs: freq_active = cluster_freqs.frequency.apply(lambda x: 1 if x == freq else 0) active_t = cluster_freqs.active * freq_active # Compute total time by integrating the square wave nonidle_time.append(series_integrate(active_t)) time_df["active_time"] = pd.DataFrame(index=available_freqs, data=nonidle_time) return time_df
def df_cluster_idle_state_residency(self, cluster): """ Compute time spent by a given cluster in each idle state. :param cluster: list of CPU IDs :type cluster: list(int) :returns: a :class:`pandas.DataFrame` with: * Idle states as index * A ``time`` column (The time spent in the idle state) """ idle_df = self.trace.df_events('cpu_idle') # Each core in a cluster can be in a different idle state, but the # cluster lies in the idle state with lowest ID, that is the shallowest # idle state among the idle states of its CPUs cl_idle = idle_df[idle_df.cpu_id == cluster[0]].state.to_frame( name=cluster[0]) for cpu in cluster[1:]: cl_idle = cl_idle.join( idle_df[idle_df.cpu_id == cpu].state.to_frame(name=cpu), how='outer') cl_idle.fillna(method='ffill', inplace=True) cl_idle = pd.DataFrame(cl_idle.min(axis=1), columns=['state']) # Build a square wave of the form: # cl_is_idle[t] == 1 if all CPUs in the cluster are reported # to be idle by cpufreq at time t # cl_is_idle[t] == 0 otherwise cl_is_idle = self.signal_cluster_active(cluster) ^ 1 # In order to compute the time spent in each idle state frequency we # multiply 2 square waves: # - cluster_is_idle # - idle_state, square wave of the form: # idle_state[t] == 1 if at time t cluster is in idle state i # idle_state[t] == 0 otherwise available_idles = sorted(idle_df.state.unique()) # Remove non-idle state from availables available_idles = available_idles[1:] cl_idle = cl_idle.join(cl_is_idle.to_frame(name='is_idle'), how='outer') cl_idle.fillna(method='ffill', inplace=True) idle_time = [] for i in available_idles: idle_state = cl_idle.state.apply(lambda x: 1 if x == i else 0) idle_t = cl_idle.is_idle * idle_state # Compute total time by integrating the square wave idle_time.append(series_integrate(idle_t)) idle_time_df = pd.DataFrame({'time': idle_time}, index=available_idles) idle_time_df.index.name = 'idle_state' return idle_time_df
def df_cpu_idle_state_residency(self, cpu): """ Compute time spent by a given CPU in each idle state. :param cpu: CPU ID :type cpu: int :returns: a :class:`pandas.DataFrame` with: * Idle states as index * A ``time`` column (The time spent in the idle state) """ idle_df = self.trace.df_events('cpu_idle') cpu_idle = idle_df[idle_df.cpu_id == cpu] cpu_is_idle = self.signal_cpu_active(cpu) ^ 1 # In order to compute the time spent in each idle state we # multiply 2 square waves: # - cpu_idle # - idle_state, square wave of the form: # idle_state[t] == 1 if at time t CPU is in idle state i # idle_state[t] == 0 otherwise available_idles = sorted(idle_df.state.unique()) # Remove non-idle state from availables available_idles = available_idles[1:] cpu_idle = cpu_idle.join(cpu_is_idle.to_frame(name='is_idle'), how='outer') cpu_idle.fillna(method='ffill', inplace=True) # Extend the last cpu_idle event to the end of the time window under # consideration final_entry = pd.DataFrame([cpu_idle.iloc[-1]], index=[self.trace.end]) cpu_idle = cpu_idle.append(final_entry) idle_time = [] for i in available_idles: idle_state = cpu_idle.state.apply(lambda x: 1 if x == i else 0) idle_t = cpu_idle.is_idle * idle_state # Compute total time by integrating the square wave idle_time.append(series_integrate(idle_t)) idle_time_df = pd.DataFrame({'time': idle_time}, index=available_idles) idle_time_df.index.name = 'idle_state' return idle_time_df
def test_areas(self) -> ResultBundle: """ Test signals are properly "dominated". The integral of `util_est_enqueued` is expected to be always not smaller than that of `util_avg`, since this last is subject to decays while the first not. The integral of `util_est_enqueued` is expected to be always greater or equal than the integral of `util_avg`, since this `util_avg` is subject to decays while `util_est_enqueued` not. On fast-ramp systems, the `util_est_ewma` signal is never smaller then the `util_est_enqueued`, thus his integral is expected to be bigger. On non fast-ramp systems instead, the `util_est_ewma` is expected to be smaller then `util_est_enqueued` in ramp-up phases, or bigger in ramp-down phases. Those conditions are checked on a single execution of a task which has three main behaviours: * STABLE: periodic big task running for a relatively long period to ensure `util_avg` saturation. * DOWN: periodic ramp-down task, to slowly decay `util_avg` * UP: periodic ramp-up task, to slowly increase `util_avg` """ failure_reasons = {} metrics = {} # We have only two task: the main 'rt-app' task and our 'test_task' test_task = self.trace.analysis.rta.rtapp_tasks[-1] ue_df = self.trace.df_events('sched_util_est_task') ue_df = df_filter_task_ids(ue_df, [test_task]) ua_df = self.trace.analysis.load_tracking.df_tasks_signal('util') ua_df = df_filter_task_ids(ua_df, [test_task]) failures = [] for phase in self.trace.analysis.rta.task_phase_windows(test_task): phase_df = ue_df[phase.start:phase.end] area_enqueued = series_integrate(phase_df.util_est_enqueued) area_ewma = series_integrate(phase_df.util_est_ewma) phase_df = ua_df[phase.start:phase.end] area_util = series_integrate(phase_df.util) metrics[phase.id] = PhaseStats(phase.start, phase.end, area_util, area_enqueued, area_ewma) phase_name = "phase {}".format(phase.id) if area_enqueued < area_util: failure_reasons[ phase_name] = 'Enqueued smaller then Util Average' failures.append(phase.start) continue # Running on FastRamp kernels: if self.fast_ramp: # STABLE, DOWN and UP: if area_ewma < area_enqueued: failure_reasons[ phase_name] = 'NO_FAST_RAMP: EWMA smaller then Enqueued' failures.append(phase.start) continue # Running on (legacy) non FastRamp kernels: else: # STABLE: ewma ramping up if phase.id == 0 and area_ewma > area_enqueued: failure_reasons[ phase_name] = 'FAST_RAMP(STABLE): EWMA bigger then Enqueued' failures.append(phase.start) continue # DOWN: ewma ramping down if 0 < phase.id < 5 and area_ewma < area_enqueued: failure_reasons[ phase_name] = 'FAST_RAMP(DOWN): EWMA smaller then Enqueued' failures.append(phase.start) continue # UP: ewma ramping up if phase.id > 4 and area_ewma > area_enqueued: failure_reasons[ phase_name] = 'FAST_RAMP(UP): EWMA bigger then Enqueued' failures.append(phase.start) continue bundle = ResultBundle.from_bool(failure_reasons) bundle.add_metric("fast ramp", self.fast_ramp) bundle.add_metric("phases stats", metrics) if not failure_reasons: return bundle # Plot signals to support debugging analysis self._plot_signals(test_task, 'areas', failures) bundle.add_metric("failure reasons", failure_reasons) return bundle