The interpreted (single-BEAM-process) backend for systolic execution.
This backend runs the entire array in one process, advancing tick by tick. It is the simplest correct implementation and serves as the reference semantics for all future backends.
Tick execution order (CRITICAL)
Each tick executes in this strict order, matching the BSP
contract documented in ExSystolic.Backend:
- INJECT -- push pending external input streams into boundary
link buffers (handled by
ExSystolic.Clock.step/1). - READ -- read all link buffers (inputs from previous tick).
- EXECUTE -- run every PE's
step/4with those inputs (execute_tick/4). - COLLECT -- gather all PE outputs.
- WRITE -- write outputs into link buffers (for next tick).
- RECORD -- optionally append trace events.
This ordering guarantees that no PE reads data produced in the same tick. All reads see the state left by the previous tick; all writes prepare the state for the next tick.
Steps 1, 2, and 5 are delegated to ExSystolic.Backend.LinkOps.
Why not GenServer per PE?
Per-PE GenServers introduce concurrency, non-deterministic scheduling, and coordination overhead. The interpreted backend proves that correctness does not require concurrency. Future backends may add parallelism, but the semantics must remain identical.
Summary
Functions
Collects final results from the PEs.
Executes one tick for all PEs.
Writes PE outputs into link buffers, returning updated links.
Functions
@spec collect_results(%{ required(ExSystolic.Grid.coord()) => {module(), ExSystolic.PE.state()} }) :: %{ required(ExSystolic.Grid.coord()) => term() }
Collects final results from the PEs.
Returns a map of coord => state for all PEs.
Examples
iex> pes = %{{0,0} => {ExSystolic.PE.MAC, 42}}
iex> ExSystolic.Backend.Interpreted.collect_results(pes)
%{{0,0} => 42}
@spec execute_tick( %{required(ExSystolic.Grid.coord()) => {module(), ExSystolic.PE.state()}}, %{required({ExSystolic.Grid.coord(), atom()}) => term()}, non_neg_integer(), boolean() ) :: {%{required(ExSystolic.Grid.coord()) => {module(), ExSystolic.PE.state()}}, %{required(ExSystolic.Grid.coord()) => ExSystolic.PE.outputs()}, [ExSystolic.Trace.Event.t()]}
Executes one tick for all PEs.
Returns {new_pes, tick_outputs, trace_events} where:
new_pes-- updated PE map with new statestick_outputs-- map of coord => outputs maptrace_events-- list of trace events (empty if tracing disabled)
Examples
iex> pes = %{{0,0} => {ExSystolic.PE.MAC, 0}}
iex> inputs_map = %{{{0,0}, :west} => 3, {{0,0}, :north} => 4}
iex> {new_pes, outputs, _events} = ExSystolic.Backend.Interpreted.execute_tick(pes, inputs_map, 0, true)
iex> new_pes[{0,0}] |> elem(1)
12
iex> outputs[{0,0}].result
12
@spec write_outputs( [ExSystolic.Link.t()], %{required(ExSystolic.Grid.coord()) => ExSystolic.PE.outputs()}, %{required({ExSystolic.Grid.coord(), atom()}) => [term()]} ) :: {[ExSystolic.Link.t()], %{required({ExSystolic.Grid.coord(), atom()}) => [term()]}}
Writes PE outputs into link buffers, returning updated links.
Delegates to ExSystolic.Backend.LinkOps.write_pe_outputs/2. The
third argument (input_streams) is also injected for backwards
compatibility; the returned tuple includes the remaining (deferred)
streams.
Prefer using the split helpers ExSystolic.Backend.LinkOps.write_pe_outputs/2
and ExSystolic.Backend.LinkOps.inject_streams/2 directly.
Examples
iex> link = ExSystolic.Link.new({{0,0}, :east}, {{0,1}, :west})
iex> {new_links, _} = ExSystolic.Backend.Interpreted.write_outputs([link], %{{0,0} => %{east: 5}}, %{})
iex> ExSystolic.Link.peek(Enum.at(new_links, 0))
{:ok, 5}