Skip to main content

Tasks and concurrency

18. Dispatch Blocks​

Dispatch blocks are the primary mechanism for receiving and processing remote procedure calls (RPCs) from sockets. A dispatch block listens on a socket and routes incoming wavelets to handler functions based on the wavelet's function name.

Basic Syntax​

forever dispatch(socket_name) {
handler_name(type1 arg1, type2 arg2, ...) {
// handler body
}
}

Each handler is a hardware task. wavelet is the reserved handler name for plain data wavelets: the handler runs once per wavelet, so its arguments must fit in one 32-bit wavelet. Two plain 16-bit sends can share one wavelet (see Sockets), and wavelet(xp v) then sees only the lower half: five xp sends of 1 to 5 start it twice, with v = 1 and 3. A handler with any other name receives named messages. It gets a 6-bit task ID, and a call such as sock[] <- store(i, x) sends a control wavelet that carries the ID and the first argument, then data wavelets for the other arguments.

Example​

A sender passes four values to a dispatcher as messages: one store(i, value) per value, then one flush(). The dispatcher runs the handler each message names: store keeps the value at index i, and flush sends the stored values on.

Sender:

xp param N = 4;
socket requests; // to the dispatcher

hp a[n=N] = {1.0, 2.0, 3.0, 4.0};

// One store(i, value) message per element, then one flush() message.
i in [0, N) requests[] <- store(i, a[i]);
requests[] <- flush();

Dispatcher:

xp param N = 4;
socket requests; // messages from the sender
socket results; // stored values, to the next tile

hp a[n=N];

// Each incoming message runs the handler with its name and arguments.
forever main dispatch(requests) {
store(xp i, hp value) { // keep value at index i
a[i] <- value;
}
flush() { // pass the stored values on, in order
i in [0, N) results[] <- a[i];
}
}

Handler Completion​

  • In a handler, continue ends the handler exactly like falling off its end, and break continues with the code after the dispatch. This holds for a dispatch outside any parallel block; a handler of a dispatch inside one ends only by falling off its end.
  • A forever dispatch re-arms after each handler falls off its end or executes continue, and handles the next message. break does not disarm it: the handlers stay armed and a later message is still handled.

Multiple Dispatch Blocks​

Multiple dispatch blocks can run in parallel on different sockets:

parallel {
forever dispatch(rx1) {
wavelet(sp a) {
s_res[0] <- a;
}
}
forever dispatch(rx2) {
wavelet(xp b) {
x_res[0] <- b;
}
}
}

Dispatch with Dependency (Variables Between Handlers)​

Handlers within the same dispatch block can share state:

xp d = 0;
forever main dispatch(rx) {
wavelet(xp i, xp x) {
a[i + d] <- x;
}
setbase(xp offset) {
d = offset;
}
}

Bit-Packed Wavelet Arguments​

Dispatch handlers can use narrow integer types for compact encoding:

// Sender: pack three values into one wavelet
u10 i = 5;
u8 x = 10;
u10 y = 10;
sock[] <- wavelet(i, x, y);

// Receiver
forever dispatch(sock) {
wavelet(u10 i, u8 x, u10 y) {
a[i] <- x;
b[i] <- y;
}
}

Dispatch with Contexts​

Handlers can be organized into named contexts:

forever dispatch(rx) {
context_name {
handler(xp v) { a[0] <- v; }
}
}

19. Execution Blocks​

Code can be grouped into named or unnamed execution blocks:

// Unnamed block — sequential execution
{
x = 1;
y = 2;
z = x + y;
}

// Named block
main {
i in [0, N) a[i] = a[i] + 1;
}

20. Parallel and Serial Blocks​

Parallel Blocks​

Each statement of a parallel block is one arm. An arm that sends or receives runs as a hardware microthread and proceeds as its data arrives. Other arms run on the main thread while those microthreads wait. The statement after the block runs when every arm has finished:

parallel {
i in [0, N) tx[] <- a[i]; // arm 1: send, a microthread
i in [0, N) b[i] <- rx[]; // arm 2: receive, a microthread
i in [0, N) c[i] <+- d[i]; // arm 3: accumulate, on the main thread
}

Serial Blocks​

Serial blocks (the default) execute sequentially:

serial {
step1();
step2();
}

// or simply:
{
step1();
step2();
}

Parallel with Iterators​

Loops can be made parallel:

parallel i in [0, N) {
a[i] <- 1; // the body is one statement
}

A parallel iterator is not a loop for break and continue. A break or continue whose nearest enclosing loop would be the parallel iterator is a compile-time error, whether or not it is conditional and however many arms the body has; use a serial iterator or an explicit while loop instead. Loop control inside a while or serial iterator nested in the body binds to that construct and follows §14.

parallel i in [0, N) {
if (skip[i]) continue; // error: continue cannot bind to a parallel iterator
}
parallel i in [0, N) {
{ j in [0, M) { if (a[i][j] == 0) break; b[i] += 1; } } // ok: binds j
}

21. Filters​

Filters allow selective processing of incoming wavelets in dispatch blocks by matching against a field value.

Range Filter​

filter f[range] = {.min = 0, .max = 4};

forever dispatch(sock) {
wavelet(xp i, hp x) with f(i) {
// only called when min <= i < max
b[i] <- x;
}
}

A range filter applies only to a handler named wavelet; on a handler with another name it is rejected (range filter is only supported on wavelet).

Counter Filter​

filter f[counter] = {.limit = 3, .socket = rx};

A counter filter passes one wavelet, drops the next .limit, and repeats: of the u32 values 1 to 12 sent to rx, the kernel receives 1, 5 and 9. It counts wavelets, and two 16-bit sends can share one wavelet (see Sockets). Name the socket with .socket: a filter that names none is configured on color 0, not on a socket of the kernel.

Filters can also be associated with sockets:

filter drop_inpad[range] = {.min = 0x0, .max = 0xfffe, .socket = rx};

The .socket initializer must name a socket and may appear at most once. A filter gates one socket: repeating the same association through dispatch(socket) with filter is idempotent, while binding the filter to a second socket is rejected.


22. Locks and Synchronization​

Lock Declaration​

lock job_ready;
lock retry_ready, jobs_pending, jobs_resolved;

Wait and Signal​

// Block until the lock is signaled
wait job_ready;

// Release the statement that waits on the lock
signal jobs_pending;

Locks synchronize the arms of one parallel block on a PE: an arm that waits blocks until another arm of the same block signals, under the rules below.

Signal and Wait Rules​

  • Outside a parallel block, statements already run in program order, so a wait that follows a signal of the same lock needs no synchronization. A wait with no earlier signal of its lock is rejected (wait on lock 'l' before anything signals it).
  • Between the arms of a parallel block, a lock is a run-time hand-off: the arm that waits blocks until an arm of the same block signals. A lock is waited on at most once in a parallel block, and the wait must not be under a loop or branch of its arm. The signal must come from an arm of that same block, not from code outside it or from a nested parallel block. Each violation is rejected.
  • Naming an undeclared lock reports lock X is not defined.
lock ready;

parallel {
{ a[0] = 1; signal ready; }
{ wait ready; b[0] = a[0]; }
}

23. Termination Conditions​

A DSD normally runs its programmed length to completion. until <termcond> instead predicates the loop with a premature DSD-termination condition. The default (no until) is run-to-completion:

until …triggermeaning
(none)run to completionrun the full programmed length (default)
until controlcontrol waveletterminate when a control wavelet arrives on the fabric-read DSD
until emptyFIFO drainedterminate as soon as the source FIFO empties

A termcond is only meaningful when the instruction reads from a fabric (for control) or from a FIFO (for empty): the premature-termination triggers exist only on the read side of a stream DSD.

until empty may be written on a single transfer or attached to an iterator-range loop, so every iteration of the loop inherits the condition. until control applies to a fabric read inside an iterator loop; on a bare instruction it is rejected:

until empty tx[] <- ff[]; // run until the source FIFO is empty
until empty i in [0, N) tx[] <- ff[]; // termcond on the loop
i in [0, N) until control dst[i] <- rx[]; // run until a control wavelet arrives

The control wavelet that ends the loop stays at the head of the input queue, so the next read on that socket receives it as data. Remove it with one 32-bit read into a u32, such as cw = (u32)rx[];, before reading data again.