diff --git a/.ratexcludes b/.ratexcludes index 8acb1c02ea..964a183a24 100644 --- a/.ratexcludes +++ b/.ratexcludes @@ -17,3 +17,4 @@ **/PACMAN/** **/DataSpecification/** **/spalloc/** +**/unittests/**/*.rpt diff --git a/spinn_front_end_common/interface/abstract_spinnaker_base.py b/spinn_front_end_common/interface/abstract_spinnaker_base.py index f11fac6a12..1d3909fc9b 100644 --- a/spinn_front_end_common/interface/abstract_spinnaker_base.py +++ b/spinn_front_end_common/interface/abstract_spinnaker_base.py @@ -68,6 +68,8 @@ AbstractVertexWithEdgeToDependentVertices, AbstractCanReset) from spinn_front_end_common.interface.buffer_management import BufferManager +from spinn_front_end_common.interface.buffer_management.storage_objects \ + import BufferDatabase from spinn_front_end_common.interface.config_handler import ConfigHandler from spinn_front_end_common.interface.interface_functions import ( application_finisher, application_runner, @@ -109,7 +111,7 @@ memory_map_on_host_chip_report, network_specification, router_collision_potential_report, routing_table_from_machine_report, tags_from_machine_report, - write_json_machine, write_json_placements, + write_chip_active_report, write_json_machine, write_json_placements, write_json_routing_tables, drift_report) from spinn_front_end_common.utilities.iobuf_extractor import IOBufExtractor from spinn_front_end_common.utilities.utility_objs import ExecutableType @@ -2270,10 +2272,34 @@ def _print_iobuf(errors, warnings): for error in errors: logger.error(error) + def _execute_prepare_chip_power(self): + with FecTimer("Prepare Chip Power", TimerWork.REPORT) as timer: + if timer.skip_if_cfg_false("Reports", "write_energy_report"): + return + if timer.skip_if_virtual_board(): + return + db = BufferDatabase() + db.store_placements() + db.store_chip_power_monitors() + db.close() + + def _report_chip_active(self): + with FecTimer("Prepare Chip Power", TimerWork.REPORT) as timer: + if timer.skip_if_cfg_false("Reports", "write_energy_report"): + return + if timer.skip_if_virtual_board(): + return + write_chip_active_report() + + def _do_end_of_run(self): + if not self._data_writer.is_ran_last(): + return + self._execute_prepare_chip_power() + self._report_chip_active() + def reset(self): """ Code that puts the simulation back at time zero """ - FecTimer.start_category(TimerCategory.RESETTING) if not self._data_writer.is_ran_last(): if not self._data_writer.is_ran_ever(): logger.error("Ignoring the reset before the run") @@ -2281,8 +2307,11 @@ def reset(self): logger.error("Ignoring the repeated reset call") return + FecTimer.start_category(TimerCategory.RESETTING) logger.info("Resetting") + self._do_end_of_run() + # rewind the buffers from the buffer manager, to start at the beginning # of the simulation again and clear buffered out if self._data_writer.has_buffer_manager(): @@ -2367,6 +2396,8 @@ def stop(self): set_config("Reports", "read_provenance_data", "True") self._do_read_provenance() + self._do_end_of_run() + except Exception as e: self._recover_from_error(e) self.write_errored_file() diff --git a/spinn_front_end_common/interface/buffer_management/storage_objects/sqllite_database.py b/spinn_front_end_common/interface/buffer_management/storage_objects/sqllite_database.py index 4bf8db2943..3d25121412 100644 --- a/spinn_front_end_common/interface/buffer_management/storage_objects/sqllite_database.py +++ b/spinn_front_end_common/interface/buffer_management/storage_objects/sqllite_database.py @@ -53,11 +53,10 @@ def __init__(self, database_file=None): :param str database_file: The name of a file that contains (or will contain) an SQLite database holding the data. - If omitted the default location will be used. + If omitted the default location will be used """ if database_file is None: database_file = self.default_database_file() - super().__init__(database_file, ddl_file=_DDL_FILE) @classmethod @@ -274,7 +273,7 @@ def store_data_in_region_buffer(self, x, y, p, region, missing, data): region_id, content, content_len) VALUES (?, CAST(? AS BLOB), ?) """, (region_id, datablob, len(data))) - assert cursor.rowcount == 1 + assert cursor.rowcount == 1 def __use_main_table(self, cursor, region_id): """ @@ -314,3 +313,100 @@ def get_region_data(self, x, y, p, region): return data, False except LookupError: return memoryview(b''), True + + def store_placements(self): + exists = False + with self.transaction() as cursor: + for row in cursor.execute("PRAGMA TABLE_INFO(core)"): + if row["name"] == "label": + exists = True + + if exists: + return + # already done so no need to repeat + + cursor.execute("ALTER TABLE core ADD COLUMN label STRING") + + for placement in FecDataView.iterate_placemements(): + core_id = self.__get_core_id( + cursor, placement.x, placement.y, placement.p) + cursor.execute( + "UPDATE core SET label = ? WHERE core_id = ?", + (placement.vertex.label, core_id)) + assert cursor.rowcount == 1 + + for chip in FecDataView.get_machine().chips: + for processor in chip.processors: + if processor.is_monitor: + core_id = self.__get_core_id( + cursor, chip.x, chip.y, processor.processor_id) + cursor.execute( + """ + UPDATE core SET label = 'MONITOR' + WHERE core_id = ? + """, [core_id]) + assert cursor.rowcount == 1 + + def get_label(self, x, y, p): + with self.transaction() as cursor: + for row in cursor.execute( + """ + SELECT label + FROM core + WHERE x = ? AND y = ? and processor = ? + """, (x, y, p)): + return str(row["label"], 'utf8') + return "" + + def store_chip_power_monitors(self): + # delayed import due to circular refrences + from spinn_front_end_common.utility_models.\ + chip_power_monitor_machine_vertex import ( + ChipPowerMonitorMachineVertex) + + with self.transaction() as cursor: + for _ in cursor.execute( + """ + SELECT name FROM sqlite_master + WHERE type='table' AND name='chip_power_monitor' + """): + # Already exists so no need to run again + return + + cursor.execute( + """ + CREATE TABLE chip_power_monitors( + cpm_id INTEGER PRIMARY KEY autoincrement, + core_id INTEGER NOT NULL + REFERENCES core(core_id) ON DELETE RESTRICT, + sampling_frequency FLOAT NOT NULL) + """) + + cursor.execute( + """ + CREATE VIEW chip_power_monitors_view AS + SELECT core_id, x, y, processor, sampling_frequency + FROM core NATURAL JOIN chip_power_monitors + """) + + for placement in FecDataView.iterate_placements_by_vertex_type( + ChipPowerMonitorMachineVertex): + core_id = self.__get_core_id( + cursor, placement.x, placement.y, placement.p) + cursor.execute( + """ + INSERT INTO chip_power_monitors( + core_id, sampling_frequency) + VALUES (?, ?) + """, (core_id, placement.vertex.sampling_frequency)) + assert cursor.rowcount == 1 + + def iterate_chip_power_monitor_cores(self): + with self.transaction() as cursor: + for row in cursor.execute( + """ + SELECT x, y, processor, sampling_frequency + FROM chip_power_monitors_view + ORDER BY core_id + """): + yield row diff --git a/spinn_front_end_common/utilities/report_functions/__init__.py b/spinn_front_end_common/utilities/report_functions/__init__.py index 2a28071ecc..c66d0aa9e2 100644 --- a/spinn_front_end_common/utilities/report_functions/__init__.py +++ b/spinn_front_end_common/utilities/report_functions/__init__.py @@ -16,6 +16,7 @@ from .bit_field_compressor_report import bitfield_compressor_report from .bit_field_summary import BitFieldSummary from .board_chip_report import board_chip_report +from .chip_active_report import write_chip_active_report from .energy_report import EnergyReport from .fixed_route_from_machine_report import fixed_route_from_machine_report from .memory_map_on_host_chip_report import memory_map_on_host_chip_report @@ -44,6 +45,7 @@ "router_collision_potential_report", "routing_table_from_machine_report", "tags_from_machine_report", + "write_chip_active_report", "write_json_machine", "write_json_placements", "write_json_routing_tables", diff --git a/spinn_front_end_common/utilities/report_functions/chip_active_report.py b/spinn_front_end_common/utilities/report_functions/chip_active_report.py new file mode 100644 index 0000000000..6b4d872c41 --- /dev/null +++ b/spinn_front_end_common/utilities/report_functions/chip_active_report.py @@ -0,0 +1,101 @@ +# Copyright (c) 2017-2019 The University of Manchester +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import numpy +import os +from spinn_utilities.config_holder import get_config_int +from spinn_utilities.exceptions import SpiNNUtilsException +from spinn_utilities.log import FormatAdapter +from spinn_front_end_common.data import FecDataView +from spinn_front_end_common.interface.buffer_management.storage_objects \ + import (BufferDatabase) +from spinn_front_end_common.interface.interface_functions.compute_energy_used\ + import (MILLIWATTS_PER_CHIP_ACTIVE_OVERHEAD) + +logger = FormatAdapter(logging.getLogger(__name__)) + +#: converter between joules to kilowatt hours +JOULES_TO_KILOWATT_HOURS = 3600000 + +# energy report file name +CHIP_ACTIVE_FILENAME = "chip_active_report.rpt" + + +def write_chip_active_report(report_path=None, buffer_path=None): + """ Writes the report. + + :param report_path: Where to write the report if not using the default + :type report_path: None or str + :param buffer_path: Where the provenance sqlite3 files is located + if not using the default. + :type buffer_path: None or str + :rtype: None + """ + if report_path is None: + try: + report_dir = FecDataView.get_run_dir_path() + report_path = os.path.join( + report_dir, CHIP_ACTIVE_FILENAME) + except SpiNNUtilsException: + report_path = os.path.join( + os.path.curdir, CHIP_ACTIVE_FILENAME) + logger.warning(f"no report_path so writing to {report_path}") + + # create detailed report + with open(report_path, "w", encoding="utf-8") as f: + __write_report(f, buffer_path) + + +def __write_report(f, buffer_path): + db = BufferDatabase(buffer_path) + n_samples_per_recording = get_config_int( + "EnergyMonitor", "n_samples_per_recording_entry") + + milliwatts = MILLIWATTS_PER_CHIP_ACTIVE_OVERHEAD / 18 + activity_total = 0 + energy_total = 0 + + for row in db.iterate_chip_power_monitor_cores(): + record_raw, data_missing = db.get_region_data( + row["x"], row["y"], row["processor"], 0) + results = ( + numpy.frombuffer(record_raw, dtype="uint32").reshape(-1, 18) / + n_samples_per_recording) + active_sums = numpy.sum(results, axis=0) + activity_count = numpy.sum(results) + time_for_recorded_sample =\ + (row["sampling_frequency"] * n_samples_per_recording) / 1000 + energy_factor = time_for_recorded_sample * milliwatts + + for core in range(0, 18): + label = db.get_label(row["x"], row["y"], core) + if (active_sums[core] > 0) or label: + f.write( + f"processor {row['x']}:{row['y']}:{core}({label})" + f" was active for {active_sums[core]}ms " + f" using { active_sums[core] * energy_factor} Joules\n") + + energy = activity_count * energy_factor + activity_total += activity_count + energy_total += energy + f.write( + f"Total for chip {row['x']}:{row['y']} " + f" was {activity_count}ms of activity " + f" using {energy} Joules\n\n") + f.write( + f"Total " + f" was {activity_total}ms of activity " + f" using {energy_total} Joules\n\n") diff --git a/unittests/utilities/.gitignore b/unittests/utilities/.gitignore new file mode 100644 index 0000000000..b911337edd --- /dev/null +++ b/unittests/utilities/.gitignore @@ -0,0 +1 @@ +*.rpt \ No newline at end of file diff --git a/unittests/utilities/buffer.sqlite3 b/unittests/utilities/buffer.sqlite3 new file mode 100644 index 0000000000..c36eac0b3c Binary files /dev/null and b/unittests/utilities/buffer.sqlite3 differ diff --git a/unittests/utilities/test_chip_active.py b/unittests/utilities/test_chip_active.py new file mode 100644 index 0000000000..422396e9c1 --- /dev/null +++ b/unittests/utilities/test_chip_active.py @@ -0,0 +1,62 @@ +# Copyright (c) 2017-2022 The University of Manchester +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import unittest +from spinn_utilities.exceptions import InvalidDirectory +from spinn_front_end_common.data.fec_data_writer import FecDataWriter +from spinn_front_end_common.interface.config_setup import unittest_setup +from spinn_front_end_common.utilities.report_functions import ( + write_chip_active_report) + + +class TestChipActive(unittest.TestCase): + + def setUp(cls): + unittest_setup() + + def test_no_params(self): + try: + write_chip_active_report() + failed = False + except Exception as ex: + self.assertIn("no such table", str(ex)) + failed = True + self.assertTrue(failed) + + def test_db_only(self): + # make sure there is not run_dir_path so falls back on default + writer = FecDataWriter.setup() + try: + writer.set_run_dir_path("THIS DIRECTORY DOES NOT EXIST") + except InvalidDirectory: + pass + db_path = os.path.join(os.path.dirname(__file__), "buffer.sqlite3") + write_chip_active_report(buffer_path=db_path) + + def test_all_params(self): + # make sure there is not run_dir_path so falls back on default + writer = FecDataWriter.setup() + try: + writer.set_run_dir_path("THIS DIRECTORY DOES NOT EXIST") + except InvalidDirectory: + pass + db_path = os.path.join(os.path.dirname(__file__), "buffer.sqlite3") + report = os.path.join(os.path.dirname(__file__), "my_active.rpt") + write_chip_active_report(report_path=report, buffer_path=db_path) + + +if __name__ == '__main__': + unittest.main()