Lesson 1 — Hello, wafer!
This lesson builds, runs and checks the smallest complete program: one PE adds 1 to each element of an array. Before the code, here is how a program for the Cerebras machine is put together.
What you will be able to do
- Build and run a one-PE program with
make, and tell a passing test from a failing one. - Say what each line of a Tungsten kernel and of its Paint layout does.
- Find an array in
bench.mapand read its values withmiget. - Recognize the two silent traps in this program: a
letwithout its;, and an initializer that is longer than its array.
The machine, the languages and the compiler
The tutorial programs the Cerebras Wafer-Scale Engine (WSE), here the WSE-3, the processor of a CS-3 system. The WSE is a grid of processing elements (PEs). Each PE has three parts:
- a compute engine (CE), a processor that runs the PE's program;
- 48 KB of memory, which holds the program's instructions and its data together;
- a router, which connects the PE to its four neighbours.
A PE shares no memory with any other PE. PEs cooperate only by sending data to each other through their routers, from lesson 4 on.
A program for the WSE is written in two languages:
- Tungsten describes what one PE does: its data, its loops, and what it sends and receives. A
Tungsten source file (
.w) is a program for one PE, called a kernel. - Paint places kernels on a rectangular region of the wafer. It says which PEs run each kernel, gives each kernel its parameters, and connects the kernels to each other. One kernel can run on many PEs at once.
The Tungsten compiler is called Carbide (carbide). Paint runs Carbide on each kernel of a
layout and combines the results into one machine image for the whole region. Each lesson's Makefile
runs Paint and the simulator for you, so the lessons never call carbide or paint by hand. To run
them yourself, see Compiler usage
and Paint's command-line options.
The program
One PE holds an array a of N 16-bit integers and adds 1 to each element:
+-------------------------------+
| PE at column 0, row 0 |
| CE: runs add.w |
| memory: a = {0, 1, 2, 3} |
| router: not used |
+-------------------------------+
The test row tutorial sets N=4, and the result is a = {1, 2, 3, 4}. The row n8 sets N=8,
and the result is a = {1, 2, 3, 4, 1, 1, 1, 1}.
The kernel: add.w
xp param N;
xp a[n=N] = {0, 1, 2, 3};
let i in [0, N);
∀i a[i] <- a[i] + 1;
| Line | Meaning |
|---|---|
xp param N; | A compile-time parameter N. Paint gives it a value; the program cannot change it. xp is the 16-bit signed integer type. Lesson 2 lists the other types. |
xp a[n=N] = {0, 1, 2, 3}; | An array a of N elements. n names its dimension and N is its size. The initializer gives the first four elements; any elements after them start at 0. |
let i in [0, N); | An iterator i over the half-open range 0 to N−1. |
∀i a[i] <- a[i] + 1; | For every i, a[i] gets a[i] + 1. |
The statements at the top of a kernel run once, from top to bottom. The kernel ends when its last statement ends.
<- and = are the same assignment: a[i] = a[i] + 1; compiles to the same code. Both spellings
appear in the lessons. An array's initializer list needs =: xp a[n=4] <- {0, 1, 2, 3}; is a
syntax error. A scalar declaration takes either sign: xp s <- 5; and xp s = 5; are the same.
∀ ("for all") runs the statement after it once for each value of the iterator. A loop can also
declare its range inline, with no let and no ∀, in plain ASCII:
i in [0, N) a[i] <- a[i] + 1;
The Unicode support table gives the ASCII spelling of each symbol.
The ; after let matters
The ; ends the let declaration. Without it, let i in [0, N) becomes a loop over the statement
that follows, so the next line runs once for each i:
let i in [0, N)
∀i a[i] <- a[i] + 1;
This still compiles, with no error or warning. With N=4 the ∀i statement now runs four times,
and a ends as {4, 5, 6, 7}.
The layout: bench.paint
let xadd = tile()
xadd : code('add [N:N])
| Line | Meaning |
|---|---|
let xadd = tile() | tile() makes a region of one PE, at column 0, row 0. let xadd = gives the region a name in Paint. |
xadd : code('add [N:N]) | The PE runs the kernel add, from add.w. [N:N] sets the kernel's parameter N to Paint's variable N, which the test row supplies. |
The layout has one PE, so it has no colors or routes. Lesson 4 adds them.
The test
test.rc
tutorial N=4 ARCH=sdr
n8 N=8 ARCH=sdr
Each line is one test. Its first word, such as tutorial, labels it. N=4 sets the variable N,
and ARCH=sdr builds for the WSE-3. Every row must set each variable the Makefile names in
PAINTVARS. A row without N= stops Paint with Command-line error: 'source' not specified.
The Makefile
include $(FLUTEMK)
include $(TESTMK)
BENCH_NAME=add
SIMFLAGS=cycles=1000 $(SIMOPTS)
PAINTVARS=N
PAINTFLAGS=lib=.. $(PAINTOPTS) verbose arch=${ARCH}
PIPELINEFLAGS=arch=${ARCH}
MODE ?= sim
all: test
clean: cleanup
test: test/$(MODE)
test/$(MODE): out-ref.bin out-$(MODE).bin
cmp $^
in.bin: bench.img bench.map
miget image=$< map=bench.map symbol=a data=$@
out-%.bin: bench-%.core bench.map
miget image=$< map=bench.map symbol=a data=$@
out-ref.bin: in.bin
python3 ../ref.py $< $@
include $(FLUTEMK)brings in the rules that build and simulate a layout, andinclude $(TESTMK)the rules that run the rows oftest.rc.- Each row builds in its own directory, such as
test-tutorial/, solib=..and../ref.pypoint back at the lesson's files. PAINTVARS=Npasses the row'sNto Paint.SIMFLAGS=cycles=1000stops the simulator after 1,000 cycles. This kernel needs far fewer: its last write to memory happened at cycle 14. A cap that is too small cuts off a program that needs more cycles. The test checks memory when the simulator stops, so a pass does not prove that the kernel finished.PIPELINEFLAGSis for the debug views of Debugging kernels.
What make runs, in order
For the row tutorial, in test-tutorial/:
- Paint reads
bench.paintand calls Carbide to compileadd.wwithN=4. It writes the machine imagebench.objand the symbol mapbench.map. milinkfinishes the image, and the build copies it tobench.img.migetreadsafrombench.img, the memory as it is before the run, intoin.bin.ref.pyreadsin.bin, adds 1, and writes the expected result toout-ref.bin.- The simulator runs
bench.imgand writes the final memory of every PE tobench-sim.core. migetreadsafrombench-sim.coreintoout-sim.bin.cmpcomparesout-ref.binwithout-sim.bin. The test passes when they are equal, byte for byte.makedeletesbench-sim.coreandbench.obj, which it treats as intermediate files.bench.img,bench.map,in.bin,out-sim.binandmake-logstay.
One line of bench.map
bench.map lists every symbol of the image. The line for a in test-tutorial/bench.map is:
a [ n<4 ] ( 0x0+n 0 0 )
It says that a has one dimension, n, of size 4, and that element n is the 16-bit word at
address 0x0+n of the PE at column 0, row 0. miget uses this line to find a. The other lines
name the PE's registers and the compiler's own symbols.
ref.py
import sys
import numpy as np
in_name = sys.argv[1]
outfile = sys.argv[2]
a = np.fromfile(in_name, dtype=np.int16)
b = np.array([x + 1 for x in a])
b.astype('int16').tofile(outfile)
The reference reads the initial array from the image, not from add.w, adds 1 in Python, and writes
the expected output.
What the hardware does
The CE runs the kernel from its own memory. Memory is addressed in 16-bit words, not bytes: the
48 KB are 24,576 words, at addresses 0x0000 to 0x5FFF. bench.map shows that a sits at word 0,
and the kernel's instructions sit in the same memory. The
memory map shows the rest of the address
space.
Carbide compiles the whole loop ∀i a[i] <- a[i] + 1; into a single instruction, which adds 1 to all
N elements. Lesson 3 shows how to read that instruction in the compiler's listing, and what it costs.
When the last statement has run, the PE has nothing left to do. When the simulation ends, the
simulator writes each PE's memory to the core file, which miget reads.
Run it
Copy the SDK's tutorial, <sdk>/share/tutorial in the SDK's directory <sdk>, to a directory of
your own, as the run guide shows.
Every later lesson works in this copy. Then run the row labelled tutorial:
cp -r <sdk>/share/tutorial tutorial
cd tutorial/main-1-one-pe/01-hello-wafer
make TEST=tutorial
TEST= picks rows of test.rc: TEST=tutorial the row labelled tutorial, TEST=2 the second row,
and TEST='*' every row (see Test infrastructure).
A passing test prints one line, such as
01-hello-wafer : 0:03.73 PASS tutorial [ N=4 ARCH=sdr ].
To see the values themselves, make the core file again in the row's directory and read it:
cd test-tutorial
make -f ../Makefile VPATH=.. TEST=tutorial bench.map bench-sim.core
miget image=bench-sim.core map=bench.map symbol=a decimal
cd ..
The second line makes bench-sim.core and bench.map again and keeps them. miget prints
1 2 3 4. Its decimal output is unsigned: a negative xp such as -2 prints as 65534.
A failing test prints FAIL instead. test-tutorial/make-log holds every command make ran and its
output, including cmp's report of the first byte that differs, for example
out-ref.bin out-sim.bin differ: byte 1, line 1. To see a failure, change + 1 to + 2 in add.w,
run make clean, and run the test again: ref.py still adds 1. Then restore + 1 in add.w and
run make clean before you go on.
Pitfalls
- Run
make cleanafter every edit.makereuses the build intest-tutorial/when it exists, and reports the old result. - The
;afterlet, shown above. - An initializer is not checked against the array's size. A list longer than the array is cut off without a diagnostic (see the exercise).
- A list of one value fills every element, unlike C:
xp a[n=4] = {5};holds 5, 5, 5, 5. The Tungsten overview's Rules That Differ from C lists this rule with the others.
Exercises
- Run the
n8row withmake TEST=n8, and explain the last four values ofa. - Add a row
n3 N=3 ARCH=sdrtotest.rc. Predicta, then run the row. Carbide drops the fourth initializer value without a diagnostic, and the test still passes. Why does it pass, and why is that dangerous? (Look at whereref.pygets its input.) - Delete the
;afterlet i in [0, N), predict the result, runmake clean, and run thetutorialrow.
Next
Lesson 2 — Values, control flow and functions writes ordinary code on one
PE: types, if, while and functions.
References
Source code
The Tungsten and Paint files of this example, as the toolset installs them under share/tutorial/main-1-one-pe/01-hello-wafer/. Select a line number to link to that line.