Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version flexible Virtual Machine #256

Merged
merged 18 commits into from
Apr 19, 2024
6 changes: 4 additions & 2 deletions spinn_machine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,12 @@
from .router import Router
from .routing_entry import RoutingEntry
from .spinnaker_triad_geometry import SpiNNakerTriadGeometry
from .virtual_machine import virtual_machine
from .virtual_machine import (
virtual_machine, virtual_machine_by_boards, virtual_machine_by_cores)


__all__ = ["Chip", "CoreSubset", "CoreSubsets",
"FrozenCoreSubsets", "Link", "Machine", "MulticastRoutingEntry",
"Router", "RoutingEntry", "SpiNNakerTriadGeometry",
"virtual_machine"]
"virtual_machine",
"virtual_machine_by_boards", "virtual_machine_by_cores"]
33 changes: 33 additions & 0 deletions spinn_machine/data/machine_data_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class _MachineDataModel(object):

__slots__ = [
# Data values cached
"_all_monitor_cores",
"_ethernet_monitor_cores",
"_machine",
"_machine_generator",
"_machine_version",
Expand Down Expand Up @@ -73,6 +75,8 @@ def _hard_reset(self) -> None:
This does NOT clear the machine as it may have been asked for before
"""
self._soft_reset()
self._all_monitor_cores: int = 0
self._ethernet_monitor_cores: int = 0
self._machine: Optional[Machine] = None
self._user_accessed_machine = False

Expand Down Expand Up @@ -263,3 +267,32 @@ def get_machine_version(cls) -> AbstractVersion:
if cls.__data._machine_version is None:
cls.__data._machine_version = version_factory()
return cls.__data._machine_version

@classmethod
def get_all_monitor_cores(cls) -> int:
"""
The number of cores on every chip reported to be used by \
monitor vertices.

Ethernet-enabled chips may have more.

Does not include the system core reserved by the machine/ scamp.

:rtype: int
"""
return cls.__data._all_monitor_cores

@classmethod
def get_ethernet_monitor_cores(cls) -> int:
"""
The number of cores on every Ethernet chip reported to be used by \
monitor vertices.

This includes the one returned by get_all_monitor_cores unless for
some reason these are not on Ethernet chips.

Does not include the system core reserved by the machine/ scamp.

:rtype: int
"""
return cls.__data._ethernet_monitor_cores
28 changes: 21 additions & 7 deletions spinn_machine/data/machine_data_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from spinn_utilities.data.utils_data_writer import UtilsDataWriter
from spinn_utilities.overrides import overrides
from spinn_utilities.log import FormatAdapter
from spinn_machine import Machine, virtual_machine
from spinn_machine import Machine, virtual_machine_by_boards
from .machine_data_view import MachineDataView, _MachineDataModel
logger = FormatAdapter(logging.getLogger(__name__))
__temp_dir = None
Expand Down Expand Up @@ -46,12 +46,7 @@ def _mock_machine(self) -> None:
"""
Method to create a virtual machine in mock mode.
"""
if self.get_machine_version().number == 3:
self.set_machine(virtual_machine(width=2, height=2))
elif self.get_machine_version().number == 5:
self.set_machine(virtual_machine(width=8, height=8))
else:
raise NotImplementedError("Please set machine version")
self.set_machine(virtual_machine_by_boards(1))

@overrides(UtilsDataWriter._setup)
def _setup(self) -> None:
Expand Down Expand Up @@ -109,3 +104,22 @@ def set_machine_generator(self, machine_generator: Callable[[], None]):
if not callable(machine_generator):
raise TypeError("machine_generator must be callable")
self.__data._machine_generator = machine_generator

def add_monitor_core(self, all_chips: bool):
"""
Accepts a simple of the monitor cores to be added.

Called by PacmanDataWriter add_sample_monitor_vertex.

Only affect is to change the numbers reported by the
get_all/ethernet_monitor methods.

:param bool all_chips:
If True assumes that this Vertex will be placed on all chips
including Ethernet ones.
If False assumes that this Vertex type will only be placed on
Ethernet Vertices
"""
self.__data._ethernet_monitor_cores += 1
if all_chips:
self.__data._all_monitor_cores += 1
53 changes: 53 additions & 0 deletions spinn_machine/version/abstract_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from spinn_utilities.log import FormatAdapter
from spinn_utilities.config_holder import get_config_int_or_none
from spinn_utilities.typing.coords import XY
from spinn_machine.data import MachineDataView
from spinn_machine.exceptions import SpinnMachineException
if TYPE_CHECKING:
from spinn_machine.machine import Machine
Expand Down Expand Up @@ -351,3 +352,55 @@ def illegal_ethernet_message(self, x: int, y: int) -> Optional[str]:
:return: An explanation that the x and y can never be an Ethernet
"""
raise NotImplementedError

def size_from_n_cores(self, n_cores: int) -> Tuple[int, int]:
"""
Returns the size needed to support this many cores.

Takes into consideration scamp and monitor cores.

Designed for use with virtual boards.
Does not include a safety factor for blacklisted cores or chips.
For real machines a slightly bigger Machine may be needed.

:param int n_cores: Number of None Scamp and monitor cores needed
:rtype: (int, int)
"""
cores_per_board = sum(self.chip_core_map.values())
cores_per_board -= MachineDataView.get_ethernet_monitor_cores()
cores_per_board -= (
(MachineDataView.get_all_monitor_cores() + self.n_scamp_cores)
* self.n_chips_per_board)
# Double minus to round up
return self.size_from_n_boards(-(-n_cores // cores_per_board))

def size_from_n_chips(self, n_chips: int) -> Tuple[int, int]:
"""
Returns the size needed to support this many chips.

Designed for use with virtual boards.
Does not include a safety factor for blacklisted Chips.
For real machines a slightly bigger Machine may be needed.

:param int n_boards:
:rtype: (int, int)
:raises SpinnMachineException:
If multiple boards are needed but not supported
"""
# Double minus to round up
return self.size_from_n_boards(-(-n_chips // self.n_chips_per_board))

def size_from_n_boards(self, n_boards: int) -> Tuple[int, int]:
"""
Returns the size needed to support this many boards.

:param int n_boards:
:rtype: (int, int)
:raises SpinnMachineException:
If multiple boards are needed but not supported
"""
# Override for versions that support multiple boards
if n_boards == 1:
return self.board_shape
raise SpinnMachineException(
f"Version {self} does not support multiple boards")
4 changes: 2 additions & 2 deletions spinn_machine/version/version_3.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ def get_potential_ethernet_chips(
@overrides(VersionSpin1._verify_size)
def _verify_size(self, width: int, height: int):
if width != 2:
raise SpinnMachineException("Unexpected {width=}")
raise SpinnMachineException(f"Unexpected {width=}")
if height != 2:
raise SpinnMachineException("Unexpected {height=}")
raise SpinnMachineException(f"Unexpected {height=}")

@overrides(VersionSpin1._create_machine)
def _create_machine(self, width: int, height: int, origin: str) -> Machine:
Expand Down
12 changes: 12 additions & 0 deletions spinn_machine/version/version_5.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import math
from typing import Final, Mapping, Optional, Sequence, Tuple
from spinn_utilities.overrides import overrides
from spinn_utilities.typing.coords import XY
Expand Down Expand Up @@ -107,3 +108,14 @@ def illegal_ethernet_message(self, x: int, y: int) -> Optional[str]:
return "Only Chip with x + y divisible by 12 " \
"may be an Ethernet Chip"
return None

@overrides(VersionSpin1.size_from_n_boards)
def size_from_n_boards(self, n_boards: int) -> Tuple[int, int]:
if n_boards <= 1:
return 8, 8
# This replicates how spalloc does it
# returning a rectangle of triads
triads = math.ceil(n_boards / 3)
width = math.ceil(math.sqrt(triads))
height = math.ceil(triads / width)
return width * 12 + 4, height * 12 + 4
110 changes: 104 additions & 6 deletions spinn_machine/virtual_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# 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 math
from collections import defaultdict
import logging
from typing import Dict, List, Optional, Set, Tuple
Expand All @@ -28,21 +28,120 @@
logger = FormatAdapter(logging.getLogger(__name__))


def virtual_machine(
width: int, height: int, validate: bool = True):
def virtual_machine(width: int, height: int, validate: bool = True):
"""
Create a virtual SpiNNaker machine, used for planning execution.

:param int width: the width of the virtual machine in chips
:param int height: the height of the virtual machine in chips
:param bool validate: if True will call the machine validate function

:returns: a virtual machine (that cannot execute code)
:rtype: ~spinn_machine.Machine
"""
factory = _VirtualMachine(width, height, validate)
return factory.machine


def virtual_machine_by_min_size(
width: int, height: int, validate: bool = True):
"""
Create a virtual SpiNNaker machine, used for planning execution.

:param int width: the minimum width of the virtual machine in chips
:param int height: the minimum height of the virtual machine in chips
:param bool validate: if True will call the machine validate function

:returns: a virtual machine (that cannot execute code)
:rtype: ~spinn_machine.Machine
"""
version = MachineDataView.get_machine_version()
w_board, h_board = version.board_shape
# check for edge case
if width <= w_board and height > h_board:
width = w_board * 2
if height <= h_board and width > w_board:
height = h_board * 2
width = w_board * math.ceil(width / w_board)
height = h_board * math.ceil(height / h_board)
return virtual_machine(width, height, validate)


def virtual_machine_by_cores(n_cores: int, validate: bool = True):
"""
Create a virtual SpiNNaker machine, used for planning execution.

Semantic sugar for

MachineDataView.get_machine_version()

width, height = version.size_from_n_cores(n_cores)

return virtual_machine(width, height, validate)

:param n_cores: Minimum number of user cores
:param bool validate: if True will call the machine validate function

:returns: a virtual machine (that cannot execute code)
:rtype: ~spinn_machine.Machine
:raises SpinnMachineException:
If multiple boards are needed but not supported
"""
version = MachineDataView.get_machine_version()
width, height = version.size_from_n_cores(n_cores)
return virtual_machine(width, height, validate)


def virtual_machine_by_chips(n_chips: int, validate: bool = True):
"""
Create a virtual SpiNNaker machine, used for planning execution.

Semantic sugar for

MachineDataView.get_machine_version()

width, height = version.size_from_n_cchips(n_cores)

return virtual_machine(width, height, validate)

:param n_chips: Minimum number of chips
:param bool validate: if True will call the machine validate function

:returns: a virtual machine (that cannot execute code)
:rtype: ~spinn_machine.Machine
:raises SpinnMachineException:
If multiple boards are needed but not supported
"""
version = MachineDataView.get_machine_version()
width, height = version.size_from_n_chips(n_chips)
return virtual_machine(width, height, validate)


def virtual_machine_by_boards(n_boards: int, validate: bool = True):
"""
Create a virtual SpiNNaker machine, used for planning execution.

semantic sugar for:

version = MachineDataView.get_machine_version()

width, height = version.size_from_n_boards(n_boards)

return virtual_machine(width, height, validate)

:param n_boards: Minimum number of boards
:param bool validate: if True will call the machine validate function

:returns: a virtual machine (that cannot execute code)
:rtype: ~spinn_machine.Machine
:raises SpinnMachineException:
If multiple boards are needed but not supported
"""
version = MachineDataView.get_machine_version()
width, height = version.size_from_n_boards(n_boards)
return virtual_machine(width, height, validate)


class _VirtualMachine(object):
"""
A Virtual SpiNNaker machine factory
Expand All @@ -63,10 +162,9 @@ class _VirtualMachine(object):

ORIGIN = "Virtual"

def __init__(
self, width: int, height: int, validate: bool = True):
def __init__(self, width: int, height: int, validate: bool = True):
version = MachineDataView.get_machine_version()
version.verify_size(height, width)
version.verify_size(width, height)
max_cores = version.max_cores_per_chip
self._n_router_entries = version.n_router_entries
self._machine = version.create_machine(
Expand Down
12 changes: 12 additions & 0 deletions unittests/data/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,15 @@ def test_where_is_setup(self):
"None",
MachineDataView.where_is_chip(None)
)

def test_get_monitors(self):
writer = MachineDataWriter.setup()
self.assertEqual(0, MachineDataView.get_all_monitor_cores())
self.assertEqual(0, MachineDataView.get_ethernet_monitor_cores())
writer.add_monitor_core(True)
self.assertEqual(1, MachineDataView.get_all_monitor_cores())
self.assertEqual(1, MachineDataView.get_ethernet_monitor_cores())
writer.add_monitor_core(False)
writer.add_monitor_core(True)
self.assertEqual(2, MachineDataView.get_all_monitor_cores())
self.assertEqual(3, MachineDataView.get_ethernet_monitor_cores())
Loading
Loading