rtl/pe.sv — Systolic Processing Element (v2)
The architectural atom of a weight-stationary systolic MAC array: one multiply-accumulate cell with mixed-precision (INT8 or INT16) operands and an INT48 accumulator wide enough to prevent overflow at every supported precision. Not currently instantiated — npu.sv inlines equivalent combinational MAC + adder-tree logic directly for maximum Icarus Verilog simulation throughput rather than chaining 256 pe instances. This file exists as the standalone architectural reference for a future systolic-tiled or FPGA DSP-slice-mapped implementation.
Where it fits
Nowhere in the current build (Makefile's VERILOG_SOURCES does not include it). Conceptually, it's what each of the 256 cells of npu.sv's MAC array would be if built as a systolic chain instead of a fully-parallel combinational array.
Ports
| Direction | Name | Width | Description |
|---|---|---|---|
| in | clk, rst_n |
1, 1 | Clock / reset |
| in | clear |
1 | Synchronous accumulator clear |
| in | weight |
16 (signed) | Stationary weight (INT16; NPU sign-extends INT8 before driving) |
| in | act_in |
16 (signed) | Activation flowing in from the left |
| in | sum_in |
48 (signed) | Partial sum flowing in from the left |
| out | act_out |
16 (signed) | Activation passthrough to the right (1-cycle registered) |
| out | sum_out |
48 (signed) | Accumulated sum to the right: sum_in + weight × act_in |
Functionality
Pipeline depth: 1 cycle. Every clock edge: act_out <= act_in (pass the activation one cell to the right, unchanged) and sum_out <= sum_in + sign_extend(weight × act_in). The INT16×INT16 product is INT32; it's sign-extended to INT48 before adding to sum_in, matching the accumulator width npu.sv uses for its inlined equivalent.
weight (stationary)
│
act_in ──────────► [×] ──► (+) ──► sum_out
▲
sum_in
Chaining 16 of these horizontally (with sum_in of cell c = sum_out of cell c-1, and 0 for the leftmost) computes one row's dot product over 16 cycles of systolic flow-through, versus the fully-parallel adder-tree npu.sv actually uses (1 cycle, 256 simultaneous multiplies). The tradeoff a real systolic array buys back is area/routing — no giant multi-input adder tree — at the cost of the fill/drain latency systolic arrays are known for.
Example
Two cells in a chain, computing sum = w0×a + w1×a for a shared activation a flowing through:
cycle 0: cell0.act_in=a, cell0.sum_in=0 -> cell0.act_out<=a, cell0.sum_out<=0+w0*a
cycle 1: cell1.act_in=cell0.act_out=a
cell1.sum_in=cell0.sum_out=w0*a -> cell1.sum_out<=w0*a + w1*a
Related
- npu.md — where this cell's logic is inlined as a fully-parallel array instead
- npu.md (docs) — prose writeup of the PE's role in the conceptual architecture