axpy as a System of Uniform Recurrence Equations
axpy — a·x plus y — is the archetypal BLAS Level-1 kernel:
for a scalar and length- vectors and . It is the smallest useful catalog entry and the running example for how a fully-parallel vector operation is expressed in the domain-flow / confluence formalism.
The modelling problem: a map has no recurrence depth
Section titled “The modelling problem: a map has no recurrence depth”Unlike matmul (a reduction along k) or dot (a running sum), axpy has no
inter-index dependence: every output element is an independent
multiply-add. As a system of recurrence equations its natural domain is the
one-dimensional index set with a single equation and
no taps into other index points.
That is faithful, but it does not fit the confluence vocabulary the domain-flow representation uses to place data on the spatial fabric. In that vocabulary a tensor enters or leaves through an oriented face of the domain — a codimension-1 region pinned by one equality, with the flow direction given by the face’s outward normal. A length- result must leave through a face that spans all elements, and an oriented face needs the domain to have extent on both sides of it. A 1-D domain cannot supply that: its only faces are the two endpoints.
So we give the vector a flow axis. The length- vector rides the i axis
(the face extent), and a short pipeline axis j turns the elementwise map into a
two-cell systolic cell whose result exits through a proper terminal face.
Uniformity: every operand flows, nothing is broadcast
Section titled “Uniformity: every operand flows, nothing is broadcast”A uniform recurrence equation reads its arguments only at constant offsets
of the current index point: with the
dependence vectors independent of (Karp–Miller–Winograd, 1967). The
temptation with axpy is to write
treating as a literal scalar in the equation body. That is not a uniform recurrence: a single value read at every point is a broadcast, whose dependence on the fixed source is — it grows with . That is an affine dependence, the very thing uniformity forbids, and physically it is a fan-out bus rather than a nearest-neighbour connection.
is an operand of axpy — the a in a·x + y — on equal footing with
the vectors and . So, like them, it must enter through a confluence and be
carried by a uniform recurrence. Because is a 0-dimensional scalar, we
project it into the index space: inject it on the i = -1 boundary edge and
pipeline it across the lanes with the uniform shift
so every lane receives the same through constant-offset hops instead
of a broadcast. This is the standard uniformization of a parameter: replace the
broadcast by a propagation variable seeded on a boundary. The product
is then uniform — both taps are constant offsets of
— exactly as matmul’s is uniform despite being
nonlinear in the values.
Domain
Section titled “Domain”iindexes the vector elements — the spatial extent of every face.jis a two-step pipeline: inject at the front, drain at the back.
system ((i,j) | 0 <= i < N, 0 <= j < 2) { a(i,j) = a(i-1,j); // coefficient alpha, pipelined across lanes x(i,j) = 0; // one-shot injection: in-domain x is the additive identity y(i,j) = y(i,j-1) + a(i-1,j) * x(i,j-1); // stream y along +j, pick up alpha*x once}a(i,j)carries the scalar . It is seeded on thei = -1edge (see the confluences below) and propagates along+iby the uniform shifta(i-1,j), so every lane holds .x(i,j)is the vector operand’s carrier. The datum is injected on the haloj = -1; the in-domain equationx(i,j) = 0is a one-shot injection (defined precisely below).y(i,j)streams along+j. It is seeded on the haloj = -1with the incoming vector , accumulates atj = 0, and passes through unchanged toj = 1.
One-shot injection
Section titled “One-shot injection”The y equation is an accumulation along j: it is the prefix sum of the
increments over the pipeline, on top of the seed
. Its value at the drain face is
since . For this to equal the target , the increments must contribute exactly once, i.e. .
We arrange that by making x a one-shot injection. Read the accumulation’s
as the monoid with identity element . The datum
is placed only on the injection face (the halo j = -1); the in-domain equation
sets the carrier to that additive identity, x(i,j) = 0. The tap x(i,j-1)
therefore reads at the single step j = 0 whose offset reaches the face, and
the identity at every later step:
So the accumulation adds once and the identity thereafter — the injected datum is consumed by the one cell that reads the face and leaves no residue in the stream. (This is the general convention for a value that must enter a reduction exactly once: inject on the boundary, reset in-domain to the accumulator’s identity element — for , for .)
Tracing a fixed i makes the two increments explicit:
| step | value |
|---|---|
y(i,0) = y(i,-1) + a(i-1,0)·x(i,-1) | (x(i,-1)=x_i, the injection) |
y(i,1) = y(i,0) + a(i-1,1)·x(i,0) | (x(i,0)=0, the identity) |
The result is present at the terminal face j = 1.
Confluences (oriented faces)
Section titled “Confluences (oriented faces)”Faces are equality-pinned in the domain’s own coordinates; the outward normal is
derived as the equality plane’s outward normal with respect to the domain
(inputs are influx, tau·n < 0; outputs are outflux, tau·n > 0).
input Alpha[1] ((i,j) | i = -1, 0 <= j < 2) : a(i,j) = Alpha[0]; // project alpha -> normal (-1, 0)input X[N] ((i,j) | 0 <= i < N, j = -1) : x(i,j) = X[i]; // inject x -> normal (0,-1)input Y[N] ((i,j) | 0 <= i < N, j = -1) : y(i,j) = Y[i]; // seed y -> normal (0,-1)output R[N] ((i,j) | 0 <= i < N, j = 1) : R[i] = y(i,j); // drain R -> normal (0, 1)The scalar is projected onto the i = -1 edge (a codimension-1 face over
j); the vectors x and y sit on the one-step halo j = -1 where their
boundary values are read; the result leaves the terminal j = 1 face. The derived
normals are Alpha → (-1,0), X, Y → (0,-1), and R → (0,1) — alpha fluxes in
across the lanes, the vectors flux in along the pipeline, and the result fluxes
out.
Schedule
Section titled “Schedule”The kernel is parallel across i, with a one-step j carry and the lane-to-lane
i carry that pipelines alpha. The canonical linear schedule
is legal — every dependence vector (+j for the x/y carries, +i for the
alpha pipeline) has tau·theta = 1 ≥ 1 — and so is the
free schedule, the data-flow-earliest firing order.
Flux consistency picks out the legal schedules directly from the geometry. With
the derived normals Alpha → (-1,0), X, Y → (0,-1), R → (0,1), any
with and makes alpha flux in
across the lanes (tau·n = -τ_i < 0), the vectors flux in along the pipeline
(tau·n = -τ_j < 0), and the result flux out (tau·n = +τ_j > 0). A backward
component, e.g. , reverses a flow and is rejected by flux
revalidation before legality is even consulted.
Memory cardinality
Section titled “Memory cardinality”Under the three carriers (a, x, y) hold a bounded wavefront
of live values. For the analysis reports peakLiveValues = 8 (per
variable a = 4, x = 2, y = 2), latency 5, and work 24, and the eviction run
realizes the same footprint. The alpha pipeline a costs a wavefront across the
lanes; the vector carriers x and y are the baseline elementwise footprint.
Reductions (dot, nrm2) collapse a carrier to a single accumulator, and
Level-2/3 operators grow it by the reduction extent. (The free schedule trades a
larger footprint — peakLiveValues = 10 — for lower latency.)
Executable
Section titled “Executable”The spec above is the executable docs/SURE/axpy.sure. Run it through the SURE
front-end:
dfactl --sure docs/SURE/axpy.sure# free schedule; R = 2*{1,2,3,4} + {10,20,30,40} = {12,24,36,48}
dfactl --sure docs/SURE/axpy.sure --tau 1,-1# ILLEGAL: backward j-flux, rejected by flux revalidationThe regression test src/dfa/tests/sim/axpy_sure.cpp (CTest test_axpy_sure)
parses this document’s spec, checks the derived face normals, verifies
against a direct reference, and confirms free/linear schedule
legality and the memory analysis.
Note on the scalar α
Section titled “Note on the scalar α” enters as data on the Alpha confluence, not as a literal in an equation
body, so it is a genuine flowing operand rather than a broadcast constant. The
executable binds data Alpha = 2 for an exact integer check, but nothing in the
derivation depends on the value: the same projection carries any real , and
a per-lane coefficient (data Alpha = { ... } read along the i = -1 edge)
generalizes axpy to a diagonal scaling without changing the domain, the
confluence structure, or the schedule.
Reduced dependency graph
Section titled “Reduced dependency graph”The reduced dependency graph (RDG) draws the recurrence as
one node per variable and one arc per dependence, each annotated with its translation
vector θ. axpy’s three variables — the pipelined scalar a (= α), the input x,
and the running result y — wire up with constant offsets only:
| arc | θ | dependence |
|---|---|---|
a → a | [1,0]ᵀ | α pipelined across the lanes (+i) |
y → y | [0,1]ᵀ | the running y advances along the strip (+j) |
a → y | [1,0]ᵀ | α feeds the product |
x → y | [0,1]ᵀ | x feeds the product |
Every arc is a translation vector — no matrix, no broadcast — so axpy is a genuine
SURE: a fixed nearest-neighbour stencil, schedulable by a single linear τ.
Watch the schedule
Section titled “Watch the schedule”Under the linear schedule τ = [1,1] the wavefront sweeps diagonally across the lanes — the projected alpha pipeline and the streaming y advancing together. Shown at N = 12. Press play, or scrub; drag to orbit.