\chapter{Architettura: come funziona il chip} \label{chap:architecture} \section{Vista d'insieme} Il design realizzato in FPGA \`e un acceleratore per reti neurali quantizzate INT8, organizzato come un insieme di \textbf{elementi di elaborazione} (Processing Element, PE) paralleli che condividono un unico canale DDR3 reale, secondo un'architettura sistolica a \emph{broadcast dei pesi condiviso} (\emph{shared-weight broadcast}). \textbf{Stato reale a due configurazioni (Capitolo~\ref{chap:tests})}: \textbf{N=8} (\texttt{n8\_system\_ddr3\_top.v}, 2 gruppi da 4 PE) \`e la configurazione attualmente in fabbricazione fisica sulla scheda reale. \textbf{N=16} (\texttt{n16\_system\_ddr3\_top.v}, 4 gruppi da 4 PE) \`e ora, a seguito di una correzione reale del datapath MAC (\S\ref{sec:mac-pipeline}), \textbf{funzionalmente verificata E con timing reale chiuso} su un branch di sviluppo dedicato (\texttt{n16-timing-closure}), non ancora promossa alla scheda fisica in produzione --- una decisione hardware reale, non RTL, ancora da prendere. L'intero design gira in un unico dominio di clock reale a \textbf{155.039\,MHz} (\texttt{ui\_clk}, derivato dal MIG DDR3, Capitolo~\ref{chap:hardware}). \section{Il nucleo di calcolo: MAC INT8 impacchettati su DSP48E1} L'unit\`a base di calcolo \`e \texttt{neural\_processor\_packed.v}, usata identicamente in ogni PE. Ogni core usa \textbf{8 DSP48E1}, ciascuno configurato per eseguire \textbf{due moltiplicazioni-accumulo INT8 impacchettate per ciclo} (una corsia A e una corsia B, che condividono lo stesso peso residente) --- una tecnica di packing reale verificata sia a livello RTL sia con sintesi Xilinx effettiva: \[ 8~\text{DSP48E1} \times 2~\text{MAC/DSP} = 16~\text{MAC/ciclo per PE} \] Al clock reale di 155.039\,MHz, il picco teorico per singolo PE \`e: \[ 16~\text{MAC/ciclo} \times 155.039\times10^6~\text{cicli/s} = 2.48~\text{GMAC/s per PE (calcolo da dati misurati)} \] Questo nucleo \`e rimasto \emph{invariato} (stesso numero di DSP, stessa struttura) dalla primissima sintesi reale del progetto fino alla configurazione N=8: \`e la parte pi\`u stabile ed efficiente del design. A N=16 (\S\ref{sec:mac-pipeline}) \`e per\`o diventato, per la prima volta, il vero collo di bottiglia di temporizzazione --- non del throughput di calcolo (\S\ref{sec:bottleneck}), ma della temporizzazione fisica del place-and-route. \subsection{Correzione reale: pipeline aggiuntiva nel datapath MAC (per N=16)} \label{sec:mac-pipeline} A N=16, il margine di temporizzazione reale --- gi\`a estremamente sottile a N=2 (+0.0999962\,ns) --- \`e stato eroso oltre lo zero dalla maggiore congestione fisica complessiva del die (WNS reale misurato: $-0.338$\,ns, dopo un arbitro gerarchico e un tuning delle direttive di place-and-route, Capitolo~\ref{chap:tests}). Il percorso critico reale, tracciato (non ipotizzato) fino al livello del singolo registro, va dall'uscita del moltiplicatore DSP48E1 (gi\`a ri-temporizzata automaticamente da Vivado) attraverso la logica di ``spacchettamento'' dei due prodotti INT8 impacchettati (uno shift e una somma di riporto condizionale, dominata da primitive CARRY4) fino al registro che cattura il risultato --- tutto in un solo ciclo di clock. \textbf{Correzione reale applicata}: lo stadio originale \`e stato spezzato in due stadi di pipeline reali distinti --- il primo registra il prodotto grezzo del DSP48E1 senza alcuna logica intermedia; il secondo esegue lo spacchettamento (matematica identica, invariata bit per bit) a partire dal valore gi\`a registrato. Il costo reale: un solo ciclo di clock aggiuntivo di latenza per operazione, senza alcun impatto sul throughput (l'interfaccia a maniglia valid/ready non assume mai una latenza fissa). Verificato bit-esatto in isolamento (18/18 PASS contro 2 core reali di riferimento) e funzionalmente sull'intero sistema N=16 su DDR3 reale (32/32 PASS). \textbf{Risultato reale}: con questa sola correzione, il timing di N=16 \textbf{chiude realmente} (WNS $=+0.269$\,ns, WHS $=+0.026$\,ns, 0 endpoint falliti) --- si veda il Capitolo~\ref{chap:tests} per i numeri completi. La stessa correzione, applicata anche a N=8, migliora pure il suo margine (da 0.000\,ns esatto a +0.108\,ns) senza alcuna regressione funzionale in nessuna delle due configurazioni. \section{L'architettura sistolica a gruppi (N=8)} \subsection{Motivazione} Scalare il numero di PE aumentando semplicemente il numero di core indipendenti significa aumentare linearmente anche il numero di richiedenti sul canale DDR3 condiviso --- ogni PE, in un design ``piatto'', dovrebbe rifetchare autonomamente i pesi del layer anche quando pi\`u PE elaborano lo stesso layer in parallelo, sprecando banda DDR3 reale su dati identici gi\`a in transito per un altro PE. L'architettura a gruppi risolve questo con un \textbf{unico fetch dei pesi per gruppo}, condiviso via broadcast da tutti i PE del gruppo: \begin{itemize} \item Un gruppo (\texttt{systolic\_group.v}) possiede \textbf{un solo} percorso reale di prefetch pesi (\texttt{layer\_prefetch\_ctrl.v} + \texttt{layer\_weight\_buffer.v} + \texttt{weight\_tile\_gather.v} --- moduli riusati integralmente e non modificati dal design N=2 originale). \item I 4 PE del gruppo (\texttt{packed\_pe.v}, una versione di \texttt{packed\_slot.v} privata del proprio percorso di fetch pesi) ricevono il tile di peso corrente via un bus di broadcast, tramite un meccanismo reale di sincronizzazione a barriera: il gruppo avanza al tile successivo solo quando \emph{tutti e 4} i PE hanno confermato (\texttt{pe\_tile\_ack}) il consumo del tile corrente. \item Ogni PE elabora indipendentemente e in parallelo le proprie posizioni di attivazione (nessuna propagazione di risultati tra PE --- non \`e uno shift register sistolico letterale, ma un vero broadcast condiviso: scelta esplicitamente confermata con l'utente dopo un confronto diretto tra le due alternative). \end{itemize} Con questa scelta, il fetch pesi \`e ridotto di un fattore 4 per gruppo rispetto a un design piatto con lo stesso numero di PE, mentre il fetch delle attivazioni --- che non ha riuso possibile tra posizioni diverse --- resta uno per PE, esattamente come nel design N=2 originale. \subsection{Struttura a N=8} \begin{itemize} \item \textbf{2 gruppi} $\times$ \textbf{4 PE} = 8 PE reali totali. \item \textbf{64 DSP48E1} totali (8 per PE $\times$ 8 PE). \item Il modulo \texttt{neural\_director\_grouped.v} dispatcha \textbf{ottetti} di job (8 posizioni che condividono la stessa base dei pesi) a un gruppo libero --- estensione diretta della disciplina di accoppiamento a coppie gi\`a usata dal Director originale N=2, generalizzata a 8 posizioni. \end{itemize} Un fatto reale rilevante: il protocollo SPI verso l'host \textbf{non cambia affatto} passando da N=2 a N=8 --- l'host continua a sottomettere un job alla volta con la stessa struttura a 16 byte (\S\ref{sec:spi-protocol}); \`e il Director interno a raggruppare automaticamente gli 8 job pi\`u vecchi in coda quando condividono la stessa base dei pesi. \section{Arbitraggio gerarchico dell'accesso a DDR3} Con 8 PE reali pi\`u 2 unit\`a di fetch pesi di gruppo pi\`u l'accesso diretto dell'host, il numero di richiedenti reali sul singolo canale DDR3 fisico \`e 11. Un arbitro piatto a selezione singola (usato inizialmente per il tentativo a N=16) genera un multiplexer troppo largo, con un impatto reale misurato sul timing dominato dal \emph{routing} fisico (distanza tra le 20+ sorgenti sparse e il punto centrale di arbitraggio), non dalla profondit\`a logica. La soluzione reale adottata \`e un \textbf{arbitro gerarchico a due livelli} (\texttt{sdram\_arbiter\_hier.v}): \begin{itemize} \item \textbf{Livello foglia} (uno per gruppo): un'istanza dell'arbitro N-way gi\`a esistente (\texttt{sdram\_arbiter\_n.v}, riutilizzato \emph{senza modifiche}), che arbitra i 5 richiedenti locali del gruppo (1 fetch pesi + 4 PE), fisicamente vicini tra loro sul die. \item \textbf{Livello superiore}: una seconda istanza dello stesso arbitro, che arbitra tra i 2 gruppi e l'host, con un vero stadio di pipeline (un ciclo di registro) tra i due livelli --- questo \`e ci\`o che effettivamente permette al piazzatore/router di Vivado di distribuire il problema su due finestre di clock fisiche separate invece di forzare tutte le sorgenti a convergere in un solo ciclo. \item L'accesso dell'host \textbf{bypassa} il primo livello (collegamento diretto al livello superiore) poich\'e non ha mai rappresentato il collo di bottiglia reale. \end{itemize} Un vincolo di correttezza gi\`a stabilito nel progetto (dalla prima versione dell'arbitro, 2026) \`e che un richiedente a impulso singolo deve vedere il proprio \emph{grant} nello stesso ciclo in cui asserisce per la prima volta il proprio segnale di attivit\`a --- altrimenti la richiesta viene persa silenziosamente. L'arbitro gerarchico preserva questa garanzia esattamente per ogni PE e per il fetch pesi di ogni gruppo (il grant al livello foglia resta puramente combinatorio); solo il transito effettivo del dato verso il controllore DDR3 fisico viene pipeline-ato, con un costo reale di 1--2 cicli aggiuntivi di latenza per transazione --- trascurabile rispetto alla latenza intrinseca di un vero accesso DDR3 (decine di cicli). \section{Scrittura dei risultati} Ogni PE, al termine del proprio job, scrive il risultato direttamente in DDR3 tramite \texttt{result\_writeback.v} (un'istanza per PE), usando lo stesso canale di controllo gi\`a condiviso con il fetch attivazioni. Questo elimina un vincolo reale di scalabilit\`a che affliggeva le prime versioni del progetto: esporre i risultati come pin fisici dedicati del package sarebbe stato insostenibile oltre poche unit\`a di PE (a N=16 avrebbe richiesto centinaia di pin). \section{Il vincolo reale: banda DDR3, non conteggio dei DSP} \label{sec:bottleneck} Un'analisi quantitativa reale (misurata, non stimata) condotta nelle prime fasi del progetto ha stabilito che il sistema \`e \textbf{limitato dalla banda DDR3, non dal numero di DSP}, gi\`a a partire da un solo core: \begin{itemize} \item Banda DDR3 fisica reale misurata (canale a 16 bit, traccia JEDEC reale): 1.24\,GB/s. \item Banda richiesta da un singolo core al picco DSP teorico, con il packing delle attivazioni ottimizzato (1 byte DDR3 mosso per MAC utile): 2.48\,GB/s. \end{itemize} L'allargamento del canale fisico DDR3 da 16 a 32 bit (due chip Micron in parallelo, Capitolo~\ref{chap:hardware}) raddoppia realmente il tetto fisico a \textbf{2.48\,GB/s}, confermato da un vero place-and-route con timing chiuso (\S~\ref{sec:pnr-history}), non da una proiezione. L'occupazione reale di DSP48E1 resta comunque bassa anche alla configurazione N=8 (64/240, 26.7\%) --- conferma diretta che il margine di scalabilit\`a residuo del chip \`e ampio sul lato computazionale, ma \`e vincolato dal canale di memoria condiviso, non dalla logica di calcolo. \section{Configurazioni esplorate e stato reale attuale} Lo stesso RTL sistolico, tramite il parametro reale \texttt{N\_GROUPS}, \`e stato realmente sintetizzato e verificato a pi\`u configurazioni: \begin{itemize} \item \textbf{N=4} (1 gruppo, 4 PE, 32 DSP48E1) --- funzionante, margine di timing quasi nullo (non ancora rifinito con la correzione di \S\ref{sec:mac-pipeline}). \item \textbf{N=8} (\texttt{n8\_system\_ddr3\_top.v}) --- \textbf{la configurazione fisicamente in fabbricazione sulla scheda reale attuale.} Timing chiuso, WNS $=+0.108$\,ns con la correzione MAC. \item \textbf{N=16} (\texttt{n16\_system\_ddr3\_top.v}) --- dopo la correzione di \S\ref{sec:mac-pipeline}, \textbf{funzionalmente verificato E con timing reale chiuso} (WNS $=+0.269$\,ns), su un branch di sviluppo reale (\texttt{n16-timing-closure}) separato dalla scheda in produzione. \`E ora un candidato reale, verificato, per una futura revisione della scheda --- non ancora promosso alla produzione fisica corrente, una decisione hardware reale ancora da prendere con l'utente. \end{itemize} N=8 resta, ad oggi, la configurazione realmente fabbricata. N=16 non \`e pi\`u un limite architetturale reale (come inizialmente sembrava), ma una reale, verificata alternativa a parallelismo doppio, la cui adozione fisica dipende ora da una scelta dell'utente, non da un vincolo tecnico residuo.