\chapter[Rete a grafo (Tipo \#2)]{Configurazione a due livelli: rete a grafo (Tipo \#2)} \label{ch:grafo} \section{Due tipi di rete} L'engine espone due \emph{tipi di rete} selezionabili dall'host, con lo stesso comando di avvio che instrada verso il motore corretto: \begin{itemize} \item \textbf{Tipo \#1 --- rete classica (dense).} Layer con neuroni per layer, fully connected tra layer consecutivi. È il percorso di \code{layer\_sequencer} (cap.~\ref{ch:seq}), avviato da \op{RUN\_NETWORK}. Connessioni \emph{implicite per posizione}: non si enumera nulla, si definiscono solo i pesi indirizzati come \code{w\_base + k*n\_inputs + j}. \item \textbf{Tipo \#2 --- grafo arbitrario (sparse).} A partire dagli id dei neuroni di ingresso si definiscono le connessioni di ogni neurone fino all'uscita, tramite una \emph{edge-list sparsa} per-neurone. Connessioni \emph{esplicite per enumerazione}: ogni connessione è un edge \code{(src\_id, peso)}; se non è nella lista, non esiste. \end{itemize} \begin{fnnote}[La differenza in una riga] Dense: definisci i \emph{pesi} per posizione in una matrice. Graph: definisci ogni \emph{connessione} come edge \code{(src\_id, peso)} in una lista per-neurone. Le due tabelle descrittori hanno lo stesso formato di 11~byte ma campi diversi; il registro \code{net\_type} dice al motore quale interpretazione usare. \end{fnnote} \section{Buffer di attivazione globale} Il Tipo \#2 introduce un \textbf{buffer di attivazione} indicizzato per \emph{id di segnale}, un byte INT8 per id, realizzato in \textbf{block RAM on-chip \code{DP16KD}} (\code{rtl/act\_buffer.v}). Gli id \code{0..N\_in-1} sono gli ingressi; ogni neurone scrive la propria uscita nel proprio id. Il gather delle sorgenti legge da qui a \emph{accesso random a un ciclo}: è ciò che rende economico il grafo, perché è l'accesso che la PSRAM (70~ns, sequenziale) non potrebbe accelerare. \begin{fnspec}[Dimensionamento V1] \code{N\_TOTAL}=4096 segnali, id a 16~bit (spazio fino a 65.536 senza cambiare formato). Buffer = 4~KB, cioè 2 blocchi \code{DP16KD} su 108. Il vincolo reale diventa la capacità PSRAM per gli edge ($\approx$2\,M edge a 4~B), non la block RAM. \end{fnspec} \section{DAG feed-forward e vincolo \texttt{src\_id < out\_id}} Il grafo è un DAG feed-forward: ogni connessione punta a un id \textbf{già calcolato} (\code{src\_id < out\_id}). I neuroni si elaborano in ordine di id crescente, così quando si calcola un neurone tutte le sue sorgenti sono pronte nel buffer. Cicli e ricorrenza sono fuori scope per la V1. Il vincolo è verificato a due livelli: dall'assemblatore host (a compile time) e da un guard a runtime in \code{graph\_engine} (\code{STATUS.err}), nella stessa filosofia del guard di elaborazione su \code{N\_INPUTS \% PARALLEL}. \section{Formati dati} Entrambi i descrittori sono da 11~byte/voce, MSB-first, a \code{table\_base}. \subsection{Descrittore Tipo \#2 (grafo)} \begin{tabularx}{\textwidth}{L{3.4cm} C{1.6cm} Y} \toprule \rowh \thd{Campo} & \thd{Byte} & \thd{Significato} \\ \midrule \code{conn\_ptr} & 3 & Indirizzo byte in PSRAM del blocco edge del neurone. \\ \rowa \code{n\_conn} & 2 & Connessioni reali (pre-padding). \\ \code{out\_id} & 2 & Id in cui scrivere l'uscita del neurone. \\ \rowa \code{activation} & 1 & \code{ACT\_RELU} / \code{ACT\_NONE} (2 bit bassi). \\ \code{bias} & 1 & Bias del neurone (INT8). \\ \rowa \code{reserved} & 2 & 0. \\ \midrule \rowh \thd{Totale} & \thd{11} & voci in ordine di \code{out\_id} crescente \\ \bottomrule \end{tabularx} \subsection{Edge del grafo (4~byte, allineato)} \begin{tabularx}{\textwidth}{L{3.4cm} C{1.6cm} Y} \toprule \rowh \thd{Campo} & \thd{Byte} & \thd{Significato} \\ \midrule \code{src\_id} & 2 & Id sorgente (uint16 BE). \\ \rowa \code{weight} & 1 & Peso (INT8). \\ \code{reserved} & 1 & 0 (allineamento a 4~byte). \\ \bottomrule \end{tabularx} \begin{fnnote}[Padding a \texttt{PARALLEL}] \code{n\_conn} arbitrario non è multiplo di \code{PARALLEL}: la edge-list del neurone è riempita fino al multiplo con edge a \textbf{peso zero} (spreco $\le$\code{PARALLEL}$-1$ per neurone). Così il datapath e il suo guard restano intatti. \end{fnnote} \section{\texttt{graph\_engine} --- motore del grafo} \code{rtl/graph\_engine.v} orchestra il Tipo \#2 \textbf{riusando \code{neuron\_parallel} senza modificarlo}, come fa \code{neuron\_memory} per il caso denso. Differenza chiave: tra i due modi cambia \emph{solo l'indirizzamento di X}. In Tipo \#1 l'input è contiguo (\code{x\_base + i}); in Tipo \#2 è un gather (\code{act\_buf[src\_id]}). Il core aritmetico non si tocca. \begin{center} \begin{tikzpicture}[font=\scriptsize,node distance=4mm,start chain=going below, every node/.style={on chain,fnblock,minimum width=52mm}] \node[fnblockA]{\code{COPY\_INPUTS}: PSRAM \code{x\_base} $\to$ \code{act\_buf[0..N\_in-1]}}; \node{\code{READ\_DESC}: descrittore del neurone k}; \node{\code{READ\_EDGES}: stream edge + gather \code{act\_buf[src\_id]}}; \node{\code{START\_N} / \code{WAIT\_N}: gruppo da \code{PARALLEL} $\to$ \code{neuron\_parallel}}; \node[fnblockT]{\code{WRITE\_ACT}: y $\to$ \code{act\_buf[out\_id]}}; \node{neurone successivo (ordine di id)}; \node[fnblockD]{\code{WRITE\_OUTPUTS}: ultimi \code{n\_out} $\to$ PSRAM \code{out\_base}}; \foreach \i [count=\j from 2] in {1,...,6} \draw[fnarrow] (chain-\i) -- (chain-\j); \end{tikzpicture} \end{center} Le uscite sono gli \textbf{ultimi \code{n\_out}} id: nei DAG con l'ordinamento \code{src\_id < out\_id} i neuroni di uscita (sink, non riusati come sorgente) finiscono naturalmente con gli id più alti. A fine esecuzione \code{graph\_engine} copia questi \code{n\_out} byte in una regione PSRAM a \code{out\_base}, che l'host rilegge con \op{READ\_RAM}. \section{Opcode e registri del Tipo \#2} La selezione del tipo avviene con un nuovo opcode; \op{RUN\_NETWORK} fa il dispatch sul registro \code{net\_type} (dettagli in cap.~\ref{ch:spi}). \begin{tabularx}{\textwidth}{L{2.6cm} L{3.4cm} Y} \toprule \rowh \thd{Opcode / sel} & \thd{Nome} & \thd{Funzione} \\ \midrule \op{0x11} & SET\_NET\_TYPE & \code{type(1B)}: \code{0x01}=dense (\#1), \code{0x02}=graph (\#2). Default dopo \op{RESET}=dense. \\ \rowa \code{SET\_BASE sel 9} & num\_neurons\_graph & Numero di neuroni del grafo (uint16). \\ \code{SET\_BASE sel 10} & n\_out & Numero di id di uscita (uint16). \\ \bottomrule \end{tabularx} \begin{fnnote}[Zero regressioni sul Tipo \#1] Con \code{net\_type=dense} (valore di default dopo \op{RESET}) il percorso \#1 è bit-identico a prima: \op{RUN\_NETWORK} mantiene il payload \code{num\_layers(1B)} e il framing degli opcode esistenti non cambia. \end{fnnote} \section{Occupazione (Tipo \#2 abilitato)} Sintesi Yosys del sistema completo \code{spi\_neuron\_top} con Tipo \#2 abilitato (\code{PARALLEL}=2): \begin{tabularx}{\textwidth}{L{4.6cm} Y} \toprule \rowh \thd{Risorsa} & \thd{Uso} \\ \midrule \code{DP16KD} (block RAM) & 2 (buffer di attivazione) \\ \rowa \code{MULT18X18D} (DSP) & 4 (2 \code{neuron\_memory} + 2 \code{graph\_engine}) \\ LUT4 & 2619 \\ \rowa TRELLIS\_FF & 2467 \\ \code{\$\_TBUF\_} (bus PSRAM) & 16 \\ \bottomrule \end{tabularx} Il device (108 \code{DP16KD}, 72 DSP, $\approx$44k LUT/FF) resta ampiamente sotto la saturazione: il Tipo \#2 aggiunge una modalità completa a costo di risorse contenuto. LUT4/TRELLIS\_FF sono cresciuti rispetto a una misura precedente (2367/2406) per via del page mode PSRAM aggiunto al controller (cap.~\ref{ch:mem}, \S~5.5) --- sotto il 6\% di utilizzo, nessun impatto pratico. \section{Banda del gather (misurata)} Il costo per-edge del gather è stato \textbf{isolato} costruendo due grafi identici per struttura ma con conteggio edge diverso e differenziando i cicli: la sottrazione cancella l'overhead fisso per-neurone e lascia il solo costo dell'edge. \begin{fnspec}[Costo per-edge] \textbf{37.53 cicli/edge} con il page mode PSRAM abilitato (cap.~\ref{ch:mem}, \S~5.5) --- \textbf{53.25 cicli/edge} senza (baseline pre-page-mode, coerente con la teoria: 4~byte/edge $\times$ $\approx$13 cicli/byte via PSRAM asincrona $\approx$52). A 80~MHz: $\approx$2.13\,M edge/s ($\approx$8.5~MB/s, +42\% vs baseline); al clock reale di 16~MHz: $\approx$426\,k edge/s ($\approx$1.71~MB/s). \end{fnspec} Il page-mode read (roadmap G7, cap.~\ref{ch:roadmap}) è stato implementato e misurato: l'accesso sequenziale del gather ne beneficia direttamente, riducendo il costo per-edge del 29.5\% (53.25$\to$37.53 cicli/edge). Ogni edge continua comunque a pagare l'accesso byte-granulare di \code{int8\_memory\_access} (4 byte/edge); il page mode riduce il costo di ciascun byte sequenziale, non il numero di accessi. \section{Assemblatore host \texttt{netasm}} La configurazione leggibile della rete non richiede logica dedicata in FPGA: uno pseudo-assembly viene compilato \emph{sull'host} (\code{tools/netasm/}) nei byte esatti delle tabelle e degli edge, poi caricati con \op{WRITE\_RAM}. L'assemblatore valida a compile time (\code{src\_id < out\_id}, limiti \code{N\_TOTAL}, padding a \code{PARALLEL}), complementando il guard runtime. \begin{lstlisting}[language=,caption={Esempio di pseudo-assembly (grafo)},basicstyle=\ttfamily\scriptsize] NET graph INPUTS 4 ; id 0..3 NEURON n4 relu bias=2 CONN 0 w=5 CONN 1 w=-3 NEURON n5 none bias=0 CONN n4 w=2 ; riferimento simbolico all'uscita di n4 CONN 2 w=7 OUTPUT n5 END \end{lstlisting}