예제 #1
0
    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
예제 #2
0
 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
예제 #3
0
    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
예제 #4
0
    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
예제 #5
0
    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
예제 #6
0
    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
예제 #7
0
    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