ExSystolic.Backend.Interpreted (ex_systolic v0.2.0)

Copy Markdown View Source

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:

  1. INJECT -- push pending external input streams into boundary link buffers (handled by ExSystolic.Clock.step/1).
  2. READ -- read all link buffers (inputs from previous tick).
  3. EXECUTE -- run every PE's step/4 with those inputs (execute_tick/4).
  4. COLLECT -- gather all PE outputs.
  5. WRITE -- write outputs into link buffers (for next tick).
  6. 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.

Writes PE outputs into link buffers, returning updated links.

Functions

collect_results(pes)

@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}

execute_tick(pes, inputs_map, tick, trace_enabled)

@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 states
  • tick_outputs -- map of coord => outputs map
  • trace_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

write_outputs(links, tick_outputs, input_streams)

@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}