\chapter[Programmazione della rete]{Programmazione della rete neurale} \label{ch:prog} Questo capitolo è la guida pratica alla codifica di una rete per FPGA-Neural: come si dispone in memoria, quali registri si impostano e come si avvia, per entrambe le topologie. Presuppone gli opcode SPI (cap.~\ref{ch:spi}) e i formati descrittore (cap.~\ref{ch:seq}, \ref{ch:grafo}). \section{Flusso generale} Qualunque sia il tipo, il ciclo è lo stesso: l'host \emph{costruisce le strutture dati in RAM}, imposta i \emph{registri base}, dichiara il \emph{tipo di rete}, \emph{avvia} e \emph{rilegge} il risultato. \begin{center} \begin{tikzpicture}[font=\scriptsize,node distance=3mm,start chain=going below, every node/.style={on chain,fnblock,minimum width=64mm}] \node[fnblockA]{1. \op{RESET} --- azzera il motore e il latch STATUS}; \node{2. \op{SET\_NET\_TYPE} --- dense (\#1) o graph (\#2)}; \node{3. \op{WRITE\_RAM} --- tabelle, pesi/edge, bias, input X}; \node{4. \op{SET\_BASE} --- registri base (x, table, \ldots)}; \node[fnblockT]{5. \op{RUN\_NETWORK} --- dispatch su \code{net\_type}}; \node{6. \op{STATUS} in polling --- attende \code{done}}; \node[fnblockD]{7. \op{READ\_OUTPUT} / \op{READ\_RAM} --- risultato}; \foreach \i [count=\j from 2] in {1,...,6} \draw[fnarrow] (chain-\i)--(chain-\j); \end{tikzpicture} \end{center} \section{Registri e opcode coinvolti} Tutti i valori base si impostano con \op{SET\_BASE} \code{sel(1B)+addr(3B)}. Selettori: \begin{tabularx}{\textwidth}{C{1.0cm} L{3.4cm} C{1.4cm} C{1.4cm} Y} \toprule \rowh \thd{sel} & \thd{Registro} & \thd{Tipo \#1} & \thd{Tipo \#2} & \thd{Uso} \\ \midrule 0 & \code{x\_base} & \checkmark & \checkmark & Base input $X$. \\ \rowa 3 & \code{table\_base} & \checkmark & \checkmark & Tabella descrittori. \\ 4 & \code{buf\_a\_base} & \checkmark & \checkmark\textsuperscript{$\ast$} & Ping-pong A (\#1) / \code{out\_base} riuso (\#2). \\ \rowa 5 & \code{buf\_b\_base} & \checkmark & --- & Ping-pong B (\#1). \\ 9 & \code{num\_neurons\_graph} & --- & \checkmark & Numero neuroni del grafo. \\ \rowa 10 & \code{n\_out} & --- & \checkmark & Numero id di uscita. \\ \bottomrule \end{tabularx} \begin{center}\footnotesize\itshape\color{fnGrey} $\ast$ In Tipo \#2 i buffer ping-pong non servono: il selettore 4 è riusato come \code{out\_base} (regione dove copiare le uscite). I selettori 1/2/6/7/8 riguardano solo il percorso single-layer manuale (\op{START}), non \op{RUN\_NETWORK}.\end{center} Per il Tipo \#1, i \code{w\_base}/\code{bias\_addr} \emph{per-layer} \textbf{non} si impostano con \op{SET\_BASE}: sono campi della tabella descrittori. \op{SET\_NET\_TYPE} default dopo \op{RESET} è \emph{dense}, quindi una rete \#1 funziona anche senza emetterlo. % ====================================================================== \section{Tipo \#1 --- rete densa} \subsection{Layout in memoria} \begin{tabularx}{\textwidth}{L{3.4cm} Y} \toprule \rowh \thd{Struttura} & \thd{Formato} \\ \midrule Input $X$ & \code{n\_inputs\_real} byte INT8 a \code{x\_base}. \\ \rowa Pesi (per layer) & Neuron-major: neurone $k$ a \code{w\_base + k*n\_inputs\_real}, \code{n\_neurons*n\_inputs} byte. \\ Bias (per layer) & Un byte INT8 per neurone a \code{bias\_addr}. \\ \rowa Tabella descrittori & \code{num\_layers} voci da 11 byte a \code{table\_base}. \\ Buffer A/B & Uscite intermedie ping-pong. \\ \bottomrule \end{tabularx} Descrittore (11 byte, MSB-first): \code{w\_base}(3) $|$ \code{bias\_addr}(3) $|$ \code{activation}(1) $|$ \code{n\_inputs\_real}(2) $|$ \code{n\_neurons\_real}(2). \subsection{Esempio completo: rete $4\to4\to2$} Layer~0: 4 input, 4 neuroni, ReLU. Layer~1: 4 input, 2 neuroni, lineare (\code{PARALLEL}=2, quindi ogni \code{n\_inputs\_real} è multiplo di 2). Indirizzi scelti: \code{table\_base}=\code{0x000000}, \code{x\_base}=\code{0x001000}, pesi/bias L0 a \code{0x002000}/\code{0x002100}, L1 a \code{0x002200}/\code{0x002300}, buffer a \code{0x003000}/\code{0x003100}. \begin{lstlisting}[language=,caption={Tabella descrittori dense (22 byte)},basicstyle=\ttfamily\scriptsize] Layer 0: 00 20 00 | 00 21 00 | 01 | 00 04 | 00 04 w_base bias_addr ReLU n_in=4 n_neu=4 Layer 1: 00 22 00 | 00 23 00 | 00 | 00 04 | 00 02 w_base bias_addr NONE n_in=4 n_neu=2 \end{lstlisting} \begin{lstlisting}[language=,caption={Sessione SPI (dense)},basicstyle=\ttfamily\scriptsize] 0x0F RESET 0x11 01 SET_NET_TYPE = dense 0x01 000000 0016 <22 byte tabella> WRITE_RAM tabella 0x01 002000 0010 <16 byte pesi L0> WRITE_RAM pesi L0 (neuron-major) 0x01 002100 0004 <4 byte bias L0> 0x01 002200 0008 <8 byte pesi L1> 0x01 002300 0002 <2 byte bias L1> 0x01 001000 0004 WRITE_RAM input X 0x10 00 001000 SET_BASE x_base 0x10 03 000000 SET_BASE table_base 0x10 04 003000 SET_BASE buf_a 0x10 05 003100 SET_BASE buf_b 0x23 02 RUN_NETWORK num_layers=2 0x21 ... poll STATUS finche' done=1 0x22 READ_OUTPUT -> 2 byte (layer finale) \end{lstlisting} \subsection{Pseudocodice host (dense)} \begin{lstlisting}[language=,caption={Codifica e caricamento di una rete densa},basicstyle=\ttfamily\scriptsize] def load_dense(layers, X): # layers in ordine di esecuzione spi(RESET); spi(SET_NET_TYPE, DENSE) table = b"" for L in layers: # L: pesi[n][k], bias[n], act, n_in, n_out assert L.n_in % PARALLEL == 0 w = alloc(L.weights_neuron_major) # k lento, input veloce b = alloc(L.bias) table += u24(w)+u24(b)+u8(L.act)+u16(L.n_in)+u16(L.n_out) write_ram(TABLE_BASE, table) write_ram(X_BASE, X) set_base(0, X_BASE); set_base(3, TABLE_BASE) set_base(4, BUF_A); set_base(5, BUF_B) spi(RUN_NETWORK, len(layers)) wait_status_done() return read_output(layers[-1].n_out) \end{lstlisting} % ====================================================================== \section{Tipo \#2 --- rete a grafo} \subsection{Layout in memoria} \begin{tabularx}{\textwidth}{L{3.4cm} Y} \toprule \rowh \thd{Struttura} & \thd{Formato} \\ \midrule Input $X$ & \code{N\_in} byte a \code{x\_base}; copiati in \code{act\_buf[0..N\_in-1]} all'avvio. \\ \rowa Tabella descrittori & \code{num\_neurons\_graph} voci da 11 byte a \code{table\_base}, in ordine di \code{out\_id} crescente. \\ Blocchi edge & Per neurone: \code{n\_conn} edge da 4 byte a \code{conn\_ptr}, con padding a multiplo di \code{PARALLEL} (edge peso 0). \\ \rowa Uscite & \code{n\_out} byte scritti a \code{out\_base} (=selettore 4). \\ \bottomrule \end{tabularx} Descrittore graph (11 byte): \code{conn\_ptr}(3) $|$ \code{n\_conn}(2) $|$ \code{out\_id}(2) $|$ \code{activation}(1) $|$ \code{bias}(1) $|$ \code{reserved}(2). \quad Edge (4 byte): \code{src\_id}(2) $|$ \code{weight}(1) $|$ \code{reserved}(1). \quad Vincolo: \code{src\_id < out\_id} (DAG feed-forward). \subsection{Esempio completo} 4 ingressi (id 0--3). Neurone n4 (\code{out\_id}=4, ReLU, bias=2) connesso agli id 0 e 1; neurone n5 (\code{out\_id}=5, lineare, bias=0) connesso a n4 (id~4) e all'id~2; uscita = n5 (\code{n\_out}=1). \code{PARALLEL}=2, entrambi hanno 2 connessioni (nessun padding). Indirizzi: \code{table\_base}=\code{0x000000}, edge a \code{0x000100}, \code{x\_base}= \code{0x001000}, \code{out\_base}=\code{0x002000}. \begin{lstlisting}[language=,caption={Descrittori + edge grafo},basicstyle=\ttfamily\scriptsize] Descrittori (a 0x000000, 22 byte): n4: 00 01 00 | 00 02 | 00 04 | 01 | 02 | 00 00 conn_ptr n_conn out_id ReLU bias rsv n5: 00 01 08 | 00 02 | 00 05 | 00 | 00 | 00 00 conn_ptr n_conn out_id NONE bias rsv Blocchi edge (a 0x000100, 4 byte/edge: src_id, weight, rsv): n4 @0x000100: 00 00 05 00 (src=0, w=+5) 00 01 FD 00 (src=1, w=-3) ; -3 = 0xFD n5 @0x000108: 00 04 02 00 (src=4, w=+2) ; id4 = uscita di n4 00 02 07 00 (src=2, w=+7) \end{lstlisting} \begin{lstlisting}[language=,caption={Sessione SPI (graph)},basicstyle=\ttfamily\scriptsize] 0x0F RESET 0x11 02 SET_NET_TYPE = graph 0x01 000000 0016 <22 byte tabella> WRITE_RAM descrittori 0x01 000100 0010 <16 byte edge> WRITE_RAM blocchi edge 0x01 001000 0004 WRITE_RAM input X 0x10 00 001000 SET_BASE x_base 0x10 03 000000 SET_BASE table_base 0x10 04 002000 SET_BASE out_base (riuso sel 4) 0x10 09 000002 SET_BASE num_neurons_graph = 2 0x10 0A 000001 SET_BASE n_out = 1 0x23 00 RUN_NETWORK (dispatch a graph_engine) 0x21 ... poll STATUS (bit2=err se src_id>=out_id) 0x02 002000 0001 READ_RAM out_base -> 1 byte (uscita n5) \end{lstlisting} \subsection{Pseudocodice host (graph)} \begin{lstlisting}[language=,caption={Codifica e caricamento di un grafo},basicstyle=\ttfamily\scriptsize] def load_graph(neurons, X, n_out): # neurons ordinati per out_id crescente spi(RESET); spi(SET_NET_TYPE, GRAPH) edges = b""; table = b"" for N in neurons: # N: out_id, conns=[(src_id,w)...], act, bias for (src,_) in N.conns: assert src < N.out_id and src < N_TOTAL # regola DAG conn_ptr = EDGE_BASE + len(edges) padded = pad(N.conns, PARALLEL, fill=(0,0)) # edge peso 0 for (src,w) in padded: edges += u16(src)+i8(w)+u8(0) table += u24(conn_ptr)+u16(len(N.conns))+u16(N.out_id) \ + u8(N.act)+i8(N.bias)+u16(0) write_ram(TABLE_BASE, table); write_ram(EDGE_BASE, edges) write_ram(X_BASE, X) set_base(0, X_BASE); set_base(3, TABLE_BASE); set_base(4, OUT_BASE) set_base(9, len(neurons)); set_base(10, n_out) spi(RUN_NETWORK, 0) # payload ignorato in graph wait_status_done() return read_ram(OUT_BASE, n_out) \end{lstlisting} \subsection{Pseudo-assembly \texttt{netasm}} La descrizione leggibile viene compilata dall'assemblatore host (\code{tools/netasm/}) esattamente nei byte delle tabelle e degli edge sopra. Esempio equivalente al grafo dell'esempio: \begin{lstlisting}[language=,caption={netasm: sorgente e byte generati},basicstyle=\ttfamily\scriptsize] ; --- sorgente --- 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 -> id 4 CONN 2 w=7 OUTPUT n5 END ; --- l'assemblatore emette --- ; id assegnati: n4=4, n5=5 (garantito src_id < out_id) ; descrittori: 00 01 00 00 02 00 04 01 02 00 00 ; 00 01 08 00 02 00 05 00 00 00 00 ; edge: 00 00 05 00 00 01 FD 00 (n4) ; 00 04 02 00 00 02 07 00 (n5) ; registri: table_base, x_base, out_base, num_neurons=2, n_out=1 ; validato a compile-time: src_id