Topic — Locks, until conditions, rings and timer tasks
Needs: Lesson 5 — One core, many activities: parallel and FIFOs, Lesson 7 — Tasks: code that runs when a wavelet arrives.
The kernels of the main lessons know how many values they receive. This topic is about activities on
one PE that do not: a message whose length the receiver learns only when it ends, a FIFO that holds
however many values a loop left in it, a stream that never ends, and code that runs at a fixed
interval rather than when data arrives. It also shows how to order two arms of a parallel block
with a lock.
What you will be able to do
- Receive a message of unknown length that ends with a control wavelet, and remove the control wavelet from the queue.
- Count the values in a FIFO, and read it until it is empty.
- Order two arms of a
parallelblock with a lock, and know what Carbide does with the lock. - Keep the latest values of an endless stream in a ring.
- Run code at a fixed interval with a timer task, and know when it can run.
The program
Two PEs in a row share one color, c, from clock to sink.
+-----------------------------+ c +-----------------------------------+
| clock (x=0) | ------------> | sink (x=1) |
| main: sends 100 .. 99+K, | [C>R] [L>C] | 1. until control: message -> FIFO |
| then an end marker | | count it, read it out |
| timer task, every ~216 | | 2. parallel + lock: first B |
| cycles: sends 0, 1, 2, ... | | samples, then their sum |
+-----------------------------+ | 3. ring: the latest R samples |
+-----------------------------------+
clocksends a message ofKvalues, 100 to100+K-1, and then a control wavelet that marks its end. From then on its timer task sends a sample at a fixed interval: 0, 1, 2, and so on.sinkdoes not knowK. It receives the message until the end marker, counts it and copies it out. It then receives the firstBsamples and adds them up, and keeps the latestRsamples in a ring for as long as the run lasts.
The timer never stops, so the run ends at the cycle limit of 20,000, as forever programs do
(lesson 5). check.py then reads both PEs' memories. For the tutorial row (K=5, MAX=16, B=4,
R=8) it expects the message 100 … 104, a count of 5, the end marker 0x002a0000, the first
samples 0, 1, 2, 3 with the sum 6, and a ring that holds 8 consecutive sample numbers.
The clock: clock.w
xp param K;
socket tx;
u16 tsc[1] address(0x7b30);
u16 tsc_control[1] address(0x7b34);
xp ticks[n=1];
xp smem[n=4];
xp ring stamps = {.mem = smem};
main {
tsc_control[0] <- tsc_control[0] | 4;
∀i in [0, K) tx[] <- 100 + i;
tx[] <- control(0x002a0000);
}
[[timer(on), timer.limit(200)]]
forever {
tx[] <- ticks[0];
stamps[] <- tsc[0];
ticks[0] += 1;
}
tx[] <- control(w); sends the 32-bit word w as a control wavelet, a wavelet with its control
bit set. A router reads a control wavelet's word as commands for its switches (lesson 10); here all
the commands are zero, which means no operation, so the routers pass it on like data. Bits 21 to 16
are the task ID that a control wavelet carries (lesson 7). This program starts no task with it; 42
(0x2a) only makes the word easy to recognise. The other half of the file, the timer, is
explained below.
Receiving until an end marker: until control
i in [0, MAX) until control ff[] <- rx[];
cw[0] = (u32)rx[];
n[0] = ff.count;
until empty i in [0, MAX) msg[i] <- ff[];
until control ends the loop when a control wavelet reaches the front of the socket's input queue,
or after MAX values, whichever comes first. The loop writes each value into the FIFO ff, whose
array fmem has MAX elements.
The control wavelet that ends the loop is not removed from the queue. The next read on the socket
receives it as data. cw[0] = (u32)rx[]; removes it with one 32-bit read, and keeps its word:
check.py finds 0x002a0000 in cw.
The full row sends K=16, exactly MAX. The loop then ends because its count runs out, before it
sees the marker, and the marker is still waiting in the queue. The same 32-bit read removes it, so
the same code handles both cases.
In the listing, test-tutorial/sink_artefacts/sink2.yeti, the receive's descriptor has its
terminate-on-control bit set, .terminate=1:
recv rx_r_X0 register(T13) = { .length=16, .utid=0, .transform=0, .fa=0, .fi=0, .lp=1, .terminate=1, .upr=HIGH, .qid=0, .au=ACTIVATE, .ondone=9, .ta=ACTIVATE, .onterm=9, };
.upr=HIGH makes the loop one microthread (lesson 5). .ondone=9 and .onterm=9 say that when it
finishes, by count or by the control wavelet, it activates task 9, which continues with the next
statements.
Counting and draining a FIFO: ff.count and until empty
ff.count is the number of values in the FIFO: 5 in the tutorial row, 0 in the empty row, 16 in
the full row. It compiles to one instruction, qdepth R2 = [T14];.
until empty i in [0, MAX) msg[i] <- ff[]; reads the FIFO until it is empty, at most MAX times. It
reads exactly the values the message left, without using the count. In the empty row the FIFO is
empty from the start, and the loop writes nothing. A plain loop of MAX reads would wait forever for
values that never come (lesson 5).
The FIFO's descriptors show both behaviours. Its read side has .ra0=1, .ra1=0: an empty FIFO ends
the instruction. Its write side has .wa0=0, .wa1=1: a full FIFO makes the writer wait.
Ordering two arms: a lock
parallel {
{
wait got;
∀i in [0, B) total[0] <+- first[i];
}
{
i in [0, B) first[i] <- rx[];
signal got;
}
}
lock got; declares the lock. wait got; holds back the rest of its arm until another arm of the
same parallel block has executed signal got;. Here the first arm adds up first, and the lock
makes it wait until the second arm has received the B samples.
What Carbide makes of it: it runs these two arms on the main thread, one after the other.
Without a lock it keeps the order you write, so the sum would run first, on an empty first. With
the two lock lines removed, total is 0. With the lock, Carbide puts the signalling arm first: in the
listing the receive of first (source line 29) comes before the sum's loop (line 26). The receive
also runs on the main thread here, .upr=NONE.
The lock's rules, which Carbide checks:
- a
parallelblock waits on a lock at most once; - the
waitmust not be inside a loop or a branch of its arm; - the
signalmust come from an arm of the same block.
An arm that never ends after its signal. Carbide runs the whole signalling arm
before the waiting arm, not just the part before signal. With the endless ring receive of the next
section placed in the second arm after signal got;, the first arm never ran and total stayed 0.
That is why the ring receive comes after the parallel block.
Keeping the latest values: a ring
xp hmem[n=R];
xp ring hist = {.mem = hmem};
i in [0, ∞) hist[] <- rx[];
A ring is a circular buffer over an array, like a FIFO, but it has no full or empty state. A write
goes into the next slot, wraps to slot 0 after the last one, and never waits: it overwrites the
oldest value. The array therefore always holds the latest R values received.
i in [0, ∞) is an endless loop: the receive's length is 0x7fff, the length the hardware treats as
infinite. Neither of the obvious alternatives works here. A FIFO would fill after R values and stop
the receive. An array written with a[i] <- rx[] would be written past its end, into whatever follows
it in memory.
The ring's first value is sample B, so sample m lands in slot (m - B) mod R. check.py checks
that hmem holds R consecutive sample numbers in those slots, for example
84, 85, 86, 87, 88, 89, 90, 91 after 92 ticks. Which samples these are depends on how many ticks the
run had time for; the check accepts any R consecutive ones.
To read a ring in a program, declare a second ring over the same array, xp ring rd = {.mem = hmem};,
and read rd[]. Each ring has its own position, and both start at slot 0, so the reader does not
follow the writer. One ring cannot be both written and read.
Running code at an interval: a timer task
[[timer(on), timer.limit(200)]]
forever {
tx[] <- ticks[0];
stamps[] <- tsc[0];
ticks[0] += 1;
}
timer(on) on a forever loop makes the loop's body a task, the timer task (task 30). When
execution reaches the statement, Carbide starts the PE's 16-bit timer at timer.limit, 200 here. The
timer counts down once per cycle, and when it reaches 0 the timer task becomes ready. The body runs,
and Carbide starts the timer again at its end. The listing of clock shows the start:
imov16 R4 = 0x00c8; //@../clock.w:17
st16 [__tungsten_timer_config] = R4; //@../clock.w:17
0x00c8 is 200, and __tungsten_timer_config is the timer's register, at address 0x7e03.
The body writes the time of each tick into the ring stamps, and check.py prints the intervals
between the last four: 216 cycles, the limit plus the time the body takes. The test does not check them.
When the timer task can run:
- It does not interrupt another task. A task runs until it ends (lesson 5), so the timer task starts only when nothing else is running. A long task delays it.
- It starts when execution reaches the statement. Here
mainsends the message first, and only then does the timer start. - Nothing after it runs. The timer statement is a
foreverloop, so the code after it in the program never runs. Write it last.
The rules for the timer statement:
timer.limitis required, from 1 to 65535.- A program has one timer statement, outside any function, and no
breakorcontinuemay leave its loop. - The body must not carry a scalar variable from one run to the next. Keep such state in an array
element, as
ticks[0]does. - A loop in the body that uses its iterator, such as
j in [0, 7) b[j+1] <- b[j] + j;, fails to compile with[builtin-setjt] precedes the branch into the loop its iterator ends. So did awhileloop placed after the timer statement. timer.repeat(on)is meant to restart the timer at the start of the body. The period is the same with or without it.
What the hardware does
- Terminate on control. A descriptor that reads from an input queue has a terminate-on-control bit. With it set, the instruction ends when a control wavelet is at the front of the queue and values remain to be read. The control wavelet stays in the queue. If the instruction reads its last value first, it ends normally and does not look at the next wavelet. Either way the instruction can activate a task when it ends.
- FIFO descriptors. A FIFO is memory with a read position and a write position. Each side's
descriptor says what happens when the FIFO is empty or full: wait, end the instruction, jump, or
stop with a fault. A
read that ends on empty is
until empty. - Rings. A ring is the same circular buffer with no full or empty state.
- The timer. The PE has one interrupt timer, a 16-bit register at
0x7e03that counts down. When it reaches 0, task 30 becomes ready. Tasks run one at a time, each to its end (see Tasks).
Run it
In your copy of the tutorial (see Lesson 1):
cd tutorial/ext-single-pe/locks-and-timers
make TEST='*'
Each of the three rows prints a PASS line. The check is at the end of each row's log:
sed -n '/check.py [0-9]/,$p' test-tutorial/make-log
message: 5 values [100, 101, 102, 103, 104], end marker 0x002a0000: right
first 4 samples [0, 1, 2, 3], total 6: right
ring after 92 ticks: [84, 85, 86, 87, 88, 89, 90, 91], newest 91: right
timer period, last three ticks: [216, 216, 216] cycles
A failing row prints FAIL, and the line that says WRONG shows which part went wrong.
Pitfalls
- The control wavelet stays in the queue. Read it with one 32-bit read, assigned to a variable:
cw[0] = (u32)rx[];. A bare(u32)rx[];statement stops the compiler with an internal error. A 16-bit read takes only half of it, and the other half arrives as data. - A message longer than the loop. With
K=17andMAX=16, the loop stopped after 16 values. The 32-bit read then took the 17th value (116) and the low half of the marker, and the marker's high half, 42, arrived as the first sample. - An endless arm after a
signal. The waiting arm then never runs. Put the endless part after theparallelblock. - A lock does not make arms concurrent. Carbide decides how the arms run; here it ran them one
after the other. Check the listing for
.upr=HIGHwhere you need a transfer to overlap other work. - The timer statement must come last. Code after it never runs, and a loop after it does not compile.
- The timer waits for running tasks. Its period is a lower bound, not a guarantee.
Exercises
- Add a row
long K=17 MAX=16 B=4 R=8 ARCH=sdr. Predict whatcheck.pyreports, runmake TEST=long, and explain each line that saysWRONG, the ring's included. Then change the row so that a message of up to 32 values works, and rerun. - Remove the two lock lines from
sink.w, predicttotal, and check. Then put them back and swap the two arms; predict whether the result changes.
Next
This is the last topic of the group. The next group, Algorithms, starts with Topic — Matrix multiplication across PEs, which multiplies two matrices streamed across a grid of PEs, as a systolic array.
References
Source code
The Tungsten and Paint files of this example, as the toolset installs them under share/tutorial/ext-single-pe/locks-and-timers/. Select a line number to link to that line.