Rig Example Programs

This project aims to build up a series of usage examples for the Rig library. This is currently a work-in-progress.

Tutorial series:

Example 01: Hello World

In this classic example we make a SpiNNaker application which simply prints “Hello, world!” on one core and then exits.

Example Source code on GitHub

SpiNNaker application

We start by writing the SpiNNaker application itself which consists of a single call to io_printf in hello.c.

#include "sark.h"
void c_main(void)
{
  io_printf(IO_BUF, "Hello, world!\n");
}

This call writes our famous message to the “IO buffer”, an area of system memory which we can later read back from the host. This is then compiled using the usual SpiNNaker-tools makefile to produce hello.aplx:

$ cd /path/to/spinnaker_tools
$ source ./setup
$ cd -
$ make APP=hello

Note

For those who don’t have the ARM cross-compiler installed a precompiled binary is included with this example.

Host-side application

Now that we have our compiled binary we must boot our SpiNNaker machine, load our application onto a core and then read back the IO buffer. We could do this using the ybug command included with the low-level software tools but since we’re building up towards a real-world application, we’ll use Rig. Rig provides a higher-level Python interface to SpiNNaker machines which is easily used as part of a larger program, unlike ybug which is designed for interactive use as a debugger.

Before we start we must install the Rig library. The easiest way to do this is via PyPI using pip:

$ pip install rig

The hello.py contains a Python program which uses Rig to boot a SpiNNaker machine, load our application and then print the result. We’ll go through this line by line below.

All control of a SpiNNaker machine is achieved via a Rig MachineController which we import like so:

from rig.machine_control import MachineController

We create an instance with the hostname/IP address set to our SpiNNaker board, taken from the command-line (to avoid having to hard-code things in our program).

import sys
mc = MachineController(sys.argv[1])

Next to boot the machine we use the boot() method, taking the width and height of the machine to boot from the next two command line arguments. For a SpiNN-2 or SpiNN-3 board these dimensions are 2 and 2. For a SpiNN-5 board the dimensions are 8 and 8.

mc.boot(int(sys.argv[2]), int(sys.argv[3]))

Next we’ll load our application using the load_application() method. This method loads our application onto core 1 of chip (0, 0), checks it was loaded successfully and then starts the program executing before returning. Note that this method can load an application onto many cores at once, hence the slightly unusual syntax.

mc.load_application("hello.aplx", {(0, 0): {1}})

When a SpiNNaker application’s c_main function returns, the application goes into the exit state. By using wait_for_cores_to_reach_state() we can wait for our hello world application to finish executing.

mc.wait_for_cores_to_reach_state("exit", 1)

After our application has exited we can fetch and print out the contents of the IO buffer for the core we ran our application on to see what it printed using the get_iobuf() method. (By convention Rig uses the name p – for processor – when identifying cores.)

print(mc.get_iobuf(x=0, y=0, p=1))

As a final step we must send the “stop” signal to SpiNNaker which frees up any resources allocated during the runing of our application. In this case that just means the memory allocated for the IO buffer.

mc.send_signal("stop")

We can finally run our script like so (for a SpiNN-5 board):

$ python hello.py my-spinn-5-board 8 8
Hello, world!

Note that this script can take a little time while the boot is carried out. If your board is already booted, you can comment out the boot line and the script should run almost instantaneously.

Note

Instead of incorporating the boot process into your scripts you can use the rig-boot command line utility to boot your machine first like so:

$ rig-boot my-spinn-5-board --spin5

For help see:

$ rig-boot --help

In later examples we’ll use this instead of including the call to boot() in our scripts.

Example 02: Reading and Writing SDRAM

Most interesting SpiNNaker applications require some sort of configuration data to be loaded onto the machine or produce result data which must be read back from the machine. Typically this is done by allocating and writing to or reading from the shared SDRAM on each SpiNNaker chip. In this example we’ll write a simple SpiNNaker application which, using a single core, adds two numbers loaded in SDRAM and writes the answer back to SDRAM.

Much of the code in this example is unchanged from the previous example so we will only discuss the changes.

Example Source code on GitHub

Allocating SDRAM

When both the host and SpiNNaker need to access the same block of SDRAM, such as when loading configuration data or reading back results, it is common for the host to allocate the SDRAM. In this example we’ll allocate some SDRAM on chip (0, 0). We’ll allocate a total of 12 bytes: 4 bytes (32 bits) each for the two values we want to be added and another 4 bytes for the result using sdram_alloc():

sdram_addr = mc.sdram_alloc(12, x=0, y=0, tag=1)

The sdram_alloc() method simply returns the address of a block of SDRAM on chip 0, 0 which was allocated.

We also need to somehow inform the SpiNNaker application of this address. To do this we can use the ‘tag’ using the argument to give an identifier to the allocated memory block. The SpiNNaker application then uses the sark_tag_ptr function to look up the address of an SDRAM block with a specified tag.

  uint32_t *numbers = sark_tag_ptr(spin1_get_core_id(), 0);

A tag is a user-defined identifier which must be unique to the SpiNNaker chip and application. By convention the SDRAM allocated for applications running on core 1 are given tag 1, those on core 2 given tag 2 and so on. This means the same application binary can be loaded onto multiple cores which can simply look up their core number to discover their unique SDRAM allocation’s address.

Reading and writing to SDRAM

We pick two random numbers to add together and write them to the SDRAM we just allocated. Note that we must pack our values into bytes using Python’s struct module. Since SpiNNaker is little-endian we must be careful to use the ‘<’ format string.

num_a = random.getrandbits(30)
num_b = random.getrandbits(30)
data = struct.pack("<II", num_a, num_b)

Next we simply write the bytes to the allocated block of SDRAM using write().

mc.write(sdram_addr, data, x=0, y=0)

After we’ve allocated and writen our config data to SDRAM we can load our application as usual. On the C side of our application, the SDRAM can be accessed like any other memory.

  numbers[2] = numbers[0] + numbers[1];

Warning

Though SpiNNaker’s SDRAM can be accessed just like normal memory within a SpiNNaker application, this comes with a significant performance penalty; ‘real’ applications should use DMA to access SDRAM.

Once the application has exited, the host can read the answer back using read().

result_data = mc.read(sdram_addr + 8, 4, x=0, y=0)
result, = struct.unpack("<I", result_data)
print("{} + {} = {}".format(num_a, num_b, result))

Finally, as in our last example we must send the stop signal using send_signal() to free up all SpiNNaker resources. This is important since until the ‘stop’ signal is sent the SDRAM and tag number will remain allocated and may not be reallocated.

mc.send_signal("stop")

Example 03: Reading and Writing SDRAM - Improved

This example is functionally identical to the previous version but changes some of the host-side code to use some of Rig’s more advanced features which help make the program more robust and concise. The SpiNNaker application remains unchanged.

Example Source code on GitHub

Reliably stopping applications

Now that we’re starting to allocate machine resources and write more complex programs it is important to be sure that the stop signal is sent to the machine at the end of our application’s execution, especially if our host-side application crashes and exits prematurely. To aid with this, the MachineController class provides an application() context manager which will send the stop signal however the block is exited. In our example, we just put the main body of our application logic in a with block:

with mc.application():

File-like memory access

When working with SDRAM it can be easy to accidentally access memory outside the range of an allocated buffer. To provide safer and more conveninent SDRAM access the sdram_alloc_as_filelike() method produces a file-like MemoryIO object. This object can then be used just like a conventional file using read(), write() and seek() methods. All writes and reads to the file will be bounded to the allocated block of SDRAM preventing accidental corruption of memory. Additionally, users of an allocation need not know anything about the chip or address of the allocation and in fact may be oblivious to the fact that they’re using anything other than a normal file.

    sdram = mc.sdram_alloc_as_filelike(12, x=0, y=0, tag=1)
    sdram.write(data)
    result_data = sdram.read(4)

Like files, reads and writes occur immediately after the previous read and write and seek() must be used to cause a read/write to occur at a different location. Note that in this case since the result value is written immediately after the two input values we do no need to seek before reading.

More complex example programs:

Simple Digital Circuit Simulator

This example application demonstrates how a simple yet functional gate-level logic circuit simulation SpiNNaker application can be built using Rig.

In this application, each individual gate is modelled by an application core in SpiNNaker. Connections between the gates are modelled by transmitting multicast packets. Each core simply recomputes and transmits its value every millisecond based on the most recently received input values. Some applications may also be used to playback predefined stimulus signals or record the state of signals as the simulation progresses. A simple Rig-based program (circuit_sim) runs on the host taking care of mapping a description of a circuit into applications and routes, loading and configuring the machine and retrieving results.

Though this example is very simple (and makes incredibly wasteful use of SpiNNaker’s resources), its operation is analogous to the more complex task of building neural simulation software for SpiNNaker. Just as in many neural modelling applications we translate a high-level description of a problem into a graph of application cores connected by routes, map this onto a SpiNNaker machine and handle basic configuration and result retrieval.

Source code on GitHub

Usage

You’ll need to install some Python dependencies before you start:

$ pip install rig numpy matplotlib bitarray

Then make sure your board has been booted, e.g.:

$ rig-boot spinnaker-board-hostname --spin5

Two example circuits are provided which can be executed using one of:

$ python example_xor.py spinnaker-board-hostname
$ python example_flipflop.py spinnaker-board-hostname

These scripts describe (at a high level) a circuit and then run a simulation on an attached SpiNNaker board and then plot the results using matplotlib. The example_xor.py script describes an XOR gate implemented using sum-of-products and produces a plot which looks like the following where the top two waveforms are the inputs to the XOR and the bottom waveform is the output.

XOR Example output

The circuit_sim.py file contains the full Rig-based program which maps the high level circuit description onto a SpiNNaker board and runs the model. This module has a set of complementary tests which demonstrate how to test host-side applications without the need for a SpiNNaker board to be present. The test suite’s dependencies can be installed and the tests executed using:

$ pip install pytest mock
$ py.test test_circuit_sim.py

The spinnaker_applications directory contains the C sources and compiled binaries for the SpiNNaker applications which implement the SpiNNaker-side of the simulation. See the subsection below for details of how to recompile these applications.

SpiNNaker Binary Compilation

Precompiled binaries are included in the repository so you don’t need to compile the binaries in order to play with the program as it stands. If you want to compile the SpiNNaker binaries, after sourcing the setup script in the official spinnaker low-level software release, execute the following quick-hack snippet:

cd spinnaker_applications
for f in *.c; do
    make APP=${f%.c} clean; make APP=${f%.c};
done