Skip to main content

How to Create a New Example

This guide walks you through creating a new Tungsten + Paint example from scratch, step by step.


Step 1: Create the Directory​

mkdir my-example
cd my-example

Create the directory anywhere; the build needs only the setup in the tutorial's prerequisites, with FLUTEMK and TESTMK set.

Step 2: Write the Tungsten Kernel(s)​

Create one .w file per PE role. For a single-PE example, you need just one file. For several PEs, create a file for each distinct role (e.g., send.w, compute.w, recv.w).

Single-PE template: my_kernel.w​

/* my_kernel.w — describe what this kernel does */

xp param N; /* parameters from Paint */

xp a[n=N] = {0, 1, 2, 3}; /* input data, for N=4 */
xp b[n=N]; /* output data */

let i in [0, N);
∀i b[i] <- a[i] * 2; /* your computation */

Multi-PE template​

send.w — sends data to the next PE:

xp param N;
socket tx;
xp a[n=N] = {0, 1, 2, 3}; /* for N=4 */

let i in [0, N);
∀i tx[] <- a[i];

recv.w — receives data from the previous PE and doubles it, as the single-PE template does:

xp param N;
socket rx;
xp b[n=N];

let i in [0, N);
∀i b[i] <- rx[] * 2;

Message-Passing Template​

For MP-based communication (no static color routes):

sender.w — sends via coordinate-based message passing:

xp param dst_col; // destination column (row 0), from Paint
xp param my_value; // value to send, from Paint
socket mp_sock; // bound to color(bank) in Paint
xp row_id[1] address(0x7C45); // own row coordinate

union mp_header {
u32 u;
xp w[2];
};

union payload {
u32 u;
xp w[2];
};

{
// north (0x80) only if the destination has a smaller row ID, south
// (0x40) only if it has a larger one; neither on the same row
xp dir = 0;
if (row_id[0] > 0) dir <- 0x80;

union mp_header hdr;
hdr.w[1] <- dst_col << 4; // destination column; row 0
hdr.w[0] <- 1 | dir; // direction, len=1

union payload data;
data.w[0] <- my_value;
data.w[1] <- 0;

mp_sock[] <- hdr.u;
mp_sock[] <- data.u;
}

receiver.w — receives MP message:

socket mp_sock;
xp result[n=1];

union mp_header { u32 u; xp w[2]; };
union payload { u32 u; xp w[2]; };

{
union mp_header hdr;
hdr.u <- mp_sock[];

union payload data;
data.u <- mp_sock[];

result[0] <- data.w[0];
}

Critical: All wavelets on MP colors must be 32 bits. Never send raw xp on an MP socket — always send a u32. A union is one way to build it, and u32 shifts or a (u32) cast work too.

Design Principle: One Kernel Per Role​

Never duplicate a Tungsten kernel just to change initialisation data. If several PEs run the same computation but with different data (e.g., different rows of a matrix), write one kernel with uninitialised arrays and use miset to load per-PE data after compilation.

Bad practice — duplicating kernels:

mul0.w -> hp row[k=K] = { 1.0, 2.0 }; # DON'T
mul1.w -> hp row[k=K] = { 3.0, 4.0 }; # DON'T

Good practice — single kernel + miset:

/* compute.w — one file for all compute PEs */
hp row[k=K]; /* uninitialised — filled by miset */
# Makefile — load different data per PE into the linked image
MISET = miset image=$@ out=$@ map=bench.map

# miset reads bench.map, so the rule lists it
bench.img: bench.obj row0.bin row1.bin bench.map
cp $< $@
$(MISET) symbol=mul0.row order="k" data=row0.bin
$(MISET) symbol=mul1.row order="k" data=row1.bin

The topic Matrix multiplication across PEs is a complete working example of this pattern.

An array of a 32-bit type (sp, u32) has a second dimension in the map, _word, for the two 16-bit words of each element, low word first. List it in order, as in order="k _word"; with order="k", miset stops with row0.bin: too long.

Step 3: Write the Paint Layout​

Create bench.paint to describe PE placement and routing. Paint's tile() makes one PE.

Single-PE​

let t = tile()
t : code('my_kernel [N:N])

Two PEs with communication​

let c = color()

let send = tile()
send : code('send [tx:c N:N])
send : paint(c [C>R])
send : name('send)

let recv = tile()
recv : code('recv [rx:c N:N])
recv : paint(c [L>C])
recv : name('recv)

hstack(send recv)

Message-Passing Layout​

let b0 = bank(0)
let b0color = color(b0)

let sender = tile()
sender : code('sender [mp_sock:b0color dst_col:0 my_value:42])
sender : mp(b0)
sender : name('sender)

let receiver = tile()
receiver : code('receiver [mp_sock:b0color])
receiver : mp(b0)
receiver : name('receiver)

hstack(receiver sender)

Key: every PE on a message's path needs mp(b0), including the PEs that only pass it on.

Checklist for Paint​

  • One color() per independent data stream
  • code() binds all sockets and parameters from the .w file
  • paint() rules for each color on each PE
  • For MP: bank(), color(bank), and mp(bank) on every PE involved
  • name() on every region of a multi-PE layout (miget symbols are <region>.<variable>)
  • Layout combinator (hstack, vstack, union) to position the PEs

Step 4: Write the Makefile​

Standard template (with input extraction)​

include $(FLUTEMK)
include $(TESTMK)

BENCH_NAME=my_kernel

# The cycle cap; a test.rc row or the command line can set CYCLES
CYCLES ?= 1000
SIMFLAGS=cycles=$(CYCLES) $(SIMOPTS)
# Paint variables taken from the test.rc row
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 $^

# Extract input data from compiled image
in.bin: bench.img bench.map
miget image=$< map=bench.map symbol=a data=$@

# Extract output from simulation core dump
out-%.bin: bench-%.core bench.map
miget image=$< map=bench.map symbol=b data=$@

# Generate reference output
out-ref.bin: in.bin
python3 ../ref.py $< $@

For the two-PE layout, each symbol starts with the name that name() gives its region in Step 3. Replace the template's two miget rules with these:

in.bin: bench.img bench.map
miget image=$< map=bench.map symbol=send.a data=$@

out-%.bin: bench-%.core bench.map
miget image=$< map=bench.map symbol=recv.b data=$@

Self-contained template (ref.py generates its own output)​

For examples whose inputs are fixed at build time, in the kernel or the layout (common with MP):

include $(FLUTEMK)
include $(TESTMK)

BENCH_NAME=receiver

CYCLES ?= 2000
SIMFLAGS=cycles=$(CYCLES) $(SIMOPTS)
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 out-ref.bin out-$(MODE).bin

# Generate reference output (self-contained, no input files)
out-ref.bin:
python3 ../ref.py

# Extract simulation output
out-%.bin: bench-%.core bench.map
miget image=$< map=bench.map symbol=receiver.result data=$@

Note: lib=.. in PAINTFLAGS lets Paint find .w files in the parent directory. This is needed because the build runs in a subdirectory (test-tutorial/).

Makefile Variables​

VariableRequiredDescription
FLUTEMKYesProvides build rules for bench.paint → image (Paint compiles each .w with Carbide) → simulation core dump
TESTMKYesProvides test harness (TEST= row selection, cleanup, etc.); test/$(MODE) is defined in your Makefile
CYCLESNoThe cycle cap the templates pass in SIMFLAGS; a test.rc row can set it
SIMFLAGSNo (defaults to $(SIMOPTS))Simulation options: cycles=N sets max cycles
PAINTVARSIf neededSpace-separated list of variables to pass to Paint from test.rc
PAINTFLAGSYesFlags for the paint tool: lib=, arch=, etc.
PIPELINEFLAGSOnly for the debug bundlePipeline flags: arch=${ARCH}
MODEOptionalsim (the default) or another execution mode

Symbol Paths in miget​

PEsSymbol syntaxExample
One, unnamedsymbol=array_namesymbol=a
Named regionssymbol=region_name.array_namesymbol=send.a
Nestedsymbol=parent.child.array_namesymbol=outer.inner.A

Step 5: Write the Test Configuration​

Create test.rc with one or more test instances. The templates that list N in PAINTVARS take it from the row:

tutorial N=4 ARCH=sdr

A layout with no row variables, such as the message-passing one, needs only the architecture:

tutorial ARCH=sdr

Format​

LABEL PARAM1=VALUE1 PARAM2=VALUE2 ARCH=arch
FieldDescription
LABELTest name: tutorial, smoke, full, etc.
PARAM=VALUESets a make variable (and Paint variable if in PAINTVARS)
ARCH=sdrTarget architecture: sdr (WSE-3)

A label is any name. Every lesson labels its first row tutorial, and the run instructions use make TEST=tutorial:

LabelDescription
tutorialThe first row; make TEST=tutorial runs it
n8, full, …Other rows, named for what they test
# at line startRow disabled (reported SKIP under TEST='*')

Multiple test lines create multiple test instances:

tutorial N=4 ARCH=sdr
n8 N=8 ARCH=sdr
full N=16 ARCH=sdr

Give every row its own label. Each row runs in its own directory, test-<label>/, here test-tutorial/, test-n8/ and test-full/. A label used on two rows names neither of them: their directories become test-1/ and test-2/, after their line numbers, and make TEST=<label> runs both.

With CYCLES ?= in the Makefile, a row can set its own cycle cap: full N=16 CYCLES=4000 ARCH=sdr.

With N=8 or N=16, the template kernel's four-value list leaves the other elements of a at 0. The standard template's ref.py reads the initial data from the image, so the test still checks every element.

Run a specific test: make TEST=n8 runs the row labelled n8, make TEST=2 the second line, and make TEST='*' every row. Under TEST='*', the first failing row stops the run; make -k TEST='*' runs the rest too.

Step 6: Write the Reference Implementation​

Create ref.py — a Python script that computes the expected output.

Pattern A: CLI-arg based (reads extracted input data)​

import sys
import numpy as np

in_name = sys.argv[1]
outfile = sys.argv[2]

# Read input (must match the dtype in your .w file)
a = np.fromfile(in_name, dtype=np.int16) # xp → int16
# For hp: dtype=np.float16
# For sp: dtype=np.float32

# Compute expected result (must match your kernel logic)
b = a * 2

# Write output
b.astype('int16').tofile(outfile)

The multi-PE template of Step 2 uses the same ref.py: recv doubles the values that send sends.

Pattern B: Self-contained (hardcoded input, writes directly)​

For examples whose inputs are fixed at build time, in the kernel or the layout (typical for MP):

import numpy as np

# Mirror the value the layout passes to the sender (my_value:42)
values = [42]

# The receiver stores the one value it receives (xp result[n=1])
ref = np.array(values, dtype=np.int16)
ref.tofile("out-ref.bin")
print(f"Expected: {list(ref)}")

This pattern is simpler. The topic Collectives over messages uses it, with the values that its test.rc row gives the layout.

Type Mapping​

Tungsten typeNumPy dtype
xpnp.int16
hpnp.float16
spnp.float32
u16np.uint16

Step 7: Run Your Test​

cd my-example
make TEST=tutorial

MODE=sim, the simulator, is the default.

What Happens​

  1. Paint lays out bench.paint and has Carbide compile each .w that a code() names → bench.obj (image) + bench.map (symbol table)
  2. milink applies the layout's link annotations to bench.obj; bench.img is a copy, into which the Makefile may miset inputs
  3. miget extracts initial data from bench.img → in.bin
  4. ref.py computes the expected output → out-ref.bin
  5. mielf converts bench.img to an ELF, the simulator runs it, and elfmi turns its core back into an image → bench-sim.core
  6. miget extracts results from the core → out-sim.bin
  7. cmp compares out-sim.bin with out-ref.bin

A passing test prints one PASS line, such as 01-hello-wafer : 0:03.73 PASS tutorial [ N=4 ARCH=sdr ]. A failing test prints FAIL; cmp's report of the byte offset where the files differ is in test-tutorial/make-log. A kernel that is still waiting when the simulation reaches its cycles= limit fails the same way, with its output only partly written.

After you edit a .w file, bench.paint, ref.py, the Makefile or test.rc, run make clean before make: make does not rebuild an existing test-<label>/ directory, so it would report the old result.

Step 8: Debug if Needed​

If the test fails:

# Run in debug mode; clean first so the simulation reruns with tracing
make clean
make TEST=tutorial GOAL=debugx

# Launch the Portrait visualizer
cd test-tutorial/
portrait
# Open the printed URL in your browser

See the Debugging Guide for more details.


Quick Checklist​

  • .w file(s) — one per PE role
  • bench.paint — PE layout and routing
  • Makefile — build and test rules
  • test.rc — test parameters
  • ref.py — reference implementation
  • make TEST=tutorial — verify it works

References​