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
234 lines
11 KiB
TeX
234 lines
11 KiB
TeX
\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 <x0 x1 x2 x3> 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 <x0 x1 x2 x3> 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<out_id, N_TOTAL, padding a PARALLEL
|
|
\end{lstlisting}
|
|
|
|
\begin{fnnote}[Perche' due livelli di codifica]
|
|
Lo pseudocodice host e il \code{netasm} producono gli \emph{stessi byte}. Il primo è utile
|
|
quando la rete è generata a runtime (es. pesi da training); il secondo quando la topologia
|
|
è scritta a mano o versionata come sorgente. In entrambi i casi l'FPGA riceve solo tabelle
|
|
e dati via \op{WRITE\_RAM}: nessun interprete a bordo.
|
|
\end{fnnote}
|