docs: bring datasheet/ into the main repo under hardware/v2/docs

Was a separate, untracked directory (DataSheet/) outside the repo.
Renamed to lowercase and moved in as hardware/v2/docs/datasheet/, with
its own .gitignore for LaTeX build byproducts (compiled PDFs stay
tracked, .aux/.log/.toc/etc do not). Now versioned and shares this
repo's own remote instead of living untracked on disk.

Content: IT+EN LaTeX chapter sources, reference manufacturer PDFs, and
compiled datasheet PDFs including the 2026-09-07 SDRAM upgrade
addendum (AS4C32M16SB-7BIN part/pinout/timing) in the v2-en chapters.

Note: hardware/v2/docs/DatasheetLatex/ (and the v1 sibling) is a
separate, already-tracked, differently-structured LaTeX document that
predates this move -- left untouched, not merged, since its chapter
set and content differ and merging was not requested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
2026-09-07 05:09:14 +02:00
co-authored by Claude Sonnet 5
commit 0e73eb4726
66 changed files with 7652 additions and 0 deletions
@@ -0,0 +1,139 @@
\thispagestyle{plain}
\noindent
\begin{tikzpicture}
\node[fill=fnDark,text=white,rounded corners=2pt,inner sep=6pt,
minimum width=\textwidth,anchor=west]
{\large\bfseries FPGA-Neural V2 --- General description and features};
\end{tikzpicture}
\vspace{6pt}
\noindent
{\small FPGA-Neural V2 is a \textbf{neural multiprocessor / dataflow machine},
the evolution of the V1 sequential accelerator (documented separately, frozen
and unmodified as the project's golden reference). Where V1 executes one
neuron at a time under host-driven SPI control, V2 registers a
\textbf{dependency graph of neurons} and keeps \code{N\_SLOTS} independent
Neural Processor $+$ Memory Manager pairs busy concurrently, resolving data
dependencies and hiding PSRAM latency in hardware, without host
intervention once a graph is loaded. Computation (INT8 MAC, ReLU,
saturation) is bit-exact identical to V1's own datapath; what changed is
everything \emph{around} it.}
\vspace{8pt}
\begin{multicols}{2}
{\color{fnDark}\large\bfseries Features}\\[2pt]
{\footnotesize
\begin{itemize}[leftmargin=1.1em]
\item \textbf{Dependency-graph scheduling}: nodes are registered with an
explicit producer list; a node becomes eligible for execution only
once every producer it depends on has genuinely completed --- verified
for 1-hop shared-producer/multi-consumer graphs and 2-hop transitive
(diamond) graphs.
\item \code{N\_SLOTS} independent \textbf{Neural Processor + Memory Manager}
pairs (default recommended: \textbf{2}), each running the identical
8-stage INT8 pipeline inherited from V1.
\item \textbf{Word-level burst memory backend}: fetches move a full 16-bit
PSRAM word per transaction instead of one byte, reusing
\code{memory\_interface.v}/\code{psram\_controller.v} directly and its
already-implemented page-mode support --- \textbf{2.24--2.37$\times$}
real wall-clock speedup, measured.
\item \textbf{Shared on-chip activation cache}: a vector of activations
shared by many neurons of the same layer is fetched from PSRAM
\emph{once}, not once per neuron --- a further real
\textbf{1.66--2.00$\times$} cycle reduction on shared-input workloads.
\item Same \textbf{real, unmodified V1 PSRAM backend} throughout
(\code{memory\_interface.v}, \code{psram\_controller.v}) --- V1 remains
the frozen golden reference and was never altered to make V2 look
faster.
\item \textbf{Real, measured} characterization at every step: Verilator
RTL simulation, Yosys synthesis, real \code{nextpnr-ecp5}
place\&route --- no theoretical number reported without a matching
real measurement.
\end{itemize}}
\columnbreak
{\color{fnDark}\large\bfseries Honest, measured limitations}\\[2pt]
{\footnotesize
\begin{itemize}[leftmargin=1.1em]
\item The system is \textbf{memory-bound}, not compute-bound: real compute-
to-memory-wait ratio on the order of 1:170--1:220. A single shared
PSRAM port saturates at $\approx$90\% utilization regardless of
\code{N\_SLOTS}$\ge$2 --- real parallel scaling beyond 2 slots is
essentially flat for large workloads.
\item \code{N\_SLOTS=4} is \textbf{not recommended}: it delivers no
additional real throughput once the shared PSRAM port saturates,
and with the activation cache active it \textbf{fails the 80\,MHz
timing target outright} (65.01\,MHz measured).
\item Fixed, lowest-index-priority arbitration (Director and memory
arbiter alike) is not fairness-balanced --- a real, measured
per-slot workload imbalance exists under sustained contention.
\end{itemize}}
\vspace{4pt}
{\color{fnDark}\large\bfseries Target \& toolchain}\\[2pt]
{\footnotesize
\begin{itemize}[leftmargin=1.1em]
\item FPGA: Lattice ECP5 \code{LFE5U-45F-8BG381C} ($-8$, CABGA381) --- same
target device as V1.
\item Synthesis: Yosys; place\&route: real \code{nextpnr-ecp5}.
\item Simulation: Verilator 5.050 (\code{--binary --timing}) --- adopted
for V2 after two independent Icarus Verilog v13.0 scheduling
defects were found and reproduced on minimal repros (V1's own
certification, performed separately, was unaffected).
\item PSRAM: ISSI \code{IS66WVE4M16EBLL-70BLI} (64\,Mb, 4M$\times$16),
real chain reused byte-for-byte from V1.
\end{itemize}}
\end{multicols}
\vspace{2pt}
% --- key parameter table ---
\noindent
{\small\color{fnDark}\bfseries Key parameters (recommended configuration, real measured data)}
\vspace{2pt}
\noindent
\begin{tabularx}{\textwidth}{L{3.6cm}L{3.6cm}Y}
\toprule
\rowh \thd{Quantity} & \thd{Value} & \thd{Notes} \\
\midrule
Data precision & INT8 (signed) & \code{DATA\_WIDTH}=8, identical to V1 \\
\rowa Accumulator & INT32 (signed) & \code{ACC\_WIDTH}=32 \\
Dot-product width & 8 & \code{P\_IN}=8 parallel MAC lanes per neuron \\
\rowa Recommended concurrency & \code{N\_SLOTS}=2 & real, measured net win; see ch.~\ref{ch:impl2} \\
Fmax, full system (\code{N\_SLOTS}=2) & 87.72~MHz & real place\&route, word-burst + activation cache active \\
\rowa cycles/neuron (1 neuron, 8 inputs, real PSRAM) & 166 (V1: 209) & \textbf{2.6$\times$} real wall-clock speedup vs V1 \\
Combined real speedup vs baseline (\code{N\_SLOTS}=2) & \textbf{2.45$\times$} & word-burst $+$ activation cache, D-Stress workload \\
\rowa Address space & 23~bit (byte) & \code{ADDR\_WIDTH}=23, unchanged from V1 \\
\bottomrule
\end{tabularx}
\vspace{8pt}
\noindent
{\small\color{fnDark}\bfseries System block diagram}
\begin{center}
\begin{tikzpicture}[node distance=6mm and 9mm,font=\footnotesize]
\node[fnblockD,minimum width=24mm,minimum height=15mm] (host){HOST\\{\scriptsize registers a node graph}};
\node[fnblockT,right=14mm of host,minimum width=30mm,minimum height=13mm] (dm){Dependency\\Manager};
\node[fnblockT,right=14mm of dm,minimum width=28mm,minimum height=13mm] (dir){Neural\\Director};
\node[fnreg,fill=white,right=14mm of dir,minimum width=30mm,minimum height=20mm] (slots){
\begin{tabular}{c}
N\_SLOTS $\times$ \\
Memory Manager \\
$+$ Neural Processor
\end{tabular}};
\node[fnblockA,below=9mm of dir,minimum width=28mm,minimum height=11mm] (cache){Activation\\Cache};
\node[fnblock,right=14mm of slots,minimum width=22mm,minimum height=15mm] (ram){PSRAM 8\,MB\\{\scriptsize real V1 backend}};
\draw[fnbus] (host) -- (dm);
\draw[fnbus] (dm) -- node[fnlbl,above]{ready node} (dir);
\draw[fnbus] (dir) -- (slots);
\draw[fnarrowT] (slots.south) |- (cache.east);
\draw[fnarrowT] (cache.north) |- node[fnlbl,above]{producer done} (dm.south);
\draw[fnbus] (slots) -- node[fnlbl,above]{16-bit word} (ram);
\draw[fnbus] (cache.east) -- ++(6mm,0) |- ([yshift=-2mm]ram.south);
\end{tikzpicture}
\end{center}
\begin{center}\footnotesize\itshape\color{fnGrey}
A slot's completion feeds back to the Director (frees the slot) and to the
Dependency Manager (wakes up any node waiting on it) --- closing the
dataflow loop entirely on-chip.\end{center}
@@ -0,0 +1,54 @@
\thispagestyle{plain}
\noindent
\begin{tikzpicture}
\node[fill=fnDark,text=white,rounded corners=2pt,inner sep=6pt,
minimum width=\textwidth,anchor=west]
{\large\bfseries Pinout summary --- scope and honesty note};
\end{tikzpicture}
\vspace{6pt}
\noindent
{\footnotesize
V2's top-level module, \code{neural\_multiprocessor.v}, has been
synthesized and placed\&routed \textbf{unconstrained}
(\code{nextpnr-ecp5 --lpf-allow-unconstrained}) throughout this project's
own real-toolchain characterization: every Fmax/resource number in this
datasheet is real and measured, but \textbf{no ball-by-ball pin
assignment (\code{.lpf}) has been generated for V2's top level in this
revision}. Unlike V1's own pinout chapter (which reports a real,
\code{iodb.json}-verified ball map from a constrained place\&route run),
this chapter reports what is \textbf{honestly known} and nothing
invented.
}
\vspace{6pt}
\begin{fnnote}[What is real and reusable]
V2's PSRAM-facing pins (\code{psram\_a}, \code{psram\_dq},
\code{psram\_ce\_n/oe\_n/we\_n/lb\_n/ub\_n/zz\_n}) drive the exact same,
real, unmodified V1 backend chain (\code{memory\_interface.v} $\to$
\code{psram\_controller.v}) as V1's own \code{spi\_neuron\_top}. If V2 is
deployed on the same board, \textbf{V1's own real, verified ball
assignment for these signals (ch.~10 of the V1 datasheet) applies
unchanged} --- the controller was never touched, so its pin requirements
did not change either.
\end{fnnote}
\begin{fnwarn}[What is NOT yet real]
The node-registration bus (\code{reg\_valid}, \code{reg\_node\_id},
\code{reg\_required}, \code{reg\_producer\_ids}, \code{reg\_x\_base},
\code{reg\_w\_base}, \code{reg\_n\_tiles}, \code{reg\_result\_addr},
\code{reg\_ready}) has no assigned physical pins in this revision: every
V2 measurement to date drove this bus directly from a Verilator
testbench or an unconstrained synthesis top-level, never through a real
host-facing SPI (or other) interface with its own placed pinout. Framing
this bus as a real, deployable host interface (analogous to V1's SPI
Mode~0 slave) is explicitly \textbf{future work} --- see
ch.~\ref{ch:roadmap}.
\end{fnwarn}
\vspace{4pt}
\noindent
{\footnotesize\color{fnGrey}
Logical (not physical) port list and field widths: ch.~\ref{ch:regs}
(``Register-level interface''). Real PSRAM signal reuse and board
wiring: ch.~\ref{ch:hw}.\par}
@@ -0,0 +1,82 @@
\chapter{Overview and design philosophy}
\label{ch:overview}
\section{From sequential accelerator to dataflow machine}
V1 is, structurally, a single pipeline: one neuron computes at a time,
driven by the host over SPI, one MAC group at a time, one layer at a
time. It is fast for what it is (the V1 datasheet's own ``ECP5
implementation'' chapter documents its real Fmax/timing-closure history),
but it cannot keep
more than one computational unit genuinely busy at once, and it has no
notion of a dependency graph --- the host sequences everything.
V2 keeps V1's own proven INT8 datapath (bit-exact, byte-for-byte reused
math) but wraps it in a fundamentally different control architecture:
a \textbf{Dependency Manager} tracks a graph of neuron ``jobs'', each
with an explicit list of producer nodes it depends on; a \textbf{Neural
Director} dispatches every node whose dependencies have resolved to
whichever of \code{N\_SLOTS} concurrent (Memory Manager $+$ Neural
Processor) pairs is free; a slot's completion feeds back to wake up any
node that was waiting on it. Once a graph is loaded, the whole system
runs autonomously --- no per-neuron host intervention.
\section{What did NOT change}
\begin{itemize}
\item The INT8$\times$INT8$\to$INT32 MAC math, the balanced adder tree,
ReLU/linear activation with saturation --- \code{neural\_processor.v}
is a direct, bit-exact-verified port of V1's own
\code{neuron\_parallel.v}/\code{mac8.v}/\code{mac\_unit.v}.
\item The real PSRAM backend: \code{memory\_interface.v} and
\code{psram\_controller.v} are reused \textbf{byte-for-byte,
unmodified} from V1 throughout every V2 milestone --- including
the two post-campaign optimizations (ch.~\ref{ch:mem}). V1 itself,
as a tree (\code{hardware/v1/}), is frozen and was never touched.
\item The target device (Lattice ECP5 \code{LFE5U-45F-8BG381C}) and the
real-toolchain-only measurement discipline: every number in this
datasheet is labelled \textsc{Theoretical}, \textsc{Simulated},
\textsc{Post-P\&R measured}, or \textsc{Derived}, and no result was
invented to make V2 look better than it measured (§\ref{ch:impl2}).
\end{itemize}
\section{What DID change}
\begin{itemize}
\item \textbf{Concurrency}: from one active neuron to \code{N\_SLOTS}
independent Neural Processor instances, each fed by its own Memory
Manager.
\item \textbf{Scheduling}: from host-sequenced SPI opcodes to an on-chip
dependency graph, resolved autonomously.
\item \textbf{Memory backend granularity}: from byte-at-a-time fetches
(through \code{int8\_memory\_access.v}, still frozen V1 but no
longer instantiated in V2's own datapath) to word-level bursts
talking to \code{memory\_interface.v} directly --- a real, measured
2.24--2.37$\times$ speedup (ch.~\ref{ch:mem}).
\item \textbf{Memory traffic pattern}: a new shared on-chip
\textbf{activation cache} eliminates redundant re-fetching of an
input vector shared by many neurons of the same layer --- a
further real 1.66--2.00$\times$ cycle reduction, at a real, honestly
reported Fmax cost (ch.~\ref{ch:mem}).
\end{itemize}
\section{The central, measured finding}
The single most important result of this project's own benchmark
campaign is that \textbf{V2 is memory-bound, not compute-bound}: the
real compute-to-memory-wait ratio is on the order of 1:170--1:220, and
the one physical PSRAM port saturates at $\approx$90\% utilization
regardless of \code{N\_SLOTS}$\ge$2. Real parallel scaling from
\code{N\_SLOTS}=1 to \code{N\_SLOTS}=8 is essentially flat for
large/sustained workloads (1.05--1.06$\times$), and once real,
place\&route-measured Fmax degradation from added routing congestion is
also accounted for, \code{N\_SLOTS}=4 measures as \emph{slower} in real
wall-clock time than \code{N\_SLOTS}=1 for the largest workload tested
--- more hardware parallelism made that specific configuration worse,
not better, because the bottleneck was never compute. This finding
directly shaped both post-campaign optimizations in ch.~\ref{ch:mem} and
the \code{N\_SLOTS}=2 recommendation carried throughout this datasheet.
\begin{fnnote}[Reproducibility]
Every real number in this datasheet traces to a specific, append-only
log entry (\code{EXP-\textit{NNNN}}, \code{DEC-\textit{NNNN}},
\code{ERR-\textit{NNNN}}) in \code{hardware/v2/logs/}, a specific git
commit, and an exact toolchain command --- the same discipline applied
throughout V1's own development.
\end{fnnote}
@@ -0,0 +1,127 @@
\chapter{Architecture}
\label{ch:arch}
\section{Module map}
\begin{center}
\begin{tikzpicture}[node distance=7mm and 11mm,font=\footnotesize]
\node[fnblockD,minimum width=34mm,minimum height=13mm] (dm){\textbf{Dependency Manager}\\{\scriptsize node table, wake-up}};
\node[fnblockT,right=13mm of dm,minimum width=32mm,minimum height=13mm] (dir){\textbf{Neural Director}\\{\scriptsize first-free dispatch}};
\node[fnreg,fill=white,right=13mm of dir,minimum width=34mm,minimum height=20mm] (mm0){
\begin{tabular}{c}\textbf{Memory Manager} 0\\ $+$ \textbf{Neural Processor} 0\end{tabular}};
\node[fnreg,fill=white,below=3mm of mm0,minimum width=34mm,minimum height=20mm] (mm1){
\begin{tabular}{c}\textbf{Memory Manager} 1\\ $+$ \textbf{Neural Processor} 1\end{tabular}};
\node[fnblockA,below=9mm of dir,minimum width=32mm,minimum height=13mm] (cache){\textbf{Activation Cache}\\{\scriptsize shared, single-tag}};
\node[fnblock,right=13mm of mm0,minimum width=26mm,minimum height=13mm] (arb){\textbf{Slot Memory}\\\textbf{Arbiter}};
\node[fnblockD,below right=9mm and 13mm of arb,minimum width=30mm,minimum height=13mm] (psram){\textbf{Real V1 PSRAM chain}\\{\scriptsize memory\_interface $\to$ psram\_controller}};
\draw[fnbus] (dm) -- node[fnlbl,above]{ready\_valid/ready} (dir);
\draw[fnbus] (dir) -- (mm0);
\draw[fnbus] (dir) -- (mm1);
\draw[fnarrowT] (mm0.south) |- (cache.east);
\draw[fnarrowT] (mm1.west) -- (cache.east);
\draw[fnarrowT] (cache.north) |- node[fnlbl,above,pos=0.3]{producer\_done} (dm.south);
\draw[fnbus] (mm0) -- (arb);
\draw[fnbus] (mm1) -- (arb);
\draw[fnbus] (cache.south) |- (arb.west);
\draw[fnbus] (arb) -- (psram);
\end{tikzpicture}
\end{center}
\begin{center}\footnotesize\itshape\color{fnGrey}
N\_SLOTS=2 shown (the recommended configuration); the architecture is
parametric in N\_SLOTS. Every arrow is a real signal path verified in
Verilator simulation and real Yosys/nextpnr-ecp5 synthesis.\end{center}
\section{Dependency Manager}
Holds a table of \code{N\_NODES} job descriptors, each tracking: node
id, state (\code{EMPTY}/\code{WAITING}/\code{READY}/\code{DISPATCHED}),
required-dependency count, resolved-dependency count, up to
\code{MAX\_DEPS} producer node ids, and the job descriptor fields
(\code{x\_base}, \code{w\_base}, \code{n\_tiles}, \code{result\_addr}). A
node with zero required dependencies is immediately \code{READY} on
registration. When a producer completes, \emph{every} \code{WAITING}
node listing it among its own producers gets its resolved-dependency
count incremented --- a single producer can satisfy several waiting
consumers (shared-producer/multi-consumer), and a node depending on
several producers accumulates resolution across separate events
(multiple dependencies). Verified for both 1-hop and 2-hop transitive
(diamond) graphs. Ready nodes are handed to the Neural Director one at a
time over a backpressure-safe valid/ready interface.
\begin{fnwarn}[No slot reclamation]
\code{ST\_DISPATCHED} is terminal: node table slots are never reused
once dispatched. A long-running system that keeps registering new
nodes without limit will eventually exhaust \code{N\_NODES} --- this is
a real, measured consequence (a benchmark testbench hit exactly this
deadlock via node-id wraparound before \code{N\_NODES} was sized
generously enough). Slot reclamation is explicitly deferred, not
forgotten.
\end{fnwarn}
\section{Neural Director}
Dispatches ready job descriptors to whichever of \code{N\_SLOTS} Memory
Manager instances is currently free --- \textbf{first-free} scheduling: a
fixed, lowest-index-wins priority scan, not load-balanced. Slot-busy
tracking and completion detection are always-active, independent of
whatever the allocate/scan control state happens to be that cycle (the
same ``don't gate a per-unit event behind one shared FSM state''
principle applied throughout this design). A completed slot's node id
is tracked (\code{slot\_node\_id}) so its completion can be resolved back
to a \code{producer\_done} event for the Dependency Manager, closing the
wake-up loop without any external glue logic.
\begin{fnnote}[Measured scheduling imbalance]
Real per-slot data (\code{N\_SLOTS}=4, a 128-neuron workload) shows
slots 0/1 delivering 1008 real tiles each while slots 2/3 deliver only
16 each, despite all four slots reporting near-100\% ``busy''
utilization --- direct, measured evidence that fixed lowest-index
priority does not distribute load evenly once the shared PSRAM port is
the real constraint. See ch.~\ref{ch:impl2}.
\end{fnnote}
\section{Memory Manager + Neural Processor (per slot)}
Each slot pairs one \code{memory\_manager.v} instance with one
\code{neural\_processor.v} instance. The Memory Manager double-buffers
tile fetches (compute tile $N$ while prefetching tile $N{+}1$) and
presents the Neural Processor with a simple ``data available''
interface (\code{operand\_valid/ready}, \code{tile\_last}) --- the
processor never sees PSRAM request/wait cycles directly. A tile's
activation half is requested from the shared Activation Cache; its
weight half is fetched directly (weights are per-neuron, never shared,
so caching them would not help). A bank is presentable to the processor
only once \emph{both} halves have arrived
(\code{bank\_ready = bank\_x\_ready \& bank\_w\_ready}).
\section{Activation Cache}
\label{sec:archcache}
A single shared instance (not one per slot) serving every Memory
Manager's activation-fetch requests. Single-tag design: one cached
\code{x\_base} at a time, filled tile-by-tile on first use, served
directly from an on-chip buffer on every subsequent request for the same
vector --- no PSRAM access on a hit. A request for a different
\code{x\_base} invalidates the cache and restarts filling from tile~0;
this is always \emph{correct} (never serves stale data) but can thrash
under interleaved, genuinely-different-\code{x\_base} concurrent
traffic --- an honestly documented limitation, not exercised by this
project's own realistic dense-layer workloads (where many neurons of one
layer share one input vector, dispatched together). Full detail,
including the real Fmax cost this module introduces, in
ch.~\ref{ch:mem}.
\section{Slot Memory Arbiter}
Funnels \code{N\_SLOTS}$+$1 independent backend ports (one per Memory
Manager's weight/write-back traffic, plus one for the Activation
Cache's own traffic) down to the one real physical PSRAM port. Fixed
lowest-index priority, same convention as the Director. Every incoming
request is latched into a per-port pending register regardless of
arbiter state --- a byte-level backend protocol quirk discovered by real
simulation (a fire-and-forget single-cycle request pulse can arrive
while the shared bus is owned by another port; a naive ``grant only
while live'' arbiter would silently drop it) made this latch a
correctness requirement, not an optimization.
\section{Real, unmodified V1 PSRAM backend}
\code{memory\_interface.v} and \code{psram\_controller.v} are reused
byte-for-byte from the frozen \code{hardware/v1/} tree. The controller's
own real page-mode support (fast same-page continuation vs.\ a slower
cold access) was already implemented in V1 and is exploited more
effectively by V2's word-level burst rewrite (ch.~\ref{ch:mem}) --- no
change to the controller itself was needed or made.
@@ -0,0 +1,95 @@
\chapter{Compute datapath}
\label{ch:datapath}
\section{Bit-exact reuse of V1's arithmetic}
\code{neural\_processor.v} implements the identical INT8/INT32 arithmetic
chain as V1's own \code{neuron\_parallel.v}/\code{mac8.v}/\code{mac\_unit.v}
--- verified bit-exact against V1's own modules, instantiated side-by-side
in the same testbench, across 7 test cases including extreme INT8 values,
back-to-back zero-gap tiles, and multi-tile jobs. What changed is the
\emph{pipelining}, not the math.
\begin{center}
\begin{tikzpicture}[font=\scriptsize,node distance=3mm,start chain=going right,
every node/.style={fnblock,minimum width=15mm,minimum height=8mm,on chain}]
\node[fnblockT]{INT8\\$\times$\,INT8};
\node{INT16\\product};
\node{sign-ext\\INT32};
\node[fnblockD]{accumulate\\INT32};
\node{$+$ bias};
\node[fnblockA]{activation};
\node[fnblockT]{sat. INT8};
\foreach \i [count=\j from 2] in {1,...,6}
\draw[fnarrow] (chain-\i) -- (chain-\j);
\end{tikzpicture}
\end{center}
\section{8-stage pipeline}
\code{neural\_processor.v} is fully pipelined, throughput-oriented (one
new tile accepted per cycle in steady state, given a continuous operand
stream):
\begin{center}
\begin{tikzpicture}[font=\scriptsize,node distance=2.5mm,start chain=going below,
every node/.style={on chain,fnblock,minimum width=64mm}]
\node[fnblockA]{Stage 0 --- input alignment / register (\code{x0}, \code{w0})};
\node{Stage 1 --- per-lane multiply (\code{P\_IN}$\times$\code{MULT18X18D})};
\node{Stages 2..$(1{+}\log_2\text{P\_IN})$ --- balanced adder tree};
\node[fnblockD]{accumulate (gated by job state, cleared at job start)};
\node{$+$bias, activation select};
\node[fnblockT]{INT8 saturation / output register};
\foreach \i [count=\j from 2] in {1,...,5}
\draw[fnarrow] (chain-\i) -- (chain-\j);
\end{tikzpicture}
\end{center}
With \code{P\_IN}=8 the adder tree has 3 levels, giving an 8-stage pipeline
overall. \code{tile\_last} is gated identically to \code{valid} at every
stage (\code{last0 <= (operand\_valid \&\& operand\_ready) ? tile\_last :
1'b0;}) --- an early draft left it ungated, letting a ``last'' tag
propagate one cycle ahead of its own valid/data pair on jobs where
\code{tile\_last} was asserted before \code{operand\_ready} rose (legal
valid-before-ready producer behavior); found and fixed via a
cycle-by-cycle dump of the pipeline's own internal valid/last signals,
re-verified against the full 7-test regression.
\section{Accumulator width: 24 vs.\ 32 bits}
A real, 6-seed placement sweep (reusing already-synthesized netlists,
real \code{nextpnr-ecp5} place\&route only) resolved an earlier
single-seed measurement that had suggested \code{ACC\_WIDTH}=32 was
marginally faster:
\begin{tabularx}{\textwidth}{L{2.6cm} C{2.0cm} C{2.0cm} C{2.0cm} C{1.6cm}}
\toprule
\rowh \thd{ACC\_WIDTH} & \thd{mean Fmax} & \thd{min} & \thd{max} & \thd{stdev} \\
\midrule
32 & 170.12~MHz & 145.73 & 183.96 & 14.16 \\
\rowa 24 & \textbf{180.71~MHz} & 175.16 & 185.49 & \textbf{4.21} \\
\bottomrule
\end{tabularx}
\begin{center}\footnotesize\itshape\color{fnGrey}
Real place\&route, P\_IN=8, 6 seeds each (default plus 5 explicit).\end{center}
\begin{fnnote}[Why a single seed misled]
Over 6 real placement seeds, \code{ACC\_WIDTH}=24 has both a higher mean
Fmax ($+$6.2\%) and a much tighter seed-to-seed spread ($\approx$3.4$\times$
tighter) than \code{ACC\_WIDTH}=32 --- combined with fewer LUT/FF/CCU2C at
24 bits and identical bit-exact correctness, \code{ACC\_WIDTH}=24 is
recommended for any new P\_IN=8 INT8 configuration, where product
magnitudes never need more than 24 bits of accumulator headroom.
\end{fnnote}
\section{Activation and saturation}
Identical encoding and bit-test logic to V1 (bilateral saturation for
\code{ACT\_NONE}, positive-only for \code{ACT\_RELU}, both INT8-range).
Every job dispatched by \code{dataflow\_core.v} currently hardcodes
\code{job\_bias=0}, \code{job\_activation=ACT\_RELU} --- a documented
simplification carried through every milestone since M4/M5, not yet
exposed per-node by the Dependency Manager's own job descriptor.
\begin{tabularx}{\textwidth}{L{2.6cm} C{1.4cm} Y}
\toprule
\rowh \thd{Encoding} & \thd{Value} & \thd{Behavior} \\
\midrule
\code{ACT\_NONE} & \code{2'd0} & Linear: bilateral saturation to $[-128,+127]$. \\
\rowa \code{ACT\_RELU} & \code{2'd1} & $\max(0,x)$, positive saturation to $+127$ (used by every V2 job today). \\
\bottomrule
\end{tabularx}
@@ -0,0 +1,66 @@
\chapter{Parameters and configurability}
\label{ch:param}
\section{Build parameters (synthesis-time)}
\begin{tabularx}{\textwidth}{L{3.0cm} C{1.8cm} Y}
\toprule
\rowh \thd{Parameter} & \thd{Default} & \thd{Meaning} \\
\midrule
\code{DATA\_WIDTH} & 8 & Data width (INT8), unchanged from V1. \\
\rowa \code{ACC\_WIDTH} & 32 & Accumulator width; \textbf{24 recommended} for new P\_IN=8 configurations (ch.~\ref{ch:datapath}). \\
\code{P\_IN} & 8 & Parallel MAC lanes per neuron per tile; must be even (word-level burst constraint, ch.~\ref{ch:mem}). \\
\rowa \code{ADDR\_WIDTH} & 23 & Byte-address width (8~MB), unchanged from V1. \\
\code{N\_SLOTS} & 4 (RTL default) & Concurrent Memory Manager$+$Neural Processor pairs. \textbf{2 recommended} --- see the honesty note below. \\
\rowa \code{N\_NODES} & 16 & Dependency Manager node-table depth. Sized to the largest node-id range a graph will ever use; never reclaimed (ch.~\ref{ch:arch}). \\
\code{MAX\_DEPS} & 4 & Maximum producers a single node can list. \\
\rowa \code{QUEUE\_DEPTH} & 8 & Neural Director's own ready-job FIFO depth. \\
\code{MAX\_TILES} & 16 (internal, activation\_cache.v) & Longest activation vector the shared cache can hold; not yet exposed as a top-level parameter. \\
\rowa \code{PSRAM\_DATA\_WIDTH} & 16 & Physical PSRAM data bus width, unchanged from V1. \\
\code{CLK\_FREQ\_MHZ} & 80 & Frequency used in \code{psram\_controller.v}'s own timing formulas (unmodified V1 module). \\
\bottomrule
\end{tabularx}
\begin{fnwarn}[\texttt{N\_SLOTS} is a real, measured trade-off, not a free parameter]
Unlike V1's \code{PARALLEL} (a pure resource/frequency trade-off),
\code{N\_SLOTS} interacts with a real, measured system bottleneck (the
one physical PSRAM port). \code{N\_SLOTS}=1 and \code{N\_SLOTS}=2 both
show a real net wall-clock win over the pre-optimization baseline;
\code{N\_SLOTS}=4 shows \emph{no} additional real throughput and, with
the activation cache active, \textbf{fails the 80\,MHz timing target
outright} (ch.~\ref{ch:impl2}). Do not simply raise \code{N\_SLOTS} for
more perceived parallelism without re-running the real benchmark suite.
\end{fnwarn}
\section{Word-alignment constraint (post word-burst rewrite)}
Since the memory backend now moves 16-bit words rather than bytes
(ch.~\ref{ch:mem}), every tile base address the system computes
(\code{x\_base + tile\_idx*P\_IN}, and equivalently for weights) must
land on an even byte address. \code{P\_IN} even and \code{x\_base}/
\code{w\_base} themselves even together guarantee this for every tile of
every job --- true of every address this project's own testbenches use,
and a trivial constraint for any real loader/host to satisfy.
\section{Characterized configurations}
\begin{tabularx}{\textwidth}{C{1.6cm} C{2.6cm} C{2.6cm} Y}
\toprule
\rowh \thd{N\_SLOTS} & \thd{Fmax, word-burst only} & \thd{Fmax, $+$activation cache} & \thd{Notes} \\
\midrule
1 & 152.44~MHz & 131.79~MHz & Best real wall-clock speedup (3.86$\times$ vs baseline); no arbitration contention possible. \\
\rowa 2 & 133.58~MHz & \textbf{87.72~MHz} & \textbf{Recommended default} --- real 2.45$\times$ speedup vs baseline, still comfortably above 80\,MHz. \\
4 & 112.07~MHz & 65.01~MHz (\FAIL) & No additional real throughput; fails 80\,MHz with the cache active. Not recommended. \\
\rowa 8 & 92.63~MHz (dataflow\_core only, no real PSRAM chain) & not re-measured & Real DSP ceiling for P\_IN=8 (64/72 MULT18X18D); a resource ceiling, not a useful operating point. \\
\bottomrule
\end{tabularx}
\section{Build versus runtime}
\begin{center}
\begin{tikzpicture}[font=\footnotesize,node distance=6mm]
\node[fnblockD,minimum width=54mm,minimum height=17mm](b){\textbf{BUILD (synthesis)}\\[2pt]
{\scriptsize N\_SLOTS, N\_NODES, MAX\_DEPS,}\\{\scriptsize QUEUE\_DEPTH, P\_IN, DATA\_WIDTH, ACC\_WIDTH}\\{\scriptsize $\Rightarrow$ machine ceiling}};
\node[fnblockT,right=16mm of b,minimum width=54mm,minimum height=17mm](r){\textbf{RUNTIME (node registration)}\\[2pt]
{\scriptsize reg\_node\_id, reg\_required, reg\_producer\_ids,}\\{\scriptsize reg\_x\_base/w\_base/n\_tiles/result\_addr}\\{\scriptsize $\Rightarrow$ the actual dependency graph}};
\draw[fnbus] (b) -- node[fnlbl,above]{$\le$} (r);
\end{tikzpicture}
\end{center}
Full field-level description of the runtime (node registration)
interface: ch.~\ref{ch:regs}.
@@ -0,0 +1,185 @@
\chapter{Memory subsystem}
\label{ch:mem}
\section{Baseline: reused byte-level V1 backend}
V2's first working milestones connected each Memory Manager's own
\code{prefetch\_engine.v} to the real, unmodified V1 chain
\code{int8\_memory\_access.v} $\to$ \code{memory\_interface.v} $\to$
\code{psram\_controller.v}, fetching one INT8 byte per transaction ---
exactly the contract V1's own \code{neuron\_memory.v} already used
against the same backend. This was correct and fully verified (bit-exact
end-to-end through the real PSRAM chain), but it was not the fastest
possible use of that chain.
\section{Optimization \#1 --- word-level burst reads}
\label{sec:burst}
Direct inspection of \code{int8\_memory\_access.v} shows it already
converts every 8-bit logical request into a \textbf{full 16-bit PSRAM
word access} internally (\code{mem\_addr <= addr >> 1}, one byte lane
selected via \code{lb\_n}/\code{ub\_n}) --- so a byte-at-a-time fetch was
already paying for two bytes of real PSRAM bandwidth per transaction
while using only one, and paying \code{int8\_memory\_access.v}'s own
request/wait round-trip twice for every real word instead of once.
\code{prefetch\_engine.v} (weights) and \code{activation\_cache.v}
(activations, \S\ref{sec:cache}) now talk directly to
\code{memory\_interface.v}'s own 16-bit word interface, \textbf{skipping
\code{int8\_memory\_access.v} entirely}. Both files remain frozen,
byte-for-byte unmodified V1 --- V2 simply chooses to reuse the lower
(word-level) layer of the same frozen stack instead of the byte-splitting
layer on top of it, the same precedent already set by
\code{slot\_mem\_arbiter.v} not reusing V1's own \code{mem\_arbiter.v}
verbatim.
\begin{fnnote}[Real, measured result --- single job, real PSRAM]
\begin{tabularx}{\textwidth}{C{2.2cm} C{2.4cm} C{2.4cm} C{1.6cm}}
\toprule
\rowh \thd{n\_tiles} & \thd{cycles, before} & \thd{cycles, after} & \thd{$\Delta$} \\
\midrule
1 & 166 & 84 & $-49\%$ \\
\rowa 3 & 446 & 204 & $-54\%$ \\
5 & 728 & 322 & $-56\%$ \\
\bottomrule
\end{tabularx}
Real Verilator simulation, real V1 PSRAM chain, all results still
bit-exact.
\end{fnnote}
Combined real wall-clock effect (256-neuron sustained workload, cycles
$\div$ real POST-P\&R Fmax): a \textbf{2.24--2.37$\times$} speedup across
every \code{N\_SLOTS} tested, at a negligible real Fmax cost
(unchanged at \code{N\_SLOTS}=1; $-6.2\%$ at \code{N\_SLOTS}=2; $-1.2\%$
at \code{N\_SLOTS}=4).
\begin{fnwarn}[Why not just pipeline more requests instead?]
\code{int8\_memory\_access.v}'s own \code{STATE\_IDLE} only samples a
new \code{req} once back in \code{STATE\_IDLE} after the previous
transaction's \code{mem\_ready} --- it fundamentally does not support
request pipelining. No wrapper built \emph{on top of} it can avoid
paying its round-trip cost twice per word; only bypassing it (talking to
\code{memory\_interface.v} directly) actually removes the redundancy.
This is why the fix reaches one layer lower in the stack rather than
adding queuing logic in front of the existing byte-level port.
\end{fnwarn}
\section{Optimization \#2 --- shared activation cache}
\label{sec:cache}
In the realistic dense-layer workloads this project benchmarks, many
neurons of the same layer share the \emph{exact same} activation vector.
Before this optimization, each of \code{N\_SLOTS} Memory Manager
instances re-fetched that identical vector from PSRAM independently ---
real, measured, redundant traffic on the one shared PSRAM port.
\code{activation\_cache.v} (a new, single shared instance per
\code{dataflow\_core}, not one per slot) fetches a given \code{x\_base}
vector once, tile by tile on first use, and serves every subsequent
request for the same vector directly from an on-chip buffer.
\begin{fnnote}[Real, measured result --- 256-neuron sustained workload, D-Stress]
\begin{tabularx}{\textwidth}{C{1.4cm} C{2.4cm} C{2.4cm} C{2.0cm} C{2.0cm}}
\toprule
\rowh \thd{N\_SLOTS} & \thd{cycles, burst only} & \thd{cycles, $+$cache} & \thd{Fmax, burst} & \thd{Fmax, $+$cache} \\
\midrule
1 & 348682 & 174610 & 152.44 & 131.79 \\
\rowa 2 & 307602 & 185428 & 133.58 & \textbf{87.72} \\
4 & 307346 & 184795 & 112.07 & 65.01 (\FAIL) \\
\bottomrule
\end{tabularx}
A further real 1.66--2.00$\times$ cycle reduction on top of optimization~\#1,
$\approx$4$\times$ combined vs.\ the original byte-level baseline.
\end{fnnote}
\begin{fnwarn}[Real, measured Fmax cost --- read this before raising N\_SLOTS]
The shared cache's real Fmax cost is \textbf{much steeper} than
optimization~\#1's: a single central resource with \code{N\_SLOTS}
request ports, a broadcast-capable hit-check evaluated combinationally
every cycle for every port, and a shared \code{tile\_store} array create
a genuine fan-in/routing hot spot that grows with \code{N\_SLOTS}.
\code{N\_SLOTS}=2 (recommended) still passes 80\,MHz (87.72\,MHz, margin
down from $+$67\% to $+$9.7\%); \code{N\_SLOTS}=4 \textbf{fails outright}
(65.01\,MHz). This is the central input to ch.~\ref{ch:roadmap}'s own
open work item on cache pipelining.
\end{fnwarn}
Combined real wall-clock speedup vs.\ the original byte-level baseline
(both optimizations together): \code{N\_SLOTS}=1 \textbf{3.86$\times$};
\code{N\_SLOTS}=2 \textbf{2.45$\times$} (the recommended configuration);
\code{N\_SLOTS}=4 2.29$\times$ but a real \emph{regression} versus
optimization~\#1 alone, since its own Fmax now fails 80\,MHz.
\subsection{Design notes}
Single-tag, tile-granular: a request tag mismatch invalidates the cache
and restarts filling from tile~0 for the new \code{x\_base} --- always
correct, never serves stale data, but can thrash under interleaved,
genuinely-different-\code{x\_base} concurrent traffic (not exercised by
this project's own dense-layer workloads, where sharing is real and
sustained). Requests are latched per-slot on arrival (the same
``queue, don't drop'' idiom used by the arbiter, \S\ref{sec:archcache}
of ch.~\ref{ch:arch}) and served with a broadcast ack the cycle a
matching tile becomes valid, so multiple slots pending on the same,
about-to-arrive tile are all served the same cycle.
\begin{fnnote}[Two real bugs found and fixed during implementation]
(1)~A target-bank/pending-bank race: a later handoff could queue a new
cache request (targeting a different double-buffer bank) in the same
cycle an earlier request was still awaiting its own ack, and
non-blocking-assignment ``last write wins'' semantics silently
misattributed which bank the earlier request's data landed in --- the
same bug class already found once for the weight-side
\code{pf\_target\_bank} register, fixed with the identical two-register
(pending/target) staging pattern. (2)~A zero-width Verilog replication
at \code{N\_SLOTS}=1 (\code{\{\$clog2(1)\{1'b0\}\}} $=$ \code{\{0\{...\}\}},
illegal outside a concatenation), the same class already found once in
\code{neural\_director.v} and fixed with the same width-agnostic
\code{'0} literal. Both found via real simulation, not by inspection.
\end{fnnote}
\section{Real PSRAM chain (unmodified V1)}
\code{memory\_interface.v} and \code{psram\_controller.v} are byte-for-byte
identical to V1's own copies throughout this chapter --- the real
page-mode support they already implement (fast same-page continuation,
slower cold access) is exploited more effectively by the word-level
rewrite, not changed. The real ISSI \code{IS66WVE4M16EBLL-70BLI} chip and
its board wiring are unchanged from V1 (ch.~\ref{ch:hw}).
\section{SDRAM upgrade addendum (2026-09-07) --- current, authoritative
memory architecture}
\label{sec:sdram-mem-addendum}
\begin{fnwarn}[Superseded architecture]
The PSRAM-based chain described above (\S\S\ref{sec:burst}--\ref{sec:cache})
belongs to an earlier V2 milestone. The project has since closed on a
single-external-memory architecture (real \code{decisions.log} DEC-0034):
\textbf{one SDR SDRAM device, one \code{sdram\_controller.v} instance},
serving weights, activations, AND results through
\code{sdram\_unified\_backend.v}'s two logical ports (W: 64-bit weight
read; AR: 16-bit, byte-maskable activation-read/result-write), arbitrated
2-way priority (W wins when both pending). No PSRAM, no second physical
memory device, in the current, frozen hardware path.
\end{fnwarn}
The device itself was upgraded mid-project from an 8\,MB part
(\code{AS4C4M16SA-6TIN}) to the current \textbf{AS4C32M16SB-7BIN,
64\,MB (512\,Mbit), 54-ball FBGA} --- both the row/column/bank geometry
(\code{sdram\_controller.v}'s \code{ROW\_BITS}/\code{COL\_BITS}/
\code{BANK\_BITS} parameters, now 13/10/2) and the SPI host protocol's
own address-field width (23$\to$26-bit byte address; WRITE\_JOB payload
grew 15$\to$18 bytes) changed accordingly. Full electrical/pinout data
and the complete FPGA$\leftrightarrow$SDRAM ball mapping are in
ch.~\ref{ch:hw}, \S\ref{sec:sdram-addendum} (kept in one place to avoid
two copies of the same real data).
\subsection{Real, measured clock closure}
\textbf{N\_SLOTS=4 @ 64\,MHz is the frozen production configuration}:
real \code{nextpnr-ecp5} P\&R, 8/8 tested seeds PASS (worst 66.58\,MHz,
worst WNS $+0.605$\,ns). \textbf{N\_SLOTS=8 @ 64\,MHz remains an open
item}: 5/8 seeds PASS (worst 60.12\,MHz, worst WNS $-1.009$\,ns) after
a real critical-path optimization (\code{sdram\_unified\_backend.v}'s
weight-cache hit-index encoder, rewritten from a serially-dependent
priority scan to a flat, parallel one-hot compare --- real errors.log
ERR-0029/decisions.log DEC-0040). 80\,MHz was tested with a genuinely
regenerated PLL (not merely a \code{--freq} flag) and is \textbf{not
achievable} at either processor count (0/8 seeds pass, both before and
after the ERR-0029 optimization) --- the achievable Fmax is a property
of the routed fabric, confirmed identical between the 64\,MHz- and
80\,MHz-targeted netlists. Bit-exact functional correctness (D-Stress,
256/256 neurons vs.\ golden model) is unaffected at every configuration
tested, including through this optimization.
@@ -0,0 +1,66 @@
\chapter{Dataflow scheduling}
\label{ch:sched}
\section{Node lifecycle}
\begin{center}
\begin{tikzpicture}[font=\scriptsize,node distance=16mm,>=Stealth]
\node[fnstate](e){EMPTY};
\node[fnstate,right=of e](w){WAITING};
\node[fnstate,right=of w](r){READY};
\node[fnstate,right=of r](d){DISPATCHED};
\draw[fnarrow] (e) -- node[fnlbl,above]{register, deps$>$0} (w);
\draw[fnarrow] (e) to[bend left=25] node[fnlbl,above]{register, deps$=$0} (r);
\draw[fnarrow] (w) -- node[fnlbl,above]{all producers done} (r);
\draw[fnarrow] (r) -- node[fnlbl,above]{Director accepts} (d);
\end{tikzpicture}
\end{center}
\code{DISPATCHED} is terminal (\S\ref{ch:arch}): a real, honest
consequence, not an oversight --- see the roadmap (ch.~\ref{ch:roadmap})
for the deferred slot-reclamation work item.
\section{Verified graph topologies}
\begin{tabularx}{\textwidth}{L{3.4cm} Y}
\toprule
\rowh \thd{Topology} & \thd{What it proves} \\
\midrule
Shared producer, 2 consumers & One node's completion resolves the dependency count of \emph{two} different waiting nodes independently. \\
\rowa Multiple producers, 1 consumer & A node with \code{required}$>$1 only becomes \code{READY} once \emph{every} listed producer has completed, tracked across separate wake-up events. \\
2-hop transitive diamond ($A,B$ independent; $C$ dep-$A$; $D$ dep-$B$; $E$ dep-$C,D$) & Correct cascading wake-up two hops deep --- $E$ does not fire until $C$ and $D$ have \emph{themselves} genuinely completed, not merely been marked ready. \\
\rowa Mixed-depth fan-in (node depending on both a root and a 1-hop descendant) & Dependency resolution does not assume a uniform graph depth. \\
Multilayer (8 layer-1 neurons, random INT8 data, feeding 2 layer-2 neurons reading their real shared result bytes) & Real cross-node \emph{data} forwarding through real PSRAM --- layer-2's golden values are computed from the real bytes layer-1 actually wrote, not from an independent expectation. \\
\bottomrule
\end{tabularx}
All topologies above were exercised with the real, full
\code{neural\_multiprocessor.v} (real V1 PSRAM chain, real
\code{slot\_mem\_arbiter.v}) and verified bit-exact against a software
golden model.
\section{First-free dispatch}
The Neural Director's own scheduling policy is deliberately the simplest
one that is provably correct: a fixed, lowest-index priority scan over
currently-free slots. Round-robin, least-loaded, or any fairness-aware
alternative was explicitly deferred until real measured data showed
whether it mattered (\S\ref{sec:fairness}).
\section{Measured scheduling behavior}
\label{sec:fairness}
Real per-slot data (\code{N\_SLOTS}=4, a 128-neuron dense-layer
workload) shows a striking imbalance: slots~0 and~1 each deliver 1008
real tiles, while slots~2 and~3 deliver only 16 each --- despite all four
slots reporting near-100\% ``busy'' utilization. The cause is not
unfairness in isolation: once the shared PSRAM port is saturated
(ch.~\ref{ch:mem}), there is rarely a moment where the low-index slots
are simultaneously busy \emph{and} the high-index slots have nothing to
do, so the fixed low-index-first scan keeps re-selecting the same two
slots. This is a real, measured limitation of the current scheduler,
carried into ch.~\ref{ch:roadmap} as an open item rather than patched
without first measuring whether it is worth the added complexity for
real workloads.
\section{Correctness guarantees (measured, not assumed)}
Across the full final benchmark campaign (6 workloads $\times$ 4
\code{N\_SLOTS} configurations, re-verified after both memory
optimizations): \textbf{zero} lost jobs, \textbf{zero} duplicated jobs
(\code{jobs\_allocated == jobs\_completed == neurons\_completed} exactly,
every run), \textbf{zero} deadlocks, \textbf{zero} timeouts, correct
multi-hop dependency wake-up in every topology tested.
@@ -0,0 +1,65 @@
\chapter{Host / graph-loader interface}
\label{ch:host}
\begin{fnwarn}[Scope of this chapter]
V1's own host interface is a real, placed, physically-verified SPI Mode~0
slave (ch.~7 of the V1 datasheet). V2's equivalent --- a node-registration
bus into \code{neural\_multiprocessor.v} --- has, in this revision, been
exercised exclusively from Verilator testbenches and unconstrained
synthesis top-levels. This chapter describes the \textbf{logical}
protocol only; no real host-side driver (SPI or otherwise) has been
built or placed yet. See ch.~\ref{ch:roadmap}.
\end{fnwarn}
\section{Node registration protocol}
A simple valid/ready producer interface, backpressure-safe: the loader
holds \code{reg\_valid} and the node's own fields until \code{reg\_ready}
is observed high on the same cycle, exactly like registering into any
FIFO. \code{reg\_ready} for a given \code{reg\_node\_id} is asserted
whenever that node's own table slot is \code{EMPTY} (\S\ref{ch:sched}).
\begin{tabularx}{\textwidth}{L{3.2cm} C{1.6cm} Y}
\toprule
\rowh \thd{Field} & \thd{Width} & \thd{Meaning} \\
\midrule
\code{reg\_node\_id} & $\lceil\log_2\text{N\_NODES}\rceil$ & This node's own id --- doubles as its table slot index. \\
\rowa \code{reg\_required} & $\lceil\log_2(\text{MAX\_DEPS}{+}1)\rceil$ & How many of \code{reg\_producer\_ids} are meaningful (0 $\Rightarrow$ immediately \code{READY}). \\
\code{reg\_producer\_ids} & \code{MAX\_DEPS}$\times\lceil\log_2\text{N\_NODES}\rceil$ & Packed array of producer node ids this node depends on. \\
\rowa \code{reg\_x\_base} & \code{ADDR\_WIDTH} & Base byte address of this node's activation vector. \\
\code{reg\_w\_base} & \code{ADDR\_WIDTH} & Base byte address of this node's weight vector. \\
\rowa \code{reg\_n\_tiles} & 16 & Number of P\_IN-wide tiles to accumulate. \\
\code{reg\_result\_addr} & \code{ADDR\_WIDTH} & Byte address the computed INT8 result is written to. \\
\bottomrule
\end{tabularx}
\begin{fnnote}[A node id is a real, finite resource]
Because dispatched node table slots are never reclaimed
(\S\ref{ch:sched}), a loader driving many independent jobs over a long
session must use a fresh \code{reg\_node\_id} for each one, within
\code{N\_NODES}. Reusing a value before the system has been reset will
simply be refused (\code{reg\_ready} stays low for an occupied,
non-\code{EMPTY} node id) --- it will not corrupt anything, but it will
also not register.
\end{fnnote}
\section{Result readback}
The computed INT8 result is written to \code{reg\_result\_addr} through
the same real PSRAM chain every other memory access uses --- there is no
separate result-readback port; the host/loader reads the result byte
back from PSRAM directly, the same convention every V2 testbench in this
project uses for verification.
\section{What a real host driver would still need to add}
\begin{itemize}
\item A physical transport (SPI, parallel bus, or otherwise) carrying
the fields of \S\ref{ch:host}'s own table across a real pin
interface --- not designed in this revision.
\item A completion-notification path back to the host (V1's own
\code{data\_ready\_n}/\code{STATUS.done} has no V2 analogue yet);
today, completion is only observable internally
(\code{dir\_job\_out\_done}) or by polling the expected result
address.
\item Per-job \code{bias}/\code{activation} selection, currently
hardcoded to \code{bias=0}/\code{ACT\_RELU} for every job
(\S\ref{ch:datapath}).
\end{itemize}
@@ -0,0 +1,55 @@
\chapter{Top-level module}
\label{ch:toplevel}
\section{\texttt{neural\_multiprocessor.v}}
The real, hardware-facing top level: \code{dataflow\_core.v} (Dependency
Manager $+$ Neural Director $+$ \code{N\_SLOTS}$\times$(Memory Manager $+$
Neural Processor) $+$ Activation Cache) with its \code{N\_SLOTS}$+$1
Memory Backend Interface ports funneled through \code{slot\_mem\_arbiter.v}
down to the real, unmodified V1 PSRAM chain
(\code{memory\_interface.v} $\to$ \code{psram\_controller.v}).
\begin{tabularx}{\textwidth}{L{3.4cm} C{1.2cm} C{1.6cm} Y}
\toprule
\rowh \thd{Port} & \thd{Dir} & \thd{Width} & \thd{Function} \\
\midrule
\code{clk}, \code{rst} & IN & 1 & System clock, synchronous reset. \\
\rowa \code{reg\_valid} & IN & 1 & Node registration request (ch.~\ref{ch:host}). \\
\code{reg\_ready} & OUT & 1 & This node id's table slot is \code{EMPTY}. \\
\rowa \code{reg\_node\_id} & IN & $\lceil\log_2\text{N\_NODES}\rceil$ & Node id. \\
\code{reg\_required} & IN & $\lceil\log_2(\text{MAX\_DEPS}{+}1)\rceil$ & Producer count. \\
\rowa \code{reg\_producer\_ids} & IN & \code{MAX\_DEPS}$\times\lceil\log_2\text{N\_NODES}\rceil$ & Packed producer id list. \\
\code{reg\_x\_base}, \code{reg\_w\_base}, \code{reg\_result\_addr} & IN & \code{ADDR\_WIDTH} each & Job descriptor addresses. \\
\rowa \code{reg\_n\_tiles} & IN & 16 & Tile count. \\
\code{psram\_a} & OUT & \code{ADDR\_WIDTH} & PSRAM address bus (real V1 controller, unmodified). \\
\rowa \code{psram\_dq} & INOUT & \code{PSRAM\_DATA\_WIDTH} & PSRAM bidirectional data bus. \\
\code{psram\_ce\_n}, \code{psram\_oe\_n}, \code{psram\_we\_n}, \code{psram\_lb\_n}, \code{psram\_ub\_n}, \code{psram\_zz\_n} & OUT & 1 each & PSRAM control, identical to V1's own real, verified signal set. \\
\bottomrule
\end{tabularx}
\begin{fnnote}[No \texttt{int8\_memory\_access.v} in this datapath]
Earlier milestones instantiated V1's \code{int8\_memory\_access.v}
between the arbiter and \code{memory\_interface.v}. Post word-burst
rewrite (ch.~\ref{ch:mem}), it is no longer instantiated here --- the
file itself is untouched (still frozen V1); V2 simply reuses one layer
lower in the same frozen stack.
\end{fnnote}
\section{Internal hierarchy}
\noindent\code{neural\_multiprocessor.v}
\begin{itemize}[leftmargin=2.4em]
\footnotesize
\item \code{u\_dataflow\_core} : \code{dataflow\_core.v}
\begin{itemize}
\item \code{u\_dep\_mgr} : \code{dependency\_manager.v}
\item \code{u\_director} : \code{neural\_director.v}
\item \code{GEN\_SLOT[0..N\_SLOTS-1]}: \code{memory\_manager.v} $+$ \code{neural\_processor.v}
\begin{itemize}
\item[--] \code{u\_prefetch} : \code{prefetch\_engine.v} (weights only, word-level)
\end{itemize}
\item \code{u\_activation\_cache} : \code{activation\_cache.v}
\end{itemize}
\item \code{u\_arbiter} : \code{slot\_mem\_arbiter.v} (\code{N\_SLOTS}$+$1 ports)
\item \code{u\_memif} : \code{memory\_interface.v} (frozen V1)
\item \code{u\_psram\_ctrl} : \code{psram\_controller.v} (frozen V1)
\end{itemize}
@@ -0,0 +1,187 @@
\chapter[ECP5 implementation \& benchmarks]{ECP5 implementation, real benchmark campaign \& measured results}
\label{ch:impl2}
\section{Flow and verification discipline}
Every number in this chapter is labelled \textsc{Theoretical},
\textsc{Simulated}, \textsc{Post-P\&R measured}, or \textsc{Derived}
(a combination of two real measurements, e.g.\ cycles $\div$ real Fmax).
No result is invented, approximated to look better, or reported without
a matching real measurement.
\begin{tabularx}{\textwidth}{L{5.0cm} C{3.0cm} Y}
\toprule
\rowh \thd{Verification stage} & \thd{Outcome} & \thd{Covers} \\
\midrule
RTL simulation (Verilator 5.050) & \PASS & bit-exact correctness vs.\ a software golden model \\
\rowa ECP5 synthesis (Yosys \code{synth\_ecp5}) & \PASS, 0 problems & synthesizability, resource mapping \\
Place\&route (real \code{nextpnr-ecp5}) & \PASS at \code{N\_SLOTS}$\le$2 & LUT/FF/DSP, real timing \\
\rowa Full benchmark campaign & 24/24 bit-exact & 6 workloads $\times$ 4 \code{N\_SLOTS} configurations \\
\bottomrule
\end{tabularx}
\begin{fnnote}[Verilator, not Icarus, for V2]
Two independent Icarus Verilog v13.0 scheduling defects were found and
reproduced on minimal repros during V2's own M1 milestone (a
task/scope-entry desync and a spurious condition evaluation, both
edge-parity dependent) --- Verilator gives correct results on the same
repros. V1's own certification (performed separately, with Icarus) was
unaffected, since its own testbenches already avoided the trigger
pattern by convention; this is flagged honestly, not glossed over.
\end{fnnote}
\section{V1 vs.\ V2 --- final comparison}
Both systems full-system (not isolated modules), same
PARALLEL/P\_IN=8, same real, unmodified V1 PSRAM chain.
\begin{tabularx}{\textwidth}{L{3.6cm} C{2.6cm} C{2.6cm} Y}
\toprule
\rowh \thd{Metric} & \thd{V1} & \thd{V2 (N\_SLOTS=2)} & \thd{Class} \\
\midrule
Fmax & 68.65~MHz (\FAIL) & \textbf{87.72~MHz} (\PASS) & Post-P\&R \\
\rowa LUT (Total LUT4s) & 8907 & 4359 & Post-P\&R \\
FF (Total DFFs) & 4900 & 3924 & Post-P\&R \\
\rowa DSP (MULT18X18D) & 16 & 16 & Post-P\&R \\
BRAM (DP16KD) & 2 & 0 & Post-P\&R \\
\rowa cycles/neuron (1 neuron, 8 inputs, real PSRAM) & 209 & 166 & Simulated \\
Real wall-clock speedup vs.\ V1 & 1.00$\times$ & \textbf{2.6$\times$} & Derived \\
\bottomrule
\end{tabularx}
\begin{center}\footnotesize\itshape\color{fnGrey}
V1's own figures are its already-certified, frozen baseline (not
re-measured this session); V2's figures are real, current measurements
including both post-campaign optimizations.\end{center}
\begin{fnnote}[Where the win comes from --- and where it does not]
V2's advantage comes from a faster pipeline and a higher achievable
clock, \textbf{not} primarily from the multi-processor concurrency the
architecture was built to add. That concurrency's own real payoff, given
the single-PSRAM-port memory subsystem, is much smaller than a naive
\code{N\_SLOTS}$\times$\code{P\_IN} calculation would suggest ---
\S\ref{sec:scaling}.
\end{fnnote}
\section{Real \texttt{N\_SLOTS} sweep --- Fmax and resources}
Full system, real place\&route, both memory optimizations active
(word-burst \S\ref{sec:burstimpl} $+$ activation cache
\S\ref{sec:cacheimpl}).
\begin{tabularx}{\textwidth}{C{1.6cm} C{2.2cm} C{1.6cm} C{1.6cm} C{1.6cm} C{1.6cm} Y}
\toprule
\rowh \thd{N\_SLOTS} & \thd{Fmax} & \thd{LUT4} & \thd{FF} & \thd{DSP} & \thd{BRAM} & \thd{80\,MHz} \\
\midrule
1 & 131.79~MHz & 2760 & 2405 & 8/72 & 0 & \PASS \\
\rowa 2 & \textbf{87.72~MHz} & 4359 & 3924 & 16/72 & 0 & \PASS (\textbf{recommended}) \\
4 & 65.01~MHz & 9158 & 7986 & 32/72 & 0 & \FAIL \\
\bottomrule
\end{tabularx}
\subsection{Fmax versus N\_SLOTS}
\begin{center}
\begin{tikzpicture}
\begin{axis}[
width=0.68\textwidth,height=6.0cm,
xlabel={\footnotesize N\_SLOTS}, ylabel={\footnotesize Fmax [MHz]},
xtick={1,2,4}, ymin=50,ymax=145, ytick={50,65,80,95,110,125,140},
tick label style={font=\scriptsize}, label style={font=\footnotesize},
grid=major, grid style={fnRule!40},
legend style={font=\scriptsize,at={(0.5,-0.28)},anchor=north,legend columns=2}]
\addplot[fnBlue,mark=square*,thick,mark options={fill=fnBlue}]
coordinates {(1,152.44)(2,133.58)(4,112.07)};
\addlegendentry{word-burst only}
\addplot[fnRed,mark=*,thick,mark options={fill=fnRed}]
coordinates {(1,131.79)(2,87.72)(4,65.01)};
\addlegendentry{$+$ activation cache}
\draw[fnAmber,dashed,thick] (axis cs:1,80)--(axis cs:4,80);
\node[font=\scriptsize,text=fnAmber] at (axis cs:3.3,74){80 MHz target};
\end{axis}
\end{tikzpicture}
\end{center}
\begin{center}\footnotesize\itshape\color{fnGrey}
The activation cache's own real Fmax cost grows much faster with
N\_SLOTS than the arbiter-widening cost alone --- a single shared
resource with N\_SLOTS request ports and an unpipelined,
broadcast-capable hit-check.\end{center}
\section{Real parallel scaling}
\label{sec:scaling}
Not assumed --- computed from real cycle counts, largest workload
(256 independent neurons sharing one input vector).
\begin{tabularx}{\textwidth}{C{1.6cm} C{2.6cm} C{2.0cm} C{2.6cm} C{2.0cm}}
\toprule
\rowh \thd{N\_SLOTS} & \thd{Speedup(N)} & \thd{Efficiency} & \thd{PSRAM utilization} & \thd{Real wall-clock speedup vs.\ N=1} \\
\midrule
1 & 1.00$\times$ & 100\% & 55.5--71.8\% & 1.00$\times$ \\
\rowa 2 & 1.06$\times$ & 53\% & $\approx$90\% & 0.99$\times$ (a wash) \\
4 & 1.06$\times$ & 27\% & $\approx$90\% & 0.79$\times$ (\emph{slower}) \\
8 & 1.06$\times$ & 13\% & $\approx$90\% & --- \\
\bottomrule
\end{tabularx}
\begin{center}\footnotesize\itshape\color{fnGrey}
Pre-optimization figures, isolating the real scaling behavior from the
two memory optimizations' own effect (\S\ref{sec:burstimpl}--\ref{sec:cacheimpl}).\end{center}
\begin{fnwarn}[The central, measured finding]
Real cycle-count speedup from \code{N\_SLOTS}=1 to \code{N\_SLOTS}=8 is
essentially flat (1.05--1.06$\times$) for sustained, memory-bound
workloads --- the single shared PSRAM port saturates at
$\approx$90\% utilization regardless of \code{N\_SLOTS}$\ge$2. Once real
Fmax degradation is also folded in, \code{N\_SLOTS}=4 measures
\emph{slower} in real wall-clock time than \code{N\_SLOTS}=1. More
hardware parallelism made this workload class worse, not better,
because the bottleneck was never compute.
\end{fnwarn}
\section{Memory optimization \#1 --- word-level burst reads}
\label{sec:burstimpl}
See ch.~\ref{ch:mem}, \S\ref{sec:burst}, for the full rationale. Real,
measured single-job cycle reduction: $-$49\% (1 tile), $-$54\% (3
tiles), $-$56\% (5 tiles). Combined real wall-clock speedup on the
256-neuron sustained workload: 2.24--2.37$\times$ across every
\code{N\_SLOTS} tested, at negligible real Fmax cost.
\section{Memory optimization \#2 --- shared activation cache}
\label{sec:cacheimpl}
See ch.~\ref{ch:mem}, \S\ref{sec:cache}. A further real 1.66--2.00$\times$
cycle reduction on top of optimization~\#1, at a real, steep Fmax cost
that makes \code{N\_SLOTS}=4 fail 80\,MHz outright.
\begin{tabularx}{\textwidth}{C{1.6cm} C{2.4cm} C{2.4cm} C{2.4cm}}
\toprule
\rowh \thd{N\_SLOTS} & \thd{Wall-clock, baseline} & \thd{Wall-clock, final} & \thd{Total real speedup} \\
\midrule
1 & 5118.1~$\mu$s & 1324.9~$\mu$s & \textbf{3.86$\times$} \\
\rowa 2 & 5169.5~$\mu$s & 2113.9~$\mu$s & \textbf{2.45$\times$} (recommended) \\
4 & 6498.7~$\mu$s & 2842.6~$\mu$s (Fmax fails) & 2.29$\times$ but a real regression vs.\ \#1 alone \\
\bottomrule
\end{tabularx}
\section{Bottleneck analysis}
\begin{tabularx}{\textwidth}{L{2.8cm} Y}
\toprule
\rowh \thd{Candidate} & \thd{Verdict, with real evidence} \\
\midrule
\textbf{Memory (PSRAM port)} & \textbf{The real bottleneck.} $\approx$90\% utilization at N\_SLOTS$\ge$2; real compute-to-memory-wait ratio on the order of 1:170--1:220. \\
\rowa Compute (Neural Processor) & Not the bottleneck --- the pipeline is idle most of the time waiting for data. \\
Arbiter overhead & Real but small: a mandatory 1-cycle pending-latch (correctness, not choice) plus modest Fmax cost ($-$6\% at N\_SLOTS=2, word-burst alone). \\
\rowa Director/dependency logic & Not the bottleneck --- zero lost/duplicated jobs, no queueing backlog observed; a real \emph{fairness} issue exists (\S\ref{ch:sched}) but does not limit throughput. \\
DSP/LUT/FF availability & Not the bottleneck at N\_SLOTS$\le$4 --- all well under budget; DSP would eventually bind at N\_SLOTS=9 (64/72), never reached in practice since the memory bottleneck dominates first. \\
\bottomrule
\end{tabularx}
\section{Limitations, honestly stated}
\begin{itemize}
\item V1's own memory-utilization/stall figures were not re-measured
this session (V1 is frozen); only its already-certified numbers
are used for comparison.
\item No clean per-cycle split between ``processor computing'' and
``processor waiting for memory'' exists in the current
instrumentation --- reported figures use tile-delivery-rate
proxies, not an exact split.
\item Power/energy: \textbf{NOT MEASURED} --- no ECP5 power estimator
(\code{ecppower}, \code{icepower}, or equivalent) is available in
this project's toolchain; no value is invented in its place.
\item \code{N\_SLOTS}=8 was not re-measured full-system (real PSRAM
chain) after either memory optimization --- only
\code{dataflow\_core.v} alone, pre-optimization (92.63~MHz).
\end{itemize}
@@ -0,0 +1,119 @@
\chapter{Hardware and board}
\label{ch:hw}
\section{Unchanged from V1}
V2 targets the identical board and component set as V1: Lattice ECP5
\code{LFE5U-45F-8BG381C} ($-8$, CABGA381), ISSI
\code{IS66WVE4M16EBLL-70BLI} PSRAM (64\,Mb, 4M$\times$16), same 16\,MHz
reference oscillator. The real PSRAM controller
(\code{psram\_controller.v}) and its byte$\leftrightarrow$word adapter
(\code{memory\_interface.v}) are reused byte-for-byte, unmodified, from
\code{hardware/v1/} throughout every V2 milestone --- their real,
already-verified electrical/timing requirements and page-mode behavior
are unchanged, because the controller itself was never touched.
\begin{fnnote}[Real ball assignment: defer to V1's own chapter]
V1's own hardware chapter documents a real, \code{iodb.json}-verified,
place\&route-confirmed ball assignment for every PSRAM signal
(\code{psram\_a}, \code{psram\_dq}, \code{psram\_ce\_n/oe\_n/we\_n/
lb\_n/ub\_n/zz\_n}). Since V2's own \code{neural\_multiprocessor.v}
drives these signals through the identical, unmodified controller, that
same real ball assignment applies unchanged if V2 is deployed on the
same physical board --- it is not repeated here to avoid maintaining two
copies of the same real data; see the V1 datasheet directly.
\end{fnnote}
\section{What V2 has not yet placed on real hardware}
As stated in ch.~\ref{ch:host}, V2's own node-registration bus has no
physical pin assignment in this revision --- every V2 characterization
to date used either a Verilator testbench or an unconstrained
(\code{--lpf-allow-unconstrained}) synthesis top-level. A real deployment
would need:
\begin{itemize}
\item A physical host transport for the registration bus (ch.~\ref{ch:host}).
\item A real, constrained \code{nextpnr-ecp5} place\&route run
producing a genuine \code{.lpf}/ball assignment for
\code{neural\_multiprocessor.v}'s own top-level pins, analogous to
V1's own \code{tools/pinout/gen\_lpf.py} flow.
\item Re-verification that the real Fmax numbers in ch.~\ref{ch:impl2}
(obtained unconstrained) hold once real pin locations are fixed ---
pin placement can itself affect routing and therefore Fmax.
\end{itemize}
\section{Power supply, oscillator, configuration}
Unchanged from V1: same board-level power sequencing, same oscillator,
same JTAG/config-SPI boot path (fixed-function dedicated pins, outside
RTL scope). No V2-specific hardware change was made or is required
beyond the (not yet placed) registration-bus transport above.
\section{SDRAM upgrade addendum (2026-09-07) --- current, authoritative
board state}
\label{sec:sdram-addendum}
\begin{fnwarn}[This section supersedes the PSRAM description above for
the current hardware baseline]
The sections above describe an earlier V2 milestone that still reused
V1's own PSRAM chain unconstrained. The project has since made a
closed architectural decision (real \code{decisions.log} DEC-0034) to
replace external memory with a single SDR SDRAM device, and has since
upgraded that device's capacity and re-verified real, constrained
place\&route timing. This section is the current, real, measured state
--- see \code{hardware/v2/docs/MEMORY\_UPGRADE\_64MB\_N8.md} in the
repository for the full investigation.
\end{fnwarn}
\subsection{Memory device}
\textbf{Alliance Memory AS4C32M16SB-7BIN} --- 512\,Mbit (64\,MByte) SDR
SDRAM, organized 4 banks $\times$ 8M words $\times$ 16 bits, 54-ball
FBGA package (8$\times$8$\times$1.2\,mm max), $-40$ to $85^{\circ}$C
industrial, $-7$ speed grade (143\,MHz max). VDD/VDDQ 3.3\,V $\pm$0.3\,V.
Single-ended \code{CLK} --- \textbf{no \code{CLK\_N}}, this is SDR, not
DDR, SDRAM. Real distributor availability confirmed: DigiKey product
11613071, 568 units in stock, \$31.12/unit (qty 1), 16-week
manufacturer lead time.
\subsection{Complete AS4C32M16SB-7BIN ball assignment}
From the manufacturer's own \code{-7BIN}-specific datasheet (Alliance
Memory, Rev.\,1.4, June 2024, Figure~1.1 --- the real TFBGA ball
diagram, not inferred from the TSOP-II \code{-7TIN} pinout).
\begin{fnnote}[Address / Bank]
A0=H7, A1=H8, A2=J8, A3=J7, A4=J3, A5=J2, A6=H3, A7=H2, A8=H1, A9=G3,
A10/AP=H9, A11=G2, A12=G1, BA0=G7, BA1=G8.
\end{fnnote}
\begin{fnnote}[Data / Masks]
DQ0=A8, DQ1=B9, DQ2=B8, DQ3=C9, DQ4=C8, DQ5=D9, DQ6=D8, DQ7=E9, DQ8=E1,
DQ9=D2, DQ10=D1, DQ11=C2, DQ12=C1, DQ13=B2, DQ14=B1, DQ15=A2, LDQM=E8,
UDQM=F1.
\end{fnnote}
\begin{fnnote}[Control / Power]
CLK=F2, CKE=F3, CS\#=G9, RAS\#=F8, CAS\#=F7, WE\#=F9. VDD=\{A9,E7,J9\},
VSS=\{A1,E3,J1\}, VDDQ=\{A7,B3,C7,D3\}, VSSQ=\{A3,B7,C3,D7\}, NC=E2.
\end{fnnote}
\subsection{FPGA $\leftrightarrow$ SDRAM mapping (real, LPF-verified)}
From \code{hardware/v2/constraints/v2\_board\_top.lpf} (45/45 unique
FPGA balls, no duplicates, LFE5U-45F-8BG381 rev.\,3.0 CSV-verified).
\begin{fnnote}[FPGA ball $\to$ SDRAM ball, by signal group]
\code{sdram\_a[0..12]}: D5,D3,F4,E5,E3,F5,A2,B1,C2,C1,D2,D1,F1 $\to$
A0..A12 (H7,H8,J8,J7,J3,J2,H3,H2,H1,G3,H9,G2,G1). \code{sdram\_ba[0:1]}:
E4,C3 $\to$ BA0,BA1 (G7,G8). \code{sdram\_dq[0..15]}:
E1,G5,H3,J5,K3,K2,H1,J1,K1,K4,L4,L5,M5,M4,N4,N5 $\to$ DQ0..DQ15.
\code{sdram\_dqm[0:1]}: P5,N3 $\to$ LDQM,UDQM. Control:
\code{sdram\_cke/cs\_n/ras\_n/cas\_n/we\_n}: B5,C5,C4,A3,B3 $\to$
CKE,CS\#,RAS\#,CAS\#,WE\#.
\end{fnnote}
\subsection{Real, measured clock closure (nextpnr-ecp5, 8 seeds/config)}
\begin{tabularx}{\textwidth}{L{4.0cm} C{1.6cm} C{2.2cm} X}
\toprule
\rowh \thd{Configuration} & \thd{Pass} & \thd{Worst Fmax} & \thd{Notes} \\
\midrule
N\_SLOTS=4 @ 64\,MHz & 8/8 & 66.58\,MHz & Production baseline, GO \\
\rowa N\_SLOTS=8 @ 64\,MHz & 5/8 & 60.12\,MHz & Open, not production-frozen \\
N\_SLOTS=4/8 @ 80\,MHz & 0/8 & --- & NO-GO, genuine \code{ecppll}-regenerated PLL \\
\bottomrule
\end{tabularx}
Real, measured after the ERR-0029 weight-cache hit-index optimization
(serial priority scan $\to$ flat one-hot compare); see
\code{hardware/v2/logs/errors.log} and \code{decisions.log} DEC-0040.
@@ -0,0 +1,77 @@
\chapter{Register-level interface \& internal state encodings}
\label{ch:regs}
\begin{fnwarn}[No SPI register map in this revision]
V1's own quick-reference chapter documents a real SPI opcode/register
map (\code{STATUS}, \code{SET\_BASE}, \code{READ\_CONFIG}, \ldots). V2 has
no equivalent yet (ch.~\ref{ch:host}) --- this chapter instead documents
the \textbf{node-registration field layout} (repeated here for quick
reference) and the \textbf{internal FSM state encodings} exposed by each
module, useful for simulation-level debug and for a future host driver.
\end{fnwarn}
\section{Node registration fields (quick reference)}
See ch.~\ref{ch:host} for the full field-level description.
\code{reg\_node\_id}, \code{reg\_required}, \code{reg\_producer\_ids},
\code{reg\_x\_base}, \code{reg\_w\_base}, \code{reg\_n\_tiles},
\code{reg\_result\_addr} --- valid/ready handshake, \code{reg\_ready}
gated on the target node id's table slot being \code{EMPTY}.
\section{Dependency Manager node state (\texttt{node\_state})}
\begin{tabularx}{\textwidth}{C{1.4cm} L{2.6cm} Y}
\toprule
\rowh \thd{Value} & \thd{Name} & \thd{Meaning} \\
\midrule
\op{2'd0} & \code{ST\_EMPTY} & Table slot free; \code{reg\_ready} asserted for this node id. \\
\rowa \op{2'd1} & \code{ST\_WAITING} & Registered, at least one producer not yet resolved. \\
\op{2'd2} & \code{ST\_READY} & All producers resolved; eligible for dispatch. \\
\rowa \op{2'd3} & \code{ST\_DISPATCHED} & Handed to the Director; \textbf{terminal} (\S\ref{ch:sched}). \\
\bottomrule
\end{tabularx}
\section{Neural Director state (\texttt{dir\_state})}
\begin{tabularx}{\textwidth}{C{1.4cm} L{2.6cm} Y}
\toprule
\rowh \thd{Value} & \thd{Name} & \thd{Meaning} \\
\midrule
\op{4'd0} & \code{DIR\_IDLE} & Reset/startup. \\
\rowa \op{4'd1} & \code{DIR\_SCAN\_READY} & Checking whether a queued job and a free slot both exist. \\
\op{4'd2} & \code{DIR\_ALLOCATE} & Dispatching the head-of-queue job to the first free slot. \\
\rowa \op{4'd3} & \code{DIR\_ERROR} & Recoverable only via reset (an isolated fault never blocks other slots). \\
\bottomrule
\end{tabularx}
\section{Memory Manager state (\texttt{state})}
\begin{tabularx}{\textwidth}{C{1.4cm} L{3.0cm} Y}
\toprule
\rowh \thd{Value} & \thd{Name} & \thd{Meaning} \\
\midrule
\op{3'd0} & \code{MM\_IDLE} & Waiting for \code{job\_start}. \\
\rowa \op{3'd1} & \code{MM\_PREFETCH\_FIRST} & Waiting for tile~0's activation \emph{and} weight halves to both arrive. \\
\op{3'd2} & \code{MM\_STREAM} & Presenting tiles to the Neural Processor, double-buffering the next one. \\
\rowa \op{3'd3} & \code{MM\_WAIT\_RESULT} & Last tile handed off; waiting for the Neural Processor's own result. \\
\op{3'd4} & \code{MM\_WRITE\_RESULT} & Issuing the real PSRAM word write for the INT8 result. \\
\rowa \op{3'd5} & \code{MM\_DONE} & Waiting for the write's own \code{mem\_ready}; then pulses \code{job\_done}. \\
\bottomrule
\end{tabularx}
\section{Neural Processor state (\texttt{np\_state})}
\begin{tabularx}{\textwidth}{C{1.4cm} L{2.8cm} Y}
\toprule
\rowh \thd{Value} & \thd{Name} & \thd{Meaning} \\
\midrule
\op{4'd0} & \code{NP\_IDLE} & No job in flight. \\
\rowa \op{4'd1} & \code{NP\_LOAD\_JOB} & Latching \code{job\_bias}/\code{job\_activation}, clearing the accumulator. \\
\op{4'd2} & \code{NP\_WAIT\_OPERANDS} & Consuming tiles as they arrive (absorbs the per-tile MAC/accumulate/next-tile sequence). \\
\rowa \op{4'd3} & \code{NP\_FINISH} & Draining the pipeline after \code{tile\_last}. \\
\op{4'd4} & \code{NP\_WRITE\_RESULT} & Result available for the Memory Manager to consume. \\
\rowa \op{4'd5} & \code{NP\_DONE} & Job complete. \\
\op{4'd6} & \code{NP\_ERROR} & Reachable only via an unreachable \code{default} case --- isolated per-processor, never blocks other slots. \\
\bottomrule
\end{tabularx}
\section{Slot Memory Arbiter owner encoding}
\code{owner} is \code{0} for ``no port granted'', or (port index $+1$)
for the currently-granted port --- indices \code{0..N\_SLOTS-1} are the
per-slot Memory Managers' own weight/write-back traffic; index
\code{N\_SLOTS} is the shared Activation Cache's own traffic.
@@ -0,0 +1,62 @@
\chapter{Roadmap and development status}
\label{ch:roadmap}
\section{Milestones M1--M10}
\begin{tabularx}{\textwidth}{C{1.0cm} L{3.6cm} C{1.3cm} Y}
\toprule
\rowh \thd{M} & \thd{Title} & \thd{Status} & \thd{Content} \\
\midrule
1 & Neural Processor & \OK & Bit-exact 8-stage pipeline vs.\ V1, 7/7 tests; 183.12~MHz isolated. \\
\rowa 2 & Processor Array & \OK & 1/2/4/8 processors, real concurrent-slot simulation; DSP (not LUT/FF) found to saturate first. \\
3 & Buffers & \OK & \code{activation\_buffer}/\code{weight\_buffer}/\code{result\_buffer}, real \code{DP16KD} inference --- superseded in the real datapath by the Activation Cache (\S\ref{sec:roadmapcache}). \\
\rowa 4 & Memory Manager & \OK & Double-buffered prefetch, real V1 PSRAM chain, 3 real RTL bugs found/fixed. \\
5 & Neural Director & \OK & First-free dispatch, real backpressure, 4/4 tests. \\
\rowa 6 & Dependency Manager & \OK & Multi-dependency/shared-producer wake-up, 4/4 tests. \\
7 & Dataflow Core & \OK & Full M1--M6 integration, wake-up loop closed end-to-end. \\
\rowa 8 & PSRAM integration & \OK & Real, shared PSRAM across concurrent slots; 1 real arbiter bug found/fixed (dropped request under contention). \\
9 & Full benchmark & \OK & V1 vs.\ V2 comparison, every number classified. \\
\rowa 10 & Optimization & \OK & N\_SLOTS ceiling (DSP), ACC\_WIDTH 6-seed sweep, real stall/utilization instrumentation. \\
\bottomrule
\end{tabularx}
\section{Post-campaign: user-requested optimizations}
\label{sec:roadmapcache}
Following M9/M10's own final benchmark campaign
(\code{hardware/v2/docs/benchmarks/final-benchmark.md}), two concrete
optimizations were implemented and measured against the real toolchain:
\begin{enumerate}
\item \textbf{Word-level burst reads} (ch.~\ref{ch:mem}, \S\ref{sec:burst}):
real 2.24--2.37$\times$ wall-clock speedup, negligible Fmax cost.
\item \textbf{Shared activation cache} (ch.~\ref{ch:mem},
\S\ref{sec:cache}): a further real 1.66--2.00$\times$ cycle
reduction, at a real, steep Fmax cost that makes
\code{N\_SLOTS}=4 fail 80\,MHz outright.
\end{enumerate}
Combined: \textbf{2.45$\times$} real wall-clock speedup at
\code{N\_SLOTS}=2 (recommended) over the pre-optimization baseline, which
was itself already 2.6$\times$ faster than V1.
\section{Open work items (real, not hidden)}
\begin{tabularx}{\textwidth}{L{3.6cm} Y}
\toprule
\rowh \thd{Item} & \thd{Why it is open} \\
\midrule
Activation cache pipelining & The concrete fix for \code{N\_SLOTS}=4's Fmax failure: register the hit-detection/broadcast logic to break its single-cycle combinational path. Not attempted this round --- \code{N\_SLOTS}=4 delivers no real throughput benefit anyway (memory-bound), so this protects \code{N\_SLOTS}=2's own margin rather than making 4 useful. \\
\rowa Dependency Manager node-slot reclamation & \code{ST\_DISPATCHED} is terminal; a real long-running system will eventually exhaust \code{N\_NODES}. \\
Scheduler fairness & Fixed lowest-index priority shows real, measured per-slot imbalance under sustained contention (ch.~\ref{ch:sched}); no fairness-aware alternative has been measured yet. \\
\rowa Second physical PSRAM bank & The only real way to raise the memory-bandwidth ceiling itself, rather than use existing bandwidth more efficiently --- a board-level change, not attempted this round. \\
Real host driver \& pinout & No physical transport or placed pin assignment exists for the node-registration bus (ch.~\ref{ch:host}, ch.~\ref{ch:hw}). \\
\rowa Per-node bias/activation & Every job currently hardcodes \code{bias=0}/\code{ACT\_RELU}; not yet exposed by the Dependency Manager's own job descriptor. \\
Power/energy characterization & No ECP5 power estimator available in this toolchain; honestly reported as \textsc{Not measured}, not invented. \\
\bottomrule
\end{tabularx}
\begin{fnnote}[Every claim in this datasheet traces to a log entry]
\code{hardware/v2/logs/}: \code{development.log}, \code{simulation.log},
\code{synthesis.log}, \code{timing.log}, \code{benchmark.log},
\code{decisions.log} (\code{DEC-\textit{NNNN}}), \code{experiments.log}
(\code{EXP-\textit{NNNN}}), \code{errors.log} (\code{ERR-\textit{NNNN}}).
IDs are never reused, past results are never overwritten, even failed
ones --- the same discipline V1's own \code{docs/validation/} campaign
followed.
\end{fnnote}
@@ -0,0 +1,261 @@
\chapter{The Neural Memory System (NMS)}
\label{ch:nms}
\begin{fnnote}[Scope of this chapter]
Chapters~\ref{ch:arch}--\ref{ch:impl2} document \textbf{Current V2}
(\code{memory\_manager.v} + \code{activation\_cache.v}, DEC-0015/
DEC-0016) as a complete, frozen, real-measured system in its own
right. This chapter documents a \textbf{parallel, later evolution} ---
the Neural Memory System (NMS) --- built to directly address Current
V2's own central finding (\S\ref{sec:cache}'s own honest warning:
real parallel scaling flat beyond \code{N\_SLOTS}=2, a single shared
PSRAM port saturating regardless of on-chip organization). Both
systems are real, both are independently synthesizable and
simulatable, and both remain available: \textbf{Current V2 is not
being retired by this chapter} --- \S\ref{sec:nmscompare}'s own real
data shows the choice between them is configuration-dependent, not a
strict win for either.
\end{fnnote}
\section{Design goal}
Current V2's own memory path is fundamentally an on-demand,
per-request architecture: every tile fetch is a fresh transaction,
arbitrated one at a time onto the shared PSRAM port, with the
activation cache's own single shared instance introducing exactly the
kind of centralized combinational hit-check that \S\ref{sec:cache}
already flagged as a real Fmax risk at higher \code{N\_SLOTS}. The
NMS instead asks: \emph{what is the minimum on-chip organization that
lets the Neural Processor array run at close to its own compute rate,
treating PSRAM purely as backing storage?} Following the project's own
established discipline, this was answered with real, measured data at
every step (a real bandwidth-requirement study, a real bank-contention
sweep, real candidate synthesis) rather than assumed.
\section{STEP1 --- real bandwidth requirement study}
\label{sec:nmsstep1}
An idealized backing-store model (runtime-configurable latency and
bandwidth, simulation-only, never synthesized) drove the real,
unmodified \code{neural\_processor.v} directly, sweeping \code{N\_SLOTS}
$\times$ \code{PREFETCH\_DEPTH} $\times$ latency $\times$ bandwidth (768
real Verilator data points). Three real bugs in the study harness
itself were found and fixed first (a registered-grant race, a
single-transfer-at-a-time serialization cap, and a stale-value
issuance throttle) before any result was trusted.
\begin{fnnote}[Real result: a hard, linear bandwidth floor]
Minimum aggregate bandwidth for $\ge$90/95/99\% of compute-only
throughput scales \textbf{exactly linearly} with \code{N\_SLOTS} at
\textbf{16~bytes/cycle/slot} ($=2\times$\code{P\_IN}, the raw
activation+weight demand of one \code{neural\_processor.v} at its own
maximum pipelined rate) --- a hard floor, not a design margin.
\code{PREFETCH\_DEPTH} (tiles of lookahead) needed to actually reach
that floor scales with round-trip latency, independent of bandwidth:
$\approx$4 tiles hides 0--1~cycle latency; $\approx$16 tiles is
\emph{not yet enough} to hide 16~cycles (83.4\% measured, not 90\%+).
\end{fnnote}
\section{STEP2 --- closed-form traffic model}
Per slot at steady state: \textbf{weight} traffic is always
\code{P\_IN}=8~B/cycle (never shared, no amortization possible ever);
\textbf{activation} traffic is 8~B/cycle worst case (no sharing) down
to $\approx$0 amortized (full sharing across a layer); \textbf{result}
traffic is negligible ($1/n\_tiles$~B/cycle/slot). The 16~B/cycle/slot
worst-case floor measured in STEP1 is exactly $8+8$ --- a clean
cross-validation of the simulated result against the analytical model,
not a coincidence.
\section{STEP3 --- real bank-contention sweep}
\label{sec:nmsstep3}
A second simulation harness measured whether banking the shared
Activation SRAM (broadcast-on-same-address, round-robin arbitration on
conflict) actually lets \code{N\_SLOTS} scale under a \emph{realistic}
dispatch stagger (the Neural Director dispatches one job at a time,
never simultaneously) --- the exact mechanism behind Current V2's own
flat-scaling finding. Two real bugs (fixed-priority starvation causing
an actual simulation hang; a testbench/DUT handshake mismatch) were
found and fixed first.
\begin{fnnote}[Real result: banking recovers real parallel scaling]
With \code{N\_BANKS}=\code{N\_SLOTS}, aggregate throughput scales
\textbf{near-linearly} regardless of dispatch stagger (0--8 cycles
tested): \code{N\_SLOTS}=1\,$\to$\,0.990, 2\,$\to$\,1.979 (1.999$\times$),
4\,$\to$\,3.950 (3.990$\times$), 8\,$\to$\,7.869 (7.949$\times$)
tiles/cycle. With \code{N\_BANKS}=1 (matching Current V2's own single
shared port), utilization collapses under any nonzero stagger exactly
as Current V2's own real benchmark showed (e.g.\ \code{N\_SLOTS}=2,
stagger=1: 49.8\%) --- the first real, simulated confirmation in this
project that \code{N=2>N=1} and \code{N=4>N=2} are achievable without
the shared memory nullifying parallelism.
\end{fnnote}
\section{STEP4--7 --- real candidate synthesis and selection}
Two real, synthesizable candidates were built and bit-exact verified
for \emph{each} SRAM, then compared on real Yosys+nextpnr-ecp5 data
(never chosen a priori):
\textbf{Activation SRAM.} Candidate~A (\code{N\_SLOTS} private
replicated copies, broadcast-write fill) vs.\ Candidate~B (banked +
round-robin arbiter + 2-stage registered crossbar, deliberately
pipelined per \S\ref{sec:cache}'s own Fmax lesson). Candidate~A won
decisively: 2--4$\times$ higher real Fmax and $\approx$24$\times$
fewer LUTs than Candidate~B at \code{N\_SLOTS}=8 (\code{MAX\_TILES}=16),
for a real BRAM cost that stays cheap even at a much deeper, more
realistic vector length (8~DP16KD, 7\% of the chip, at
\code{MAX\_TILES}=256/\code{N\_SLOTS}=8) --- confirming the M3-era
warning against assuming ``shallower depth $=$ less BRAM'': at
\code{MAX\_TILES}=16 \emph{neither} candidate used any real BRAM at
all (Yosys chose distributed LUT-RAM for both).
\textbf{Weight SRAM.} Candidate~W1 (one native-width memory per slot,
mirroring \code{weight\_buffer.v}'s own M3-era structure) vs.\
Candidate~W2 (per-MAC-lane packed narrow memories). At
\code{MAX\_TILES}=256 both use \emph{identical} real DP16KD count
(one full block's own native 16\,Kbit capacity per slot, either way),
but packed uses $\approx$2$\times$ fewer LUTs/FFs at \code{N\_SLOTS}=8
for the same BRAM cost --- the wide single memory's own byte-lane
write-enable decode logic is exactly what per-lane packing avoids by
construction.
\textbf{Selected}: replicated Activation SRAM + packed Weight SRAM.
Combined real cost at \code{N\_SLOTS}=8/\code{MAX\_TILES}=256: 16
DP16KD (14.8\% of the LFE5U-45F's 108 total) --- an honestly affordable
real price for this project's own realistic workload sizes.
\section{STEP8 --- full integration}
\code{nms\_dataflow\_core.v} mirrors \code{dataflow\_core.v}'s own
scope exactly: the Dependency Manager and Neural Director are
\textbf{reused verbatim}, unmodified --- only the memory cluster
changed. Each slot's own \code{nms\_memory\_manager.v} is structurally
simpler than \code{memory\_manager.v}: since the on-chip SRAMs now hold
the \emph{entire} vector (not just 2 double-buffered banks), there is
no more bank-swap logic --- a slot simply reads sequentially once its
own weight-fetch progress and the shared activation controller's own
resident count both exceed the tile index it needs.
\begin{fnwarn}[Four real bugs found at full integration scale]
All four are the same root cause: a counter that must represent the
\emph{value} \code{MAX\_TILES} itself (e.g.\ a 16-tile job with
\code{MAX\_TILES}=16) needs one more bit than an address field
indexing \code{0..MAX\_TILES$-$1} --- easy to miss because every test
smaller than \code{MAX\_TILES} passes regardless. Found only once a
real \code{n\_tiles}=\code{MAX\_TILES} job (this project's own
realistic 16-tile neurons) was actually run: a truncated 16-bit
compare that read 16 as 0 (hanging weight fetch entirely); an
undersized counter wrapping 15$\to$0 instead of reaching 16 (an
infinite re-fetch loop); a logic error comparing the wrong two signals
introduced while fixing the first bug (deadlocking exactly the last
tile of every job); and a top-level connecting wire left at the
narrower width after both endpoint modules were widened (silently
truncating the real value 16 back to 0 one wire short of the fix).
Each was isolated via real cycle-by-cycle signal tracing, the same
discipline used throughout this project.
\end{fnwarn}
7/7 bit-exact tests pass at \code{N\_SLOTS}=2, including the exact
scenario STEP3 modeled (two slots dispatched together on the identical
\code{x\_base}, different never-shared weights) and a new
multi-tile test that specifically catches bug class 2 above.
\section{STEP9--10 --- real end-to-end benchmark vs.\ Current V2}
\label{sec:nmscompare}
\code{nms\_neural\_multiprocessor.v} mirrors
\code{neural\_multiprocessor.v}'s own real hardware-facing scope
exactly (same real \code{slot\_mem\_arbiter.v}, same real,
unmodified V1 PSRAM chain). The \textbf{identical} D-Stress workload
(256 neurons, 16~inputs$\times$8 tiles, one shared input vector) used
for every Current-V2 number in this datasheet was run through it,
bit-exact against the same golden model.
\begin{fnnote}[Real, direct comparison --- same workload, same toolchain]
\begin{tabularx}{\textwidth}{L{3.6cm} C{2.8cm} C{2.8cm} C{1.6cm}}
\toprule
\rowh \thd{Metric (\code{N\_SLOTS}=2)} & \thd{Current V2} & \thd{NMS} & \thd{$\Delta$} \\
\midrule
Fmax (real P\&R) & 87.72~MHz & \textbf{93.10~MHz} & $+$6.1\% \\
\rowa LUT4 & 4359 & \textbf{1948} & $-$55.3\% \\
CCU2C & 366 & 266 & $-$27.3\% \\
\rowa TRELLIS\_FF & 3924 & 3522 & $-$10.2\% \\
DSP / BRAM & 16 / 0 & 16 / 0 & $=$ \\
\rowa D-Stress cycles & 185428 & 185645 & $+$0.1\% \\
D-Stress wall-clock & 2113.9~$\mu$s & \textbf{1994.0~$\mu$s} & \textbf{$+$6.0\% faster} \\
\rowa Effective MAC/s & 15.50~M & \textbf{16.43~M} & $+$6.0\% \\
\bottomrule
\end{tabularx}
\begin{tabularx}{\textwidth}{L{3.6cm} C{2.8cm} C{2.8cm} C{1.6cm}}
\toprule
\rowh \thd{Metric (\code{N\_SLOTS}=4)} & \thd{Current V2} & \thd{NMS} & \thd{$\Delta$} \\
\midrule
Fmax (real P\&R) & 65.01~MHz (\FAIL) & 56.62~MHz (\FAIL) & $-$12.9pp \\
\rowa D-Stress cycles & 184795 & 184764 & $-$0.02\% \\
D-Stress wall-clock & 2842.6~$\mu$s & \textbf{3263.2~$\mu$s} & $-$12.9\% (NMS slower) \\
\bottomrule
\end{tabularx}
\end{fnnote}
Cycles are essentially flat between \code{N\_SLOTS}=2 and 4 for
\emph{both} systems (185645$\to$184764 for NMS, $-$0.5\%) ---
confirming STEP1's own analytical floor: a single real PSRAM port caps
\emph{aggregate} throughput regardless of on-chip organization; NMS's
banking work makes the on-chip side efficient, it cannot and does not
remove the external bandwidth ceiling.
\begin{fnwarn}[Real critical path found at N\_SLOTS=4/8 --- not hidden]
Real nextpnr-ecp5 critical-path tracing at \code{N\_SLOTS}=4 shows the
worst path running through
\code{nms\_activation\_fill\_ctrl.v}'s own combinational
priority-scan/address logic (6.26\,ns logic $+$ 11.40\,ns routing) ---
the \emph{same class} of unpipelined, \code{N\_SLOTS}-scaling
combinational cost \S\ref{sec:cache} already documented for
\code{activation\_cache.v}, reintroduced here in the module that
decides \emph{which} shared tag to chase (a genuinely different piece
from the replicated SRAM itself, which has no such problem in
isolation). \code{N\_SLOTS}$\le$2 is unaffected and real, measured
faster; \code{N\_SLOTS}$\ge$4 is a real, open regression, not
recommended, until this scan is pipelined (\S\ref{sec:nmsfuture}).
\end{fnwarn}
\section{Real per-metric detail, N\_SLOTS=2 (D-Stress)}
\begin{tabularx}{\textwidth}{L{4.4cm} C{2.4cm} Y}
\toprule
\rowh \thd{Metric} & \thd{Value} & \thd{Note} \\
\midrule
Processor utilization & 1.10\% & tiles(4096)/(2$\times$185645 cycles) --- consistent with the project's own 1:170--1:220 compute-to-memory-wait finding \\
\rowa Memory (PSRAM port) utilization & 90.4\% & 167830/185645 busy cycles \\
Memory stall (per slot) & 93.6\% & 92.5\% waiting on weight $+$ 1.1\% waiting on activation, measured directly \\
\rowa Compute stall & $\equiv$ memory stall & the Neural Processor stalls \emph{only} on a missing operand in this design --- no separate compute-only stall source exists \\
Weight-buffer hit rate & 0\% & confirmed empirically (2048 real fetches $=$ 2048 tiles/slot, zero reuse) --- weights are never shared, by design \\
\rowa Activation-buffer hit rate & 99.61\% & only 16 real PSRAM fetches for 4096 tile-consumptions (256 neurons share one vector) \\
Prefetch effectiveness & low ($\approx$0\%) & a real, honest gap: this revision fetches weight ``as fast as possible'' but with no bounded lookahead buffer (\code{PREFETCH\_DISTANCE}), so weight-fetch latency dominates stall almost entirely --- see \S\ref{sec:nmsfuture} \\
\rowa Parallel efficiency (N=2 vs.\ N=1) & 48.1\% & real speedup $=$ cycles(1)/cycles(2) $=$ 178432/185645 $=$ 0.961$\times$ (N=2 needs \emph{more} cycles than N=1) --- the shared PSRAM port is still the bottleneck \\
\bottomrule
\end{tabularx}
\section{Recommendation}
Adopt NMS at \code{N\_SLOTS}$\le$2 as a real, measured upgrade over
Current V2 at its own already-recommended default: faster, smaller,
higher Fmax margin, bit-exact, same workload. Do \textbf{not} adopt
NMS at \code{N\_SLOTS}=4/8 yet --- Current V2 is really faster there
until the fill-controller pipelining fix below is implemented and
re-measured. Both systems remain in the repository; selecting between
them is a real, configuration-dependent decision, not a blanket
replacement.
\section{Open work (real, not hidden)}
\label{sec:nmsfuture}
\begin{itemize}
\item \textbf{Pipeline \code{nms\_activation\_fill\_ctrl.v}'s own
priority-scan/address logic} --- the concrete, identified fix
for the \code{N\_SLOTS}=4/8 Fmax regression above.
\item \textbf{Implement real bounded-lookahead weight prefetch}
(\code{PREFETCH\_DISTANCE}, per STEP1's own findings) --- the
current single-shot ``fetch as fast as possible'' weight path is
why prefetch effectiveness measures low; STEP1's own data shows
a real, achievable fix (depth scaled to real round-trip latency).
\item Re-measure \code{N\_SLOTS}=1 and 8 D-Stress cycle counts for
full parity with Current V2's own 4-point table (only 2 and 4
measured this round, time-bounded).
\item A fixed, smaller-\code{N\_BANKS} Activation SRAM variant was
never revisited after full replication was selected --- BRAM
cost was cheap enough at this project's real workload sizes that
it was never worth reconsidering.
\end{itemize}
@@ -0,0 +1,65 @@
\chapter{Module and file map}
\label{ap:modules}
\section{V2 RTL (\texttt{hardware/v2/rtl/})}
\begin{tabularx}{\textwidth}{L{4.2cm} Y}
\toprule
\rowh \thd{File} & \thd{Role} \\
\midrule
\code{neural\_processor.v} & 8-stage INT8 pipeline (M1); bit-exact vs.\ V1. \\
\rowa \code{neural\_processor\_array.v} & N-processor array used for the M2 concurrency sweep. \\
\code{activation\_buffer.v}, \code{weight\_buffer.v}, \code{result\_buffer.v} & M3 BRAM-backed buffers; superseded in the real datapath by \code{activation\_cache.v}. \\
\rowa \code{prefetch\_engine.v} & Weight-only, word-level burst fetch engine (M4, rewritten DEC-0015/DEC-0016). \\
\code{memory\_manager.v} & Double-buffered per-slot tile manager; coordinates the Activation Cache (X) and \code{prefetch\_engine.v} (W). \\
\rowa \code{neural\_director.v} & First-free job dispatch (M5). \\
\code{dependency\_manager.v} & Node table, dependency counting, wake-up (M6). \\
\rowa \code{dataflow\_core.v} & Full M1--M6 integration $+$ Activation Cache (M7, extended DEC-0016). \\
\code{slot\_mem\_arbiter.v} & Generic N-port arbiter to the real PSRAM chain (M8). \\
\rowa \code{activation\_cache.v} & Shared, single-tag activation cache (post-M10, DEC-0016). \\
\code{neural\_multiprocessor.v} & Real hardware-facing top level (M8). \\
\bottomrule
\end{tabularx}
\section{NMS RTL (\texttt{hardware/v2/nms/rtl/}, ch.~\ref{ch:nms})}
\begin{tabularx}{\textwidth}{L{4.6cm} Y}
\toprule
\rowh \thd{File} & \thd{Role} \\
\midrule
\code{nms\_activation\_replicated.v} & Selected Activation SRAM: \code{N\_SLOTS} private full-vector copies, broadcast-write fill (DEC-0019). \\
\rowa \code{nms\_activation\_fill\_ctrl.v} & Shared dedup/fetch controller backing it -- the real \code{N\_SLOTS}=4/8 Fmax bottleneck identified in ch.~\ref{ch:nms}. \\
\code{nms\_weight\_packed.v} & Selected Weight SRAM: per-MAC-lane packed private copies (DEC-0020). \\
\rowa \code{nms\_memory\_manager.v} & Per-slot job FSM, drop-in replacement for \code{memory\_manager.v}'s own external interface. \\
\code{nms\_dataflow\_core.v} & Full NMS integration, mirrors \code{dataflow\_core.v}'s own scope (STEP8). \\
\rowa \code{nms\_neural\_multiprocessor.v} & Real hardware-facing top level, mirrors \code{neural\_multiprocessor.v}'s own scope (STEP9). \\
\bottomrule
\end{tabularx}
Also reused verbatim, unmodified, in the NMS datapath:
\code{neural\_processor.v}, \code{prefetch\_engine.v} (as a generic
P\_IN-byte-tile fetch engine, not weight-specific despite its name),
\code{dependency\_manager.v}, \code{neural\_director.v},
\code{slot\_mem\_arbiter.v}.
\section{Reused, unmodified V1 (\texttt{hardware/v1/rtl/})}
\begin{tabularx}{\textwidth}{L{4.2cm} Y}
\toprule
\rowh \thd{File} & \thd{Role in V2} \\
\midrule
\code{memory\_interface.v} & Word-level (16-bit) PSRAM backend port, now the direct target of both \code{prefetch\_engine.v} and \code{activation\_cache.v}. \\
\rowa \code{psram\_controller.v} & Real PSRAM controller, page-mode support exploited more effectively by the word-burst rewrite. \\
\code{int8\_memory\_access.v} & \textbf{No longer instantiated} in V2's datapath post-DEC-0015 --- file itself untouched. \\
\bottomrule
\end{tabularx}
\section{Simulation (\texttt{hardware/v2/sim/})}
\code{tb\_neural\_processor.v}, \code{tb\_dataflow\_core.v},
\code{tb\_memory\_manager.v}, \code{tb\_neural\_director.v},
\code{tb\_dependency\_manager.v}, \code{tb\_neural\_multiprocessor.v},
\code{tb\_benchmark\_suite.v} (the final campaign's own testbench,
parametric in \code{N\_SLOTS\_CFG} via Verilator's own \code{-G} override).
\section{Documentation and logs (\texttt{hardware/v2/docs/}, \texttt{hardware/v2/logs/})}
\code{ROADMAP.md}; \code{docs/benchmarks/final-benchmark.md} (the
21-section pre-optimization campaign report); append-only logs
(\code{development}, \code{simulation}, \code{synthesis}, \code{timing},
\code{benchmark}, \code{decisions}, \code{experiments}, \code{errors})
--- the primary source of every number in this datasheet.