Skip to main content

FFT

The fast Fourier transform (FFT) computes the discrete Fourier transform (DFT) of NN complex values x0,…,xN−1x_0, \dots, x_{N-1}:

Xk=∑j=0N−1xj ωNjk,ωN=e−2πi/N,k=0,…,N−1,X_k = \sum_{j=0}^{N-1} x_j \, \omega_N^{jk}, \qquad \omega_N = e^{-2\pi i/N}, \qquad k = 0, \dots, N-1,

in O(Nlog⁡N)O(N \log N) operations.

This example computes one 1D FFT of length N=PXPYMN = P_X P_Y M on a PX×PYP_X \times P_Y grid of PEs, with MM complex fp16 values on each PE. It is a variation of the 4-step FFT: local FFTs are computed on each PE, separated by all-to-all exchanges between PEs.

The algorithm​

The 4-step FFT​

Split the length as N=N0N1N = N_0 N_1 and write the indices as j=j0N1+j1j = j_0 N_1 + j_1 and k=k0+N0k1k = k_0 + N_0 k_1. The DFT becomes

Xk0+N0k1=∑j1=0N1−1ωN1j1k1  ωNj1k0⏟twiddle  ∑j0=0N0−1xj0N1+j1 ωN0j0k0⏟FFT of length N0.X_{k_0 + N_0 k_1} = \sum_{j_1=0}^{N_1-1} \omega_{N_1}^{j_1 k_1} \; \underbrace{\omega_N^{j_1 k_0}}_{\text{twiddle}} \; \underbrace{\sum_{j_0=0}^{N_0-1} x_{j_0 N_1 + j_1} \, \omega_{N_0}^{j_0 k_0}}_{\text{FFT of length } N_0}.

Equivalently, this becomes N1N_1 FFTs of length N0N_0, a pointwise multiply by twiddle factors, then N0N_0 FFTs of length N1N_1. If each small FFT runs on a single PE, the only communication is a reshuffle between the two steps: an all-to-all.

Rounds​

However, if the number of elements per PE (MM) is small and the total number of elements (NN) is large, two steps are not enough. We can apply this same split repeatedly, giving N=∏t=0R−1Nt=N0N1⋯NR−1N = \prod_{t=0}^{R-1} N_t = N_0 N_1 \cdots N_{R-1}, with every Nt≤MN_t \leq M fitting into local PE memory. The FFT then runs

R=⌈log⁡2Nlog⁡2M⌉R = \left\lceil \frac{\log_2 N}{\log_2 M} \right\rceil

total rounds, each with three phases:

  1. Exchange (not in round 0): an all-to-all among a small group of PEs,
  2. Local FFT: M/NtM/N_t interleaved FFTs of length NtN_t on each PE,
  3. Twiddle (not in the last round): local pointwise multiplication by a table of twiddle factors.

Conceptually, the bits of global indices jj are broken up to determine each data point's PE row, PE column, and local slot within that PE. Between rounds, a few column (or row) bits are interchanged with slot bits; each exchange therefore stays within a single row (or column) of PEs.

For example, a 4×44 \times 4 PE grid with M=4M=4 computes an FFT of length N=64N = 64 in three rounds of length-4 FFTs. Every exchange here is between pairs of PEs: each PE swaps half of its values with one partner.

The three rounds of a 64-point FFT on a 4 by 4 grid of PEs with 4 values each. Round 0 runs a local FFT with no exchange. Rounds 1 and 2 each exchange along the rows, then the columns, then run a local FFT. In round 1 the partners are neighbouring PEs; in round 2 they are 2 apart. In each exchange a PE sends two of its four values to its partner and receives two back.

The diagram below follows the bits of the input index jj and the output index kk through the rounds.

PE row (py) PE column (px) slot (s)
1 0 1 0 1 0
round 0 input j1 j3 j0 j2 j5 j4
FFT j1 j3 j0 j2 k1 k0
round 1 exchange j1 k1 j0 k0 j3 j2
FFT j1 k1 j0 k0 k3 k2
round 2 exchange k3 k1 k2 k0 j1 j0
FFT k3 k1 k2 k0 k5 k4

The twiddle multiplies after rounds 0 and 1 change values but move no bits. Round 1 exchanges bit 0 of pxp_x and pyp_y, so its partners are neighbouring PEs. Round 2 exchanges bit 1, so its partners are 2 apart. A PE keeps the slots whose exchanged slot bit equals its own PE bit, and swaps the others, the shaded slots in the figure.

Larger rounds exchange more bits at once, among larger groups. A 16×1616 \times 16 grid with M=64M=64 runs rounds of 262^6, 242^4 and 242^4 points. Round 1 exchanges pxp_x and pyp_y bits 1–0, among groups of 4 neighbouring PEs. Round 2 exchanges bits 3–2, among groups of 4 PEs spaced 4 apart. python plan.py PX PY M prints the plan for any grid.

The exchanges and rounds are precomputed on the host by plan.py: each PE gets a table plan_table[t] of each round's FFT size and exchanged bits, and its twiddle tables.

The local FFT​

Each round's FFT runs on one PE, over its MM slots. When Nt<MN_t < M, the PE holds B=M/NtB = M/N_t independent FFTs, interleaved: batch bb is the values in slots b,b+B,b+2B,…b, b+B, b+2B, \dots. So the round transforms the top log⁡2Nt\log_2 N_t slot bits and leaves the low log⁡2B\log_2 B bits alone. In the 16×1616 \times 16, M=64M=64 plan above, rounds 1 and 2 each hold B=4B = 4 FFTs of length 16.

batched_fft() is an in-place, self-sorting radix-2 FFT: input and output are both in natural order, so no bit-reversal pass is needed. The buffer x is split into halves x_lo and x_hi. Each stage multiplies x_hi by twiddles, does the butterflies a±wba \pm wb across the two halves, and re-shuffles so the next stage again finds its pairs one half apart. A full MM-point FFT takes log⁡2M\log_2 M stages. Stopping after log⁡2Nt\log_2 N_t stages leaves exactly the BB interleaved FFTs of length NtN_t.

Communication​

Two networks, each on its own color:

  • Message passing carries the exchanges. Every message has a header with its destination PE, so one bank of message-passing routes serves every exchange, whatever its stride. A message holds a tag (the sender, and which part of its data this is) and up to 16 values; the receiver uses the tag to place them.
  • A switched barrier starts every exchange. Without it, a fast PE could send data for its next exchange to a PE still receiving the current one. The barrier uses router switch positions and control wavelets on a static color (see the barrier-broadcast example). Every PE but the origin starts closed, and opens its own switch when it arrives. A probe from the origin can only get back round the grid once every PE is open. The origin then sends a release to everyone and resets the switches.

Files​

FileContents
fft.wThe program: parameters, buffers, the local FFT and twiddle multiply, and main, which runs the rounds
exchange.wThe all-to-all exchange by message passing, and the barrier
bench.paintThe grid, its message-passing bank, and the barrier network
plan.pyThe plan and host tables, with a numpy model of the kernel (python plan.py self-tests it)
ref.pyWrites the inputs, and checks the output against numpy
Makefile, test.rcBuild, simulate, and check

Reading the code​

Start at main at the bottom of fft.w. It loops over the rounds, reads round tt's row of plan_table, and calls one function per phase:

PhaseFunctionFile
Exchange along the row, then the columnall_to_all(along_row, ...)exchange.w
Barrier at the start of each exchangebarrier()exchange.w
Local FFTbatched_fft(log_N_t)fft.w
Twiddletwiddle_multiply(t)fft.w

all_to_all() runs in three steps:

  1. Build every outgoing message (header, tag, values) into one envelope buffer.
  2. Call barrier(), then send the envelope and receive the other PEs' messages into receive_buffer, at the same time, in a parallel block.
  3. Copy the PE's own values (the ones that stay put) into the buffer, then the buffer back to x.

On the host side, plan.py builds the plan, and its docstring gives the index algebra in full. The barrier network's switch positions are in makebarriergrid() in bench.paint.

Lessons from the exchange​

Two details of all_to_all() matter. Without them, large configurations deadlock:

  • Send everything as one stream. A loop that sends message by message, each from its own send statement, keeps the CE from receiving in between. With 16 or more PEs in a group, the fabric's buffers fill and every PE ends up waiting on a send. So the messages are packed into envelope first, and one statement, ∀i ∈ [0, envelope_words) mp[] ← envelope[i], streams the whole envelope.
  • Receive with an iterator loop. The receive arm of the parallel block is a ∀ loop. Written as a while loop, it does not overlap with the send arm, and the exchange deadlocks.

Building and running​

The experimental SDK installs this example beside the tutorial, as projects/fft. Run every row of its test.rc with tungsten-tutorials -f projects/fft, or work by hand in a copy of the directory, set up as for the tutorials, where <sdk> is the SDK's directory:

cp -r <sdk>/share/tutorial/projects/fft . && cd fft
python plan.py 16 16 64 # show the plan for one configuration
make TEST=7 # build + simulate + check row 7 of test.rc
make TEST=grid # the three grid rows
make -k TEST='*' # every row
make clean # remove all test-*/ directories

$(SIMFABRIC) runs the simulation on the Cerebras SDK simulator. MODE (default sim) only names the run's outputs (out-$(MODE).bin); sim is the only mode the Makefile provides.

Each row of test.rc is one configuration, tagged by size (small, grid, deep, large):

grid PX=16 PY=16 M=64 ARCH=sdr
VariableMeaning
PX, PYGrid width and height, powers of 2, at least 2
MComplex values per PE, a power of 2 from 2 to 1024
MPBANKMessage-passing bank, 0 or 1 (default 0)
CYCLESSimulation cycle cap (default 50000)
ARCHsdr (WSE-3) in every row

The build compiles fft.w for the grid and writes the inputs with ref.py. It loads them into the image with miset, simulates it, reads x back with miget and checks it. Each tag appears on more than one row, so test.mk names each run directory by row number: test-1/ to test-12/. Each holds make-log (the real output; make's own stdout is a one-line PASS/FAIL).

The check compares the result with np.fft.fft of the same input, rounded to fp16. It passes when the largest error is within 2% of the largest output magnitude, max⁡k∣Xk−Xkref∣≤0.02 max⁡k∣Xkref∣\max_k |X_k - X^{\text{ref}}_k| \le 0.02 \, \max_k |X^{\text{ref}}_k|. The kernel computes in fp16, so the tolerance is loose. A passing run ends with

PASS: max error <e> (relative to max |X|)

and a failing one exits non-zero with FAIL: max error <e> (relative to max |X|) > 0.02. If the run doesn't finish within CYCLES, x is read back mid-computation and the check fails.

Tungsten and Paint concepts used​

Tungsten (language reference):

Paint (language reference):

Related examples: the message-passing tutorials (tutorial/message-passing/) and bench/app/barrier-broadcast.

Run it​

The experimental SDK installs this project beside the tutorial, as projects/fft. Run it like a tutorial: tungsten-tutorials -f projects/fft.

Source code​

Every file of this example, as the toolset installs them under share/tutorial/projects/fft/. Select a line number to link to that line.

Explorer · 9 files

Loading exchange.w…

Tungsten9.1 KBDownload exchange.w