\chapter{Datapath di calcolo} \label{ch:datapath} \section{Catena aritmetica INT8/INT32} Il datapath elementare implementa la sequenza tipica di un neurone quantizzato: \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\\prodotto}; \node{sign-ext\\INT32}; \node[fnblockD]{accumulo\\INT32}; \node{$+$ bias}; \node[fnblockA]{attivazione}; \node[fnblockT]{sat. INT8}; \foreach \i [count=\j from 2] in {1,...,6} \draw[fnarrow] (chain-\i) -- (chain-\j); \end{tikzpicture} \end{center} Ogni prodotto INT8$\times$INT8 sta in 16~bit; viene esteso con segno a 32~bit prima dell'accumulo, così l'accumulatore non trabocca su vettori lunghi. Bias e attivazione operano a 32~bit; solo l'uscita finale viene saturata a INT8. \section{\texttt{mac\_unit} --- moltiplicatore-accumulatore} Il modulo \code{mac\_unit} è puramente combinatorio e parametrico su \code{DATA\_WIDTH} e \code{ACC\_WIDTH}. Calcola: \[ \mathrm{acc\_out} = \mathrm{acc\_in} + \mathrm{signext}_{ACC}(x \cdot w) \] Il prodotto ha larghezza $2\times$\code{DATA\_WIDTH} e viene esteso con segno replicando il bit più significativo. Su ECP5 la moltiplicazione mappa su un blocco DSP \code{MULT18X18D}. \begin{lstlisting}[caption={\texttt{rtl/mac\_unit.v} --- nucleo aritmetico},label={lst:macunit}] localparam PROD_WIDTH = 2 * DATA_WIDTH; wire signed [PROD_WIDTH-1:0] product = x * w; wire signed [ACC_WIDTH-1:0] product_ext = {{(ACC_WIDTH-PROD_WIDTH){product[PROD_WIDTH-1]}}, product}; assign acc_out = acc_in + product_ext; \end{lstlisting} \section{\texttt{mac8} --- MAC parallelo e balanced adder tree} \code{mac8} istanzia \code{PARALLEL} unità \code{mac\_unit} che generano \code{PARALLEL} prodotti indipendenti, poi li somma con un \emph{albero di addizione binario bilanciato}. Rispetto alla riduzione lineare $((((p_0{+}p_1){+}p_2){+}p_3){+}\dots)$, di profondità $O(\text{PARALLEL})$, l'albero ha profondità $O(\log_2 \text{PARALLEL})$, riducendo drasticamente il percorso combinatorio. \begin{center} \begin{tikzpicture}[font=\scriptsize,level distance=11mm, every node/.style={fnreg,minimum width=8mm}, level 1/.style={sibling distance=30mm}, level 2/.style={sibling distance=15mm}, level 3/.style={sibling distance=8mm}, edge from parent/.style={fnarrowT,draw}] \node[fnblockD]{sum} child {node[fnblockT]{$+$} child {node[fnblockT]{$+$} child {node{$p_0$}} child {node{$p_1$}}} child {node[fnblockT]{$+$} child {node{$p_2$}} child {node{$p_3$}}}} child {node[fnblockT]{$+$} child {node[fnblockT]{$+$} child {node{$p_4$}} child {node{$p_5$}}} child {node[fnblockT]{$+$} child {node{$p_6$}} child {node{$p_7$}}}}; \end{tikzpicture} \end{center} \begin{center}\footnotesize\itshape\color{fnGrey} Esempio con PARALLEL=8: 3 livelli. PARALLEL=16 $\to$ 4 livelli; PARALLEL=32 $\to$ 5 livelli.\end{center} \begin{fnnote}[PARALLEL potenza di due] L'albero è pensato per \code{PARALLEL} potenza di due (8, 16, 32\ldots). Questo è anche il valore usato in tutte le configurazioni del progetto. \end{fnnote} \section{\texttt{neuron\_parallel} --- FSM del neurone} \code{neuron\_parallel} elabora \code{N\_INPUTS} ingressi in gruppi di \code{PARALLEL}, mantenendo l'accumulatore tra un gruppo e il successivo. Alla fine somma il bias, applica l'attivazione e satura a INT8. Il numero di gruppi è $\text{GROUPS}=\text{N\_INPUTS}/\text{PARALLEL}$. \begin{center} \begin{tikzpicture}[font=\scriptsize,node distance=4mm,start chain=going below, every node/.style={on chain,fnblock,minimum width=46mm}] \node[fnblockA]{\code{start}}; \node{gruppo 0 $\to$ accumulo}; \node{gruppo 1 $\to$ accumulo}; \node[draw=none,fill=none]{\vdots}; \node{gruppo GROUPS$-$1 $\to$ accumulo}; \node{$+$ bias}; \node[fnblockA]{attivazione (ACT\_RELU / ACT\_NONE)}; \node[fnblockT]{saturazione INT8}; \node[fnblockD]{\code{done}, \code{y}}; \foreach \i [count=\j from 2] in {1,...,8} \draw[fnarrow] (chain-\i) -- (chain-\j); \end{tikzpicture} \end{center} \subsection{Guard di parametri (elaboration-time)} Se \code{PARALLEL} non divide esattamente \code{N\_INPUTS} si verificano due guasti, entrambi confermati empiricamente in \code{sim/parameter\_sweep\_tb.v}: \begin{itemize} \item la divisione intera tronca \code{GROUPS} e gli ingressi in eccesso non vengono mai letti $\to$ risultato \textbf{errato}, senza errore né avviso; \item se \code{PARALLEL > N\_INPUTS}, \code{GROUPS=0} e la condizione terminale non è mai soddisfatta $\to$ il neurone \textbf{si blocca} (busy alto, done mai asserito). \end{itemize} La soluzione non modifica il datapath validato: un blocco \code{generate} istanzia un modulo deliberatamente indefinito quando $\text{N\_INPUTS} \bmod \text{PARALLEL}\neq0$, forzando un errore in \emph{elaborazione} sia in simulazione sia in sintesi. Per le configurazioni valide il ramo non viene mai elaborato. \begin{lstlisting}[caption={\texttt{rtl/neuron\_parallel.v} --- guard di parametri}] generate if (N_INPUTS == 0 || N_INPUTS % PARALLEL != 0) begin : PARAMETER_ERROR neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL invalid_parameter_combination(); end endgenerate \end{lstlisting} \begin{fnnote}[Caso limite \texttt{N\_INPUTS=0} (corretto 2026-09-04)] La condizione originale (\code{N\_INPUTS \% PARALLEL != 0}) non intercetta \code{N\_INPUTS=0}, poiché $0 \bmod \text{PARALLEL}=0$ per ogni \code{PARALLEL}: il modulo elaborava con successo (sia in simulazione sia in sintesi reale Yosys) lasciando \code{x\_bus}/\code{w\_bus} non pilotati e \code{start} silenziosamente inefficace. Trovato durante la campagna di ri-certificazione (\code{docs/validation/bugs.md}, BUG-002) e corretto estendendo il guard come sopra --- \code{N\_INPUTS=0} ora fallisce l'elaborazione esattamente come gli altri casi degeneri. \end{fnnote} \section{Funzioni di attivazione} \code{neuron\_parallel} accetta una porta \code{activation} a 2~bit. Il default è \code{ACT\_RELU}, l'unico comportamento esistente prima dell'introduzione della porta, così ogni chiamante preesistente resta invariato. \begin{tabularx}{\textwidth}{L{2.6cm} C{1.4cm} Y} \toprule \rowh \thd{Codifica} & \thd{Valore} & \thd{Comportamento} \\ \midrule \code{ACT\_NONE} & \code{2'd0} & Lineare: nessun clamp a zero, saturazione bilaterale al range INT8 $[-128,+127]$. \\ \rowa \code{ACT\_RELU} & \code{2'd1} & $\max(0,x)$, poi saturazione positiva a $+127$ (default; fallback anche per codifiche riservate). \\ \bottomrule \end{tabularx} \section{Saturazione INT8} Dopo bias e attivazione, l'accumulatore a 32~bit viene ridotto a INT8: \[ y=\begin{cases} +127 & \text{se } \mathrm{final\_acc} > 127\\ -128 & \text{se } \mathrm{final\_acc} < -128 \ \text{(solo ACT\_NONE)}\\ 0 & \text{se } \mathrm{final\_acc}\le 0 \ \text{(solo ACT\_RELU)}\\ \mathrm{final\_acc}[7:0] & \text{altrimenti} \end{cases} \] \section{\texttt{layer} --- neuroni in parallelo} \code{layer} istanzia \code{N\_NEURONS} neuroni che condividono il vettore di ingresso \code{x\_bus} ma hanno pesi e bias distinti; \code{busy} è l'OR e \code{done} l'AND dei segnali dei neuroni. È il modulo usato nei benchmark del datapath (cap.~\ref{ch:impl}), dove tutti i neuroni lavorano simultaneamente. La convenzione di indirizzamento è neuron-major: i pesi del neurone $n$ occupano \code{weights\_bus[n*N\_INPUTS*DATA\_WIDTH +: N\_INPUTS*DATA\_WIDTH]}.