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,
continueends the handler exactly like falling off its end, andbreakcontinues with the code after the dispatch. This holds for a dispatch outside anyparallelblock; a handler of a dispatch inside one ends only by falling off its end. - A
foreverdispatch re-arms after each handler falls off its end or executescontinue, and handles the next message.breakdoes 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
parallelblock, statements already run in program order, so awaitthat follows asignalof the same lock needs no synchronization. Awaitwith no earliersignalof its lock is rejected (wait on lock 'l' before anything signals it). - Between the arms of a
parallelblock, 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 aparallelblock, and thewaitmust not be under a loop or branch of its arm. Thesignalmust come from an arm of that same block, not from code outside it or from a nestedparallelblock. 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 … | trigger | meaning |
|---|---|---|
| (none) | run to completion | run the full programmed length (default) |
until control | control wavelet | terminate when a control wavelet arrives on the fabric-read DSD |
until empty | FIFO drained | terminate 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.