Skip to content

Commit

Permalink
Merge bb4c8c8 into bfda216
Browse files Browse the repository at this point in the history
  • Loading branch information
mossblaser committed Apr 23, 2015
2 parents bfda216 + bb4c8c8 commit b5b6516
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 22 deletions.
41 changes: 27 additions & 14 deletions docs/source/control.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,39 @@ necessary). If not all applications could be loaded, a
exception is raised.

Many applications require the `sync0` signal to be sent to start the
application's event handler after loading. This can be done using
:py:class:`~.MachineController.send_signal`::
application's event handler after loading. We can wait for all cores to reach
the `sync0` barrier using
:py:class:`~.MachineController.wait_for_cores_to_reach_state` and then send the
`sync0` signal using :py:class:`~.MachineController.send_signal`::

>>> from rig.machine_control.consts import AppSignal
>>> mc.send_signal(AppSignal.sync0)
>>> # In the example above we loaded 5 cores so we expect 5 cores to reach
>>> # sync0.
>>> mc.wait_for_cores_to_reach_state("sync0", 5)
5
>>> mc.send_signal("sync0")

Similarly, after execution, the application can be killed with::
Similarly, after application execution, the application can be killed with::

>>> mc.send_signal(AppSignal.stop)
>>> mc.send_signal("stop")

Since the stop signal also cleans up allocated resources in a SpiNNaker machine
(e.g. stray processes, routing entries and allocated SDRAM), it is desireable
for this signal to reliably get sent, even if something crashes in the host
application. To facilitate this, you can use the
:py:meth:`~.MachineController.application` context manager::

>>> with mc.application():
... # Main application code goes here, e.g. loading applications,
... # routing tables and SDRAM.
>>> # When the above block exits (even if due to an exception), the stop
>>> # signal will be sent to the application.

.. note::
Many application-oriented methods accept an `app_id` argument which is given
a sensible default value.
a sensible default value. If the :py:meth:`.MachineController.application`
context manager is given an app ID as its argument, this app ID will become
the default `app_id` within the `with` block. See the section on context
managers below for more details.

Loading Routing Tables
^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -186,13 +206,6 @@ reduce the repetition, Python's ``with`` statement can be used::
... block_addr = mc.sdram_alloc(1024, 3)
... mc.write(block_addr, b"Hello, world!")

Alternatively, the current context can be modified by calling
:py:meth:`~.MachineController.update_current_context`::

>>> # Following this call all commands will use app_id=56
>>> mc.update_current_context(app_id=56)


:py:class:`.BMPController` Tutorial
-----------------------------------

Expand Down
72 changes: 66 additions & 6 deletions rig/machine_control/machine_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ def send_signal(self, signal, app_id=Required):
In current implementations of SARK, signals are highly likely to
arrive but this is not guaranteed (especially when the system's
network is heavily utilised). Users should treat this mechanism
with caution.
with caution. Future versions of SARK may resolve this issue.
Parameters
----------
Expand Down Expand Up @@ -953,16 +953,17 @@ def count_cores_in_state(self, state, app_id=Required):
In current implementations of SARK, signals (which are used to
determine the state of cores) are highly likely to arrive but this
is not guaranteed (especially when the system's network is heavily
utilised). Users should treat this mechanism with caution.
utilised). Users should treat this mechanism with caution. Future
versions of SARK may resolve this issue.
Parameters
----------
state : string or :py:class:`~rig.machine_control.consts.AppSignal`
state : string or :py:class:`~rig.machine_control.consts.AppState`
Count the number of cores currently in this state. This may be
either an entry of the
:py:class:`~rig.machine_control.consts.AppSignal` enum or, for
:py:class:`~rig.machine_control.consts.AppState` enum or, for
convenience, the name of a state (defined in
:py:class:`~rig.machine_control.consts.AppSignal`) as a string.
:py:class:`~rig.machine_control.consts.AppState`) as a string.
"""
if isinstance(state, str):
try:
Expand All @@ -976,7 +977,7 @@ def count_cores_in_state(self, state, app_id=Required):
raise ValueError(
"count_cores_in_state: Unknown state {}".format(
repr(state)))

# TODO Determine a way to nicely express a way to use the region data
# stored in arg3.
region = 0x0000ffff # Largest possible machine, level 0
Expand All @@ -994,6 +995,65 @@ def count_cores_in_state(self, state, app_id=Required):
return self._send_scp(
0, 0, 0, SCPCommands.signal, arg1, arg2, arg3).arg1

@ContextMixin.use_contextual_arguments
def wait_for_cores_to_reach_state(self, state, count, app_id=Required,
poll_interval=0.1, timeout=None):
"""Block until the specified number of cores reach the specified state.
This is a simple utility-wrapper around the
:py:meth:`.count_cores_in_state` method which polls the machine until
(at least) the supplied number of cores has reached the specified
state.
.. warning::
In current implementations of SARK, signals (which are used to
determine the state of cores) are highly likely to arrive but this
is not guaranteed (especially when the system's network is heavily
utilised). As a result, in uncommon-but-possible circumstances,
this function may never exit. Users should treat this function with
caution. Future versions of SARK may resolve this issue.
Parameters
----------
state : string or :py:class:`~rig.machine_control.consts.AppState`
The state to wait for cores to enter. This may be
either an entry of the
:py:class:`~rig.machine_control.consts.AppState` enum or, for
convenience, the name of a state (defined in
:py:class:`~rig.machine_control.consts.AppState`) as a string.
count : int
The (minimum) number of cores reach the specified state before this
method terminates.
poll_interval : float
Number of seconds between state counting requests sent to the
machine.
timeout : float or Null
Maximum number of seconds which may elapse before giving up. If
None, keep trying forever.
Returns
-------
int
The number of cores in the given state (which will be less than the
number required if the method timed out).
"""
if timeout is not None:
timeout_time = time.time() + timeout

while True:
cur_count = self.count_cores_in_state(state, app_id)
if cur_count >= count:
break

# Stop if timeout elapsed
if timeout is not None and time.time() > timeout_time:
break

# Pause before retrying
time.sleep(poll_interval)

return cur_count

@ContextMixin.use_contextual_arguments
def load_routing_tables(self, routing_tables, app_id=Required):
"""Allocate space for an load multicast routing tables.
Expand Down
66 changes: 64 additions & 2 deletions rig/machine_control/tests/test_machine_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import struct
import tempfile
import os
import time
from .test_scp_connection import SendReceive, mock_conn # noqa

from ..consts import DataType, SCPCommands, LEDAction, NNCommands, NNConstants
Expand Down Expand Up @@ -1179,8 +1180,8 @@ def read_struct_field(fn, x, y, p):
@pytest.mark.parametrize("signal", ["non-existant",
consts.AppDiagnosticSignal.AND])
def test_send_signal_fails(self, signal):
# Make sure that the send_signal function rejects bad signal identifiers
# (or ones that require special treatment)
# Make sure that the send_signal function rejects bad signal
# identifiers (or ones that require special treatment)
cn = MachineController("localhost")
with pytest.raises(ValueError):
cn.send_signal(signal)
Expand Down Expand Up @@ -1264,6 +1265,67 @@ def test_count_cores_in_state_fails(self, state):
with pytest.raises(ValueError):
cn.count_cores_in_state(state)

@pytest.mark.parametrize("app_id, count", [(16, 3), (30, 68)])
@pytest.mark.parametrize("n_tries", [1, 3])
@pytest.mark.parametrize("timeout", [None, 1.0])
@pytest.mark.parametrize("excess", [True, False])
@pytest.mark.parametrize("state", [consts.AppState.idle, "run"])
def test_wait_for_cores_to_reach_state(self, app_id, count, n_tries,
timeout, excess, state):
# Create the controller
cn = MachineController("localhost")

# The count_cores_in_state mock will return less than the required
# number of cores the first n_tries attempts and then start returning a
# suffient number of cores.
cn.count_cores_in_state = mock.Mock()
n_tries_elapsed = [0]

def count_cores_in_state(state_, app_id_):
assert state_ == state
assert app_id_ == app_id

if n_tries_elapsed[0] < n_tries:
n_tries_elapsed[0] += 1
return count - 1
else:
if excess:
return count + 1
else:
return count
cn.count_cores_in_state.side_effect = count_cores_in_state

val = cn.wait_for_cores_to_reach_state(state, count, app_id,
0.001, timeout)
if excess:
assert val == count + 1
else:
assert val == count

assert n_tries_elapsed[0] == n_tries

def test_wait_for_cores_to_reach_state_timeout(self):
# Create the controller
cn = MachineController("localhost")

cn.count_cores_in_state = mock.Mock()
cn.count_cores_in_state.return_value = 0

time_before = time.time()
val = cn.wait_for_cores_to_reach_state("sync0", 10, 30, 0.01, 0.05)
time_after = time.time()

assert val == 0

# The timeout interval should have at least occurred
assert (time_after - time_before) >= 0.05

# At least two attempts should have been possible in that time
assert len(cn.count_cores_in_state.mock_calls) >= 2

for call in cn.count_cores_in_state.mock_calls:
assert call == mock.call("sync0", 30)

@pytest.mark.parametrize("x, y, app_id", [(1, 2, 32), (4, 10, 17)])
@pytest.mark.parametrize(
"entries",
Expand Down

0 comments on commit b5b6516

Please sign in to comment.