Source code for spinn_front_end_common.interface.interface_functions.compute_energy_used

# Copyright (c) 2017 The University of Manchester
#
# 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
#
#     https://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.

import itertools
from spinn_utilities.config_holder import (get_config_int, get_config_str)
from spinn_front_end_common.data import FecDataView
from spinn_front_end_common.interface.provenance import (
    GlobalProvenance, ProvenanceReader, TimerCategory, TimerWork)
from spinn_front_end_common.utilities.utility_objs import PowerUsed
from spinn_front_end_common.utility_models import (
    ChipPowerMonitorMachineVertex)

#: milliseconds per second
_MS_PER_SECOND = 1000.0

#: given from Indar's measurements
MILLIWATTS_PER_FPGA = 0.000584635

#: stated in papers (SpiNNaker: A 1-W 18 core system-on-Chip for
#: Massively-Parallel Neural Network Simulation)
JOULES_PER_SPIKE = 0.000000000800

#: stated in papers (SpiNNaker: A 1-W 18 core system-on-Chip for
#: Massively-Parallel Neural Network Simulation)
MILLIWATTS_PER_IDLE_CHIP = 0.000360

#: stated in papers (SpiNNaker: A 1-W 18 core system-on-Chip for
#: Massively-Parallel Neural Network Simulation)
MILLIWATTS_PER_CHIP_ACTIVE_OVERHEAD = (0.001 - MILLIWATTS_PER_IDLE_CHIP)

#: measured from the real power meter and timing between
#: the photos for a days powered off
MILLIWATTS_FOR_FRAME_IDLE_COST = 0.117

#: measured from the loading of the column and extrapolated
MILLIWATTS_PER_FRAME_ACTIVE_COST = 0.154163558

#: measured from the real power meter and timing between the photos
#: for a day powered off
MILLIWATTS_FOR_BOXED_48_CHIP_FRAME_IDLE_COST = 0.0045833333

# TODO needs filling in
MILLIWATTS_PER_UNBOXED_48_CHIP_FRAME_IDLE_COST = 0.01666667

# TODO verify this is correct when doing multiboard comms
N_MONITORS_ACTIVE_DURING_COMMS = 2


[docs]def compute_energy_used(): """ This algorithm does the actual work of computing energy used by a simulation (or other application) running on SpiNNaker. :rtype: PowerUsed """ machine = FecDataView.get_machine() with GlobalProvenance() as db: dsg_time = db.get_category_timer_sum(TimerCategory.DATA_GENERATION) execute_time = db.get_category_timer_sum(TimerCategory.RUN_LOOP) # NOTE: this extraction time is part of the execution time; it does not # refer to the time taken in e.g. pop.get_data() or projection.get() extraction_time = db.get_timer_sum_by_work(TimerWork.EXTRACT_DATA) load_time = db.get_category_timer_sum(TimerCategory.LOADING) mapping_time = db.get_category_timer_sum(TimerCategory.MAPPING) # TODO get_machine not include here power_used = PowerUsed() power_used.num_chips = machine.n_chips # One extra per chip for SCAMP power_used.num_cores = FecDataView.get_n_placements() + machine.n_chips power_used.exec_time_secs = execute_time / _MS_PER_SECOND power_used.loading_time_secs = load_time / _MS_PER_SECOND # extraction_time could be None if nothing is set to be recorded total_extraction_time = 0 if extraction_time is not None: total_extraction_time += extraction_time power_used.saving_time_secs = total_extraction_time / _MS_PER_SECOND power_used.data_gen_time_secs = dsg_time / _MS_PER_SECOND power_used.mapping_time_secs = mapping_time / _MS_PER_SECOND runtime_total_ms = ( FecDataView.get_current_run_timesteps() * FecDataView.get_time_scale_factor()) # TODO: extraction time as currently defined is part of execution time, # so for now don't add it on, but revisit this in the future total_booted_time = execute_time + load_time _compute_energy_consumption( machine, dsg_time, load_time, mapping_time, total_booted_time, runtime_total_ms, power_used) return power_used
def _compute_energy_consumption( machine, dsg_time, load_time, mapping_time, total_booted_time, runtime_total_ms, power_used): """ :param ~.Machine machine: :param float dsg_time: :param float load_time: :param float mapping_time: :param float total_booted_time: :param float runtime_total_ms: :param PowerUsed power_used: """ # figure active chips monitor_placements = __find_monitor_placements() # figure out packet cost _router_packet_energy(power_used) # figure FPGA cost over all booted and during runtime cost _calculate_fpga_energy( machine, total_booted_time, runtime_total_ms, power_used) # figure how many frames are using, as this is a constant cost of # routers, cooling etc power_used.num_frames = _calculate_n_frames(machine) # figure load time cost power_used.loading_joules = _calculate_loading_energy( machine, load_time, len(monitor_placements), power_used.num_frames) # figure the down time idle cost for mapping power_used.mapping_joules = _calculate_power_down_energy( mapping_time, machine, power_used.num_frames) # figure the down time idle cost for DSG power_used.data_gen_joules = _calculate_power_down_energy( dsg_time, machine, power_used.num_frames) # figure extraction time cost power_used.saving_joules = _calculate_data_extraction_energy( machine, len(monitor_placements), power_used.num_frames) # figure out active chips cost power_used.chip_energy_joules = sum( _calculate_chips_active_energy( placement, runtime_total_ms, power_used) for placement in monitor_placements) # figure out cooling/internet router idle cost during runtime power_used.baseline_joules = ( runtime_total_ms * power_used.num_frames * MILLIWATTS_FOR_FRAME_IDLE_COST) def __find_monitor_placements(): return [placement for placement in FecDataView.iterate_placements_by_vertex_type( ChipPowerMonitorMachineVertex)] _COST_PER_TYPE = { "Local_Multicast_Packets": JOULES_PER_SPIKE, "External_Multicast_Packets": JOULES_PER_SPIKE, "Reinjected": JOULES_PER_SPIKE, "Local_P2P_Packets": JOULES_PER_SPIKE * 2, "External_P2P_Packets": JOULES_PER_SPIKE * 2, "Local_NN_Packets": JOULES_PER_SPIKE, "External_NN_Packets": JOULES_PER_SPIKE, "Local_FR_Packets": JOULES_PER_SPIKE * 2, "External_FR_Packets": JOULES_PER_SPIKE * 2 } def _router_packet_energy(power_used): """ :param PowerUsed power_used: """ energy_cost = 0.0 with ProvenanceReader() as db: for name, cost in _COST_PER_TYPE.items(): data = db.get_router_by_chip(name) for (x, y, value) in data: this_cost = value * cost energy_cost += this_cost if this_cost: power_used.add_router_active_energy(x, y, this_cost) power_used.packet_joules = energy_cost def _calculate_chips_active_energy(placement, runtime_total_ms, power_used): """ Figure out the chip active cost during simulation. :param ~.Placement placement: placement :param float runtime_total_ms: :param PowerUsed power_used: :return: energy cost """ # pylint: disable=too-many-arguments # locate chip power monitor chip_power_monitor = placement.vertex # get recordings from the chip power monitor recorded_measurements = chip_power_monitor.get_recorded_data( placement=placement) # deduce time in milliseconds per recording element n_samples_per_recording = get_config_int( "EnergyMonitor", "n_samples_per_recording_entry") time_for_recorded_sample = ( chip_power_monitor.sampling_frequency * n_samples_per_recording) / 1000 cores_power_cost = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # accumulate costs for recorded_measurement in recorded_measurements: for core in range(0, 18): cores_power_cost[core] += ( recorded_measurement[core] * time_for_recorded_sample * MILLIWATTS_PER_CHIP_ACTIVE_OVERHEAD / 18) # detailed report print out for core in range(0, 18): power_used.add_core_active_energy( placement.x, placement.y, core, cores_power_cost[core]) # TAKE INTO ACCOUNT IDLE COST idle_cost = runtime_total_ms * MILLIWATTS_PER_IDLE_CHIP return sum(cores_power_cost) + idle_cost def _calculate_fpga_energy( machine, total_runtime, runtime_total_ms, power_used): """ :param ~.Machine machine: :param float total_runtime: :param float runtime_total_ms: :param PowerUsed power_used: """ # pylint: disable=too-many-arguments total_fpgas = 0 # if not spalloc, then could be any type of board if (not get_config_str("Machine", "spalloc_server") and not get_config_str("Machine", "remote_spinnaker_url")): # if a spinn2 or spinn3 (4 chip boards) then they have no fpgas if machine.n_chips <= 4: return 0, 0 # if the spinn4 or spinn5 board, need to verify if wrap-arounds # are there, if not then assume fpgas are turned off. # how many fpgas are active total_fpgas = __board_n_operational_fpgas( machine.ethernet_connected_chips[0]) # active fpgas if total_fpgas == 0: return 0, 0 else: # spalloc machine, need to check each board for ethernet_connected_chip in machine.ethernet_connected_chips: total_fpgas += __board_n_operational_fpgas( ethernet_connected_chip) # Only need to update this here now that we've learned there are FPGAs # in use power_used.num_fpgas = total_fpgas power_usage_total = ( total_runtime * MILLIWATTS_PER_FPGA * total_fpgas) power_usage_runtime = ( runtime_total_ms * MILLIWATTS_PER_FPGA * total_fpgas) power_used.fpga_total_energy_joules = power_usage_total power_used.fpga_exec_energy_joules = power_usage_runtime def __board_n_operational_fpgas(ethernet_chip): """ Figures out how many FPGAs were switched on for a particular SpiNN-5 board. :param ~.Chip ethernet_chip: the ethernet chip to look from :return: number of FPGAs on, on this board """ # TODO: should be possible to get this info from Machine # As the Chips can be None use the machine call and not the View call machine = FecDataView.get_machine() # positions to check for active links left_chips = ( machine.get_chip_at(ethernet_chip.x + dx, ethernet_chip.y + dy) for dx, dy in ((0, 0), (0, 1), (0, 2), (0, 3), (0, 4))) right_chips = ( machine.get_chip_at(ethernet_chip.x + dx, ethernet_chip.y + dy) for dx, dy in ((7, 3), (7, 4), (7, 5), (7, 6), (7, 7))) top_chips = ( machine.get_chip_at(ethernet_chip.x + dx, ethernet_chip.y + dy) for dx, dy in ((4, 7), (5, 7), (6, 7), (7, 7))) bottom_chips = ( machine.get_chip_at(ethernet_chip.x + dx, ethernet_chip.y + dy) for dx, dy in ((0, 0), (1, 0), (2, 0), (3, 0), (4, 0))) top_left_chips = ( machine.get_chip_at(ethernet_chip.x + dx, ethernet_chip.y + dy) for dx, dy in ((0, 3), (1, 4), (2, 5), (3, 6), (4, 7))) bottom_right_chips = ( machine.get_chip_at(ethernet_chip.x + dx, ethernet_chip.y + dy) for dx, dy in ((0, 4), (1, 5), (2, 6), (3, 7))) # bottom left, bottom fpga_0 = __deduce_fpga( bottom_chips, bottom_right_chips, (5, 4), (0, 5)) # left, and top right fpga_1 = __deduce_fpga( left_chips, top_left_chips, (3, 4), (3, 2)) # top and right fpga_2 = __deduce_fpga( top_chips, right_chips, (2, 1), (0, 1)) return fpga_0 + fpga_1 + fpga_2 def __deduce_fpga(chips_1, chips_2, links_1, links_2): """ Figure out if each FPGA was on or not. :param iterable(~.Chip) chips_1: chips on an edge of the board :param iterable(~.Chip) chips_2: chips on an edge of the board :param iterable(int) links_1: which link IDs to check from chips_1 :param iterable(int) links_2: which link IDs to check from chips_2 :return: 0 if not on, 1 if on :rtype: int """ for chip, link_id in itertools.product(chips_1, links_1): if chip and chip.router.get_link(link_id) is not None: return 1 for chip, link_id in itertools.product(chips_2, links_2): if chip and chip.router.get_link(link_id) is not None: return 1 return 0 def _calculate_loading_energy(machine, load_time_ms, n_monitors, n_frames): """ :param ~.Machine machine: :param float load_time_ms: milliseconds :param int n_monitors :param int n_frames: :rtype: float """ # pylint: disable=too-many-arguments # find time in milliseconds with GlobalProvenance() as db: total_time_ms = db.get_timer_sum_by_category(TimerCategory.LOADING) # handle monitor core active cost # min between chips that are active and fixed monitor, as when 1 # chip is used its one monitor, if more than 1 chip, # the ethernet connected chip and the monitor handling the read/write # this is checked by min n_monitors_active = min(N_MONITORS_ACTIVE_DURING_COMMS, n_monitors) energy_cost = ( total_time_ms * n_monitors_active * MILLIWATTS_PER_CHIP_ACTIVE_OVERHEAD / machine.DEFAULT_MAX_CORES_PER_CHIP) # handle all idle cores energy_cost += _calculate_idle_cost(total_time_ms, machine) # handle time diff between load time and total load phase of ASB energy_cost += ( (total_time_ms - load_time_ms) * machine.n_chips * MILLIWATTS_PER_IDLE_CHIP) # handle active routers etc active_router_cost = ( load_time_ms * n_frames * MILLIWATTS_PER_FRAME_ACTIVE_COST) # accumulate energy_cost += active_router_cost return energy_cost def _calculate_data_extraction_energy(machine, n_monitors, n_frames): """ Data extraction cost. :param ~.Machine machine: machine description :param int n_monitors: :param int n_frames: :return: cost of data extraction in Joules :rtype: float """ # pylint: disable=too-many-arguments # find time # TODO is this what was desired total_time_ms = 0 with GlobalProvenance() as db: buffer_time_ms = db.get_timer_sum_by_work(TimerWork.EXTRACT_DATA) energy_cost = 0 # NOTE: Buffer time could be None if nothing was set to record if buffer_time_ms is not None: total_time_ms += buffer_time_ms # min between chips that are active and fixed monitor, as when 1 # chip is used its one monitor, if more than 1 chip, # the ethernet connected chip and the monitor handling the read/write # this is checked by min energy_cost = ( total_time_ms * min(N_MONITORS_ACTIVE_DURING_COMMS, n_monitors) * MILLIWATTS_PER_CHIP_ACTIVE_OVERHEAD / machine.DEFAULT_MAX_CORES_PER_CHIP) # add idle chip cost energy_cost += _calculate_idle_cost(total_time_ms, machine) # handle active routers etc energy_cost_of_active_router = ( total_time_ms * n_frames * MILLIWATTS_PER_FRAME_ACTIVE_COST) energy_cost += energy_cost_of_active_router return energy_cost def _calculate_idle_cost(time, machine): """ Calculate energy used by being idle. :param float time: time machine was idle, in milliseconds :param ~.Machine machine: machine description :return: cost in joules :rtype: float """ return (time * machine.total_available_user_cores * MILLIWATTS_PER_IDLE_CHIP / machine.DEFAULT_MAX_CORES_PER_CHIP) def _calculate_power_down_energy(time, machine, n_frames): """ Calculate power down costs. :param float time: time powered down, in milliseconds :param ~.Machine machine: :param int n_frames: number of frames used by this machine :return: energy in joules :rtype: float """ # pylint: disable=too-many-arguments # if spalloc or hbp if FecDataView.has_allocation_controller(): return time * n_frames * MILLIWATTS_FOR_FRAME_IDLE_COST # if 4 chip elif machine.n_chips <= 4: return machine.n_chips * time * MILLIWATTS_PER_IDLE_CHIP # if 48 chip else: return time * MILLIWATTS_FOR_BOXED_48_CHIP_FRAME_IDLE_COST def _calculate_n_frames(machine): """ Figures out how many frames are being used in this setup. A key of cabinet,frame will be used to identify unique frame. :param ~.Machine machine: the machine object :return: number of frames :rtype: int """ # if not spalloc, then could be any type of board, but unknown cooling if not FecDataView.has_allocation_controller(): return 0 # using spalloc in some form; how many unique frames? cabinet_frame = set() mac = FecDataView.get_allocation_controller() for ethernet_connected_chip in machine.ethernet_connected_chips: cabinet, frame, _ = mac.where_is_machine( ethernet_connected_chip.x, ethernet_connected_chip.y) cabinet_frame.add((cabinet, frame)) return len(cabinet_frame)