Complete examples
Appendix A: Complete Example — Vector Addition
A simple program that adds a constant to every element of an array:
xp param N;
xp a[n=N]; // no initialiser — data loaded via miset
xp b = 1;
i in [0, N) a[i] <- a[i] + b;
Appendix B: Complete Example — Send and Receive
A pair of programs communicating through sockets.
Sender (send.w):
xp param N = 4;
socket tx;
xp a[n=N] = {1, 2, 3, 4};
i in [0, N) tx[] <- a[i];
Receiver (recv.w):
xp param N = 4;
socket rx;
xp a[n=N];
i in [0, N) a[i] <- rx[];
Layout (bench.paint): one color carries tx of the sender to rx of the
receiver, its east neighbor. After simulation, the receiver's a is 1, 2, 3, 4.
let c = color()
let send = tile()
send : code('send [tx:c])
send : paint(c [C>R])
send : name('send)
let recv = tile()
recv : code('recv [rx:c])
recv : paint(c [L>C])
recv : name('recv)
hstack(send recv)
Appendix C: Complete Example — Dispatch (RPC)
The Section 18 example as a complete program on three tiles, side by
side: the sender sends store and flush messages to the dispatcher, and the dispatcher
sends the stored values to the receiver. After simulation, the receiver's a equals the
sender's.
Sender (send.w):
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 (dispatch.w):
xp param N = 4;
socket requests; // messages from the sender
socket results; // stored values, to the receiver
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];
}
}
Receiver (recv.w):
xp param N = 4;
socket results;
hp a[n=N];
i in [0, N) a[i] <- results[];
Layout (bench.paint): one color carries requests from the sender to the dispatcher,
another carries results on to the receiver.
let reqcolor = color()
let rescolor = color()
let send = tile()
send : code('send [requests:reqcolor])
send : paint(reqcolor [C>R])
send : name('send)
let dispatch = tile()
dispatch : code('dispatch [requests:reqcolor results:rescolor])
dispatch : paint(reqcolor [L>C])
dispatch : paint(rescolor [C>R])
dispatch : name('dispatch)
let recv = tile()
recv : code('recv [results:rescolor])
recv : paint(rescolor [L>C])
recv : name('recv)
hstack(send dispatch recv)
Appendix D: Complete Example — Cosine Similarity
A kernel that computes the three sums behind the cosine similarity of two
vectors: their dot product and the squared norm of each. It is a sketch: it
compiles, but it does not run on its own. Each tile
holds N pairs of rows bufA[n], bufB[n] of L elements. For each pair it
computes three partial sums and combines each with the other tiles' over the
fabric: hr[] <+- acc receives a partial sum on hr, adds acc and sends the
result on along hr, and the full sum comes back on bc. Two parts are not
shown: the kernel of the first tile in the hr chain, which sends acc
instead of adding a received sum to it, and the layout, which chains hr
through the tiles and broadcasts the total on bc. The host computes each
cosine as dotAB[n] / sqrt(normA[n] * normB[n]).
bufA.base <- abase points bufA at row n, so bufA[0][i] reads that row;
an sp element takes two words, so abase <+- 2*L steps one row.
xp param N;
xp param L;
socket hr;
socket bc;
sp bufA[n=N][l=L];
sp bufB[n=N][l=L];
xp abase = bufA.base;
xp bbase = bufB.base;
sp dotAB[n=N];
sp normA[n=N];
sp normB[n=N];
sp acc;
serial {
n in [0, N) {
bufA.base <- abase;
bufB.base <- bbase;
acc <- 0;
i in [0, L) acc <+- bufA[0][i] * bufB[0][i];
hr[] <+- acc;
dotAB[n] <- bc[];
acc <- 0;
i in [0, L) acc <+- bufA[0][i] * bufA[0][i];
hr[] <+- acc;
normA[n] <- bc[];
acc <- 0;
i in [0, L) acc <+- bufB[0][i] * bufB[0][i];
hr[] <+- acc;
normB[n] <- bc[];
abase <+- 2*L;
bbase <+- 2*L;
}
}
Appendix E: Complete Example — 3×3 Convolution
A stencil computation with halo exchange:
xp param Y = 8;
xp param X = 8;
hp A[r=3][s=3];
hp u[yy=Y][xx=X];
hp v[yy=Y+2][xx=X+2] = {};
let i in [0, Y);
let j in [0, X);
let r in [0, 3);
let s in [0, 3);
socket a, b, c, d;
hp α;
{
∀r ∀s {
α <- A[r][s];
∀i ∀j v[i+r][j+s] <+- u[i][j] * α;
}
// Reduce right/left halos: send each halo column to the neighbor
// that owns it, and add the neighbor's halo into the edge column.
parallel {
∀i in [0, Y+2) b[] <- v[i][0];
∀i in [0, Y+2) a[] <- v[i][X+1];
∀i in [0, Y+2) v[i][1] <+- a[];
∀i in [0, Y+2) v[i][X] <+- b[];
}
// Reduce top/bottom halos (the corners arrived with the columns)
parallel {
∀j in [1, X+1) d[] <- v[0][j];
∀j in [1, X+1) c[] <- v[Y+1][j];
∀j in [1, X+1) v[1][j] <+- c[];
∀j in [1, X+1) v[Y][j] <+- d[];
}
}
v holds the tile's Y×X interior at rows and columns 1…Y and 1…X,
with a one-element halo around it. Each tile sends a halo strip to the
neighbor that owns those cells and adds the strip it receives into its
own edge row or column: a and b run east and west over every row
0…Y+1, so the corners travel with the columns, and c and d then run
between rows over the interior columns 1…X.
v starts at zero (= {}) because the convolution accumulates into it.
The kernel is the program of a tile that has a neighbor on all four sides. On a grid with edges, a tile on an edge has no neighbor on that side, so its receive from that side never completes: an edge tile needs a kernel that leaves out the missing sides, and the layout needs routes for the four sockets. The 9-point stencil in the Paint examples lays out a grid with one kernel for its edge tiles and another for the rest.