Elaborato Cioffi Catello Rosario N46001344

Transcript

Elaborato Cioffi Catello Rosario N46001344
Scuola Politecnica e delle Scienze di Base
Corso di Laurea in Ingegneria Informatica
Elaborato finale in Calcolatori Elettronici 1
Accelerazione FPGA per applicazioni di
gaming
Anno Accademico 2013/2014
Candidato:
Catello Rosario Ciofffi
matr. N46/1344
A tutti i miei cari e a coloro che mi
hanno sostenuto.
Indice
Indice .................................................................................................................................................. III
Introduzione ......................................................................................................................................... 4
Capitolo 1: FPGA................................................................................................................................. 5
1.1 Architettura ................................................................................................................................ 6
1.1.1 Blocchi logici configurabili ............................................................................................... 7
1.1.2 Linee di interconnessione ................................................................................................. 8
1.1.3 Blocchi di ingresso/uscita.................................................................................................. 9
1.2 Tecnologie di programmazione ................................................................................................. 9
1.2.1 Memoria statica ............................................................................................................... 10
1.2.2 Flash ................................................................................................................................. 10
1.2.3 Anti-Fuse .......................................................................................................................... 10
1.3 Cenni sulla progettazione su FPGA ......................................................................................... 11
Capitolo 2: Background ..................................................................................................................... 14
2.1 Algoritmi per la ricerca ............................................................................................................ 15
2.1.1 Minimax ........................................................................................................................... 15
2.1.2 Alpha-beta pruning ......................................................................................................... 18
2.1.3 Tavola di Trasposizione e algoritmo di Zobrist ........................................................... 19
2.1.4 Quiescence search ........................................................................................................... 21
Capitolo 3: Da software a hardware ................................................................................................... 23
3.1 Micro-Max ............................................................................................................................... 23
3.1.1 Rappresentazione pezzi .................................................................................................. 23
3.1.2 Rappresentazione tavola................................................................................................. 23
3.1.3 Variabili globali e macro ................................................................................................ 24
3.1.4 Descrizione del software ................................................................................................. 24
Descrizione Hardware .................................................................................................................... 28
Conclusioni ........................................................................................................................................ 33
Bibliografia ........................................................................................................................................ 34
Introduzione
Le FPGA nascono nel 1985, da un’intuizione di Ross Freeman e Bernard Vonderschmitt,
fondatori della Xilinx. L’idea era quella di unire l’user control e il time to market dei PLD
con gli stessi costi e le densità che offrivano i gate array. Le caratteristiche di flessibilità,
capacità di implementare funzioni complesse e con basso time to market trovano ancora
oggi ampio raggio d’azione in applicazioni embedded.
In questo elaborato si intende mostrare come le potenzialità delle FPGA, possano essere
sfruttate per realizzare in hardware, un software di gaming ottenendo così delle prestazioni
migliori. In particolare viene approfondito il gioco degli scacchi, tenendo come
riferimento il programma Micro-Max.
Nel Capitolo 1 viene approfondita la struttura delle FPGA, passando dalla tecnologia a
cenni sulla programmazione.
Nel Capitolo 2 si analizza l’architettura generale di un videogioco di scacchi,
approfondendo gli algoritmi fondamentali per la realizzazione di un’intelligenza
artificiale.
Il Capitolo 3 si divide in due parti. Nella prima viene fornita una descrizione del software
Micro-Max. Nella seconda si illustra un possibile progetto in hadware di alto livello di tale
software.
4
Capitolo 1: FPGA
Con l’acronimo FPGA (Field Programmable Gate Array), si indica una particolare classe
di circuiti integrati a logica programmabile. Tali dispositivi sono costituiti da milioni di
celle logiche, ognuna delle quali può specificare una determinata funzione booleana e può
comunicare con le altre tramite una rete di interconnessioni. La caratteristica principale
delle FPGA è che sono programmate direttamente dall’utente, senza la necessità di dover
produrre un dispositivo specifico per una determinata applicazione come nei dispositivi
ASIC (Application Specific Integrated Circuit). I principali produttori di FPGA sono
Xilinx e Altera.
Le FPGA presentano diversi vantaggi rispetto ai dispositivi ASIC. Per produzioni di
piccole e medie dimensioni, le FPGA sono delle soluzioni più economiche e con un time
to market (TTM) più veloce rispetto agli ASIC, che hanno bisogno di più tempo e denaro
per la realizzazione del primo dispositivo. Inoltre, le FPGA hanno una notevole
flessibilità, in quanto possono essere riconfigurate semplicemente riprogrammando il
dispositivo e hanno dei cicli di design più semplice (poiché il software di sviluppo si
prende carico del routing, del timing e della disposizione dei componenti). Tuttavia la
flessibilità dei dispositivi FPGA presenta dei costi. Infatti, le FPGA sono più grandi, più
lente e consumano più potenza dei dispositivi ASIC. Di conseguenza le FPGA sono
particolarmente inadatte per applicazioni General Purpose (basati sui microprocessori) o
Special Purpose, ambito dominato dagli ASIC. Nonostante questi svantaggi, le FPGA
rappresentano una valida alternativa per lo sviluppo di sistemi digitali grazie al basso costo
unitario, per il basso time to market e la enorme flessibilità.
5
1.1 Architettura
La struttura generale di una FPGA è composta dai seguenti componenti:

Configurable Logic Blocks (CLB);

Elementi di interconnessione programmabile;

Blocchi di I/O.
Tipicamente i CLB sono disposti in una matrice bidimensionale e interconnessi con risorse
di routing programmabili. I blocchi di I/O sono, invece, posizionati ai margini della
matrice e, come i CLB, sono connessi alle risorse di routing (Fig. 1).
Figura 1: Struttura di una FPGA.
6
1.1.1 Blocchi logici configurabili
Il blocco CLB è un componente fondamentale per le FPGA e ne rappresenta la base
logica. Le CLB possono essere costituite da transistor oppure da veri e propri processori.
In generale si utilizzano CLB basate su Look-Up Tables (LUT), le quali forniscono un
buon compromesso nella granularità dei blocchi logici. Il CLB più semplice è formato da
una LUT, un flip-flop tipicamente di tipo D e un multiplexer per selezionare l’uscita (Fig.
2).
Figura 2: Struttura di un CLB.
Figura 3: Struttura di una LUT a 2 ingressi.
7
La LUT è un componente capace di implementare una qualsiasi funzione di N variabili
Booleane. La struttura hardware di quest’ultima è costituita da una serie di celle di
memoria, tipicamente di tipo SRAM, connesse ad un insieme di multiplexer (Fig. 3).
In questo modo una LUT può essere in grado sia di generare una funzione logica che di
operare come un elemento di immagazzinamento di dati.
Le moderne FPGA sono composte anche da elementi più complessi di quelli sopra
descritti come, ad esempio, memorie, moltiplicatori, addizionatori, etc. Ciò è giustificato
dal fatto che includere tali componenti è più efficiente che implementarli tramite le LUT.
Ad esempio, nelle FPGA della Xilinx sono presenti delle memorie RAM aggiuntive
chiamate Block RAM (BRAM). Una BRAM può essere di tipo dual-port o single-port, nel
primo caso il singolo blocco è come se fosse costituito da due memorie indipendenti, nel
secondo solo da una memoria. Più BRAM possono essere collegate insieme in modo da
formare blocchi di memoria più grandi. Nel caso della famiglia Spartan-3 ogni BRAM
comprende circa 18 Kb di memoria, e a seconda dello specifico modello si possono avere
da 4 a 104 blocchi, per una memoria massima che va da 72 Kb a 1872 Kb.
1.1.2 Linee di interconnessione
La rete di routing programmabile è necessaria per connettere tra di loro i vari componenti
Figura 4: Island-style architecture.
8
di una FPGA. Questa rete consiste in una serie di collegamenti e swicth, configurabili
tramite tecnologia programmabile. Sulla base della disposizione delle risorse di routing, le
architetture FPGA possono essere di tipo island-style e gerarchiche.
L’architettura island-style (Fig. 4) è la più comune in ambito commerciale. In questo tipo
si distinguono gli switch box, che consentono la connessione delle linee di routing
orizzontali e verticali, e le connection box che connettono i blocchi logici alla rete di
routing.
Nell’architettura di tipo gerarchico i blocchi logici sono divisi in cluster separati, connessi
in modo ricorsivo tale da formare una struttura gerarchica.
1.1.3 Blocchi di ingresso/uscita
I blocchi di ingresso/uscita si occupano della gestione dei segnali input/output delle FPGA
attraverso il controllo dei pin del chip. Nei dispositivi Xilinx, per esempio, ogni blocco
controlla un pin che può essere configurato come input, output, bi-direzionale o tri-state.
Sono inoltre presenti delle resistenze di pull-up/pull-down che permettono di caratterizzare
lo stato del piedino nelle situazioni di alta impedenza.
1.2 Tecnologie di programmazione
Esistono diverse tecnologie di programmazione che influenzano l’architettura delle
interconnessioni e dei blocchi logici. Tra le principali tecnologie vi sono: memoria statica,
flash e anti-fuse.
9
1.2.1 Memoria statica
È la tecnologia più usata dalle case produttrici e consiste nell’usare celle di memoria
statica SRAM (Fig. 5) per la configurazione delle interconnessioni e dei blocchi logici.
L’utilizzo delle SRAM è diventato predominante nei dispositivi FPGA, a causa della
riprogrammabilità e poiché tali celle possono essere create tramite CMOS diminuendo le
dimensioni e i consumi di potenza dinamica. Tuttavia le SRAM sono dispositivi volatili
per cui è necessario introdurre elementi aggiuntivi in grado di ripristinarne lo stato. Ciò
determina un aumento dei costi e overhead.
Figura 5: Blocco di memoria statica.
1.2.2 Flash
Questa tecnologia consiste nell’utilizzare memorie EEPROM o flash. Essa ha il vantaggio
di avere dimensioni più ridotte e di non essere volatile. A differenza delle SRAM, però,
possono essere riprogrammate solo un numero limitato di volte. Inoltre non viene
utilizzato il processo standard CMOS.
1.2.3 Anti-Fuse
Tale tecnica prevedo l’utilizzo di elementi costituiti da un sottile strato di silicio amorfo
tra due strati di materiale conduttore. La programmazione avviene applicando al
10
dispositivo una tensione a rendere conduttivo lo strato di silicio in maniera permanente. Il
principale vantaggio di questa tecnologia risiede nelle dimensioni notevolmente ridotte.
Tuttavia una volta programmati i dispositivi anti-fuse non possono essere più riconfigurati
e ciò rappresenta un grave svantaggio.
Figura 6: Dispositivo anti-fuse.
1.3 Cenni sulla progettazione su FPGA
In genere le FPGA possono essere programmate utilizzando dei linguaggi di descrizione
dell’hardware (HDL) quali il VHDL e il Verilog, oppure disegnando lo schema del
circuito da implementare tramite un apposito software. Tipicamente le case produttrici di
FPGA forniscono gratuitamente dei software di sviluppo che facilitano la fase di
progettazione.
Il processo che converte la descrizione del circuito in un formato caricabile su una FPGA,
può essere diviso in cinque fasi:

Sintesi;

Simulazione;

Mapping;

Place e Route;
11

Generazione del bitstream.
La sintesi trasforma la descrizione del circuito in linguaggio HDL, in un circuito
composto da porte logiche e flip-flop.
Il mapping crea la corrispondenza tra i componenti del circuito ottenuti nella fase di
sintesi e i CLB che compongono la FPGA. In questa fase vengono effettuate anche delle
ottimizzazioni per ridurre lo spazio occupato oppure per aumentare le prestazioni.
Nella fase di place e route vengono assegnate le celle logiche a una specifica posizione
dell’FPGA e create le interconnessioni.
Una volta posizionati gli elementi circuitali e effettuate le connessioni, viene prodotto il
bitstream che attraverso un apposito loader viene scaricato sul dispositivo FPGA.
La simulazione è una fase intermedia in cui si verifica il corretto funzionamento del
circuito.
12
13
Capitolo 2: Background
L’applicazione che verrà approfondita in questo secondo capitolo è un videogioco di
scacchi. Gli algoritmi usati per l’intelligenza artificiale sono usati anche in altre
applicazioni.
In base alla teoria dei giochi, gli scacchi sono un gioco sequenziale a informazione perfetta
(cioè il giocatore ha conoscenza di tutte le mosse eseguite dall’avversario, delle sue
strategie e la loro utilità) e a somma zero (cioè la vittoria di un giocatore determina la
sconfitta dell’altro). Questo tipo di applicazioni possono essere analizzati usando un albero
delle mosse, in cui ogni nodo rappresenta uno stato del gioco (cioè una posizione dei pezzi
sulla scacchiera) e ogni ramo indica una particolare mossa da parte del giocatore di turno
verso un determinato stato. Ciò implica che data una qualsiasi posizione dei pezzi sulla
scacchiera, a questa possa corrispondere:
1. Una vittoria per il giocatore bianco;
2. Un pareggio;
3. Una vittoria per il giocatore nero;
La grandezza dell’albero di gioco, dovuta alla complessità delle regole degli scacchi,
rende praticamente impossibile effettuare per un dato stato una valutazione esatta. Per
questo motivo si utilizzano funzioni euristiche, che tengano conto del valore dei singoli
pezzi, della loro posizione, della fase di gioco, etc.
In un videogioco di scacchi convenzionale si distinguono tre componenti:

Un generatore di mosse;

Una funzione di ricerca;
14

Uno stimatore di posizione.
La funzione di ricerca comprende i meccanismi per l’esplorazione dell’albero delle mosse.
Raggiunto un nodo foglia dell’albero, lo stimatore deve assegnare a tale nodo un
opportuno valore, per ognuno di essi, il generatore deve fornire tutte le possibili mosse che
un determinato pezzo può eseguire in accordo alle regole degli scacchi.
A questi elementi vanno aggiunti anche un componente di comunicazione e un selettore
per la migliore mossa. Il primo è essenziale per poter comunicare le mosse in ingresso o in
uscita. Il secondo serve ad aumentare la velocità della ricerca.
2.1 Algoritmi per la ricerca
L’algoritmo utilizzato per implementare la funzione di ricerca è l’alpha-beta o potatura
alpha-beta, esso è un miglioramento del minimax. Di seguito viene fornita una panoramica
su tali algoritmi e le principali tecniche usate per migliorarne l’efficienza.
2.1.1 Minimax
Il minimax è un algoritmo ricorsivo usato per determinare la prossima mossa da effettuare.
Ad ogni possibile stato del gioco viene assegnato un valore. Tale valore indica la validità
di una mossa per un determinato giocatore.
Un possibile metodo è quello di assegnare a una posizione il valore +1, nel caso questa
porti a vittoria certa il giocatore A e -1 nel caso del giocatore B. Alternativamente si può
usare rispettivamente il valore +∞ o – ∞. Per questo motivo si dice che A è il giocatore
massimizzante e B quello minimizzante. Poichè nel caso degli scacchi non è
computazionalmente possibile analizzare l’intero albero delle mosse, quindi determinare
una vittoria o una sconfitta certa, ai nodi viene assegnato un valore finito determinato in
base a delle euristiche. Si può quindi limitare l’analisi effettuata dal minimax a un
determinato numero di mosse dette ply.
L’algoritmo inizia valutando i nodi foglia, situate al livello n dell’albero, tramite una
15
funzione di valutazione. Si procede, quindi, assegnando ad ogni nodo che si trova alla
profondità n-1 il minimo dei valori che è stato assegnato ai nodi figli. Ai nodi del livello n-
Figura 7: Esempio minimax.
2 viene assegnato, invece, il massimo valore dei nodi figli di livello n-1. L’algoritmo
continua così fino a raggiungere il nodo radice a cui sarà assegnato il valore associato al
nodo che minimizza la massima perdita da parte del giocatore di turno.
Poiché il valore della posizione per il giocatore A è uguale alla negazione del valore della
posizione per il giocatore B, l'algoritmo può essere scritto in un modo più semplice, non
facendo alcuna distinzione tra il giocatore massimizzante e quello minimizzante. Tale
variante del minimax ritorna il valore negato di ogni sotto-albero e massimizza sempre il
punteggio, da qui il nome di negamax.
Di seguito viene fornito uno pseudocodice sia per il minimax che per il negamax.
function minimax(node, depth, maximizingPlayer)
if depth = 0 or node is a terminal node
return the heuristic value of node
if maximizingPlayer
bestValue := -∞
for each child of node
16
val := minimax(child, depth - 1, FALSE)
bestValue := max(bestValue, val)
return bestValue
else
bestValue := +∞
for each child of node
val := minimax(child, depth - 1, TRUE)
bestValue := min(bestValue, val)
return bestValue
//chiamata iniziale per il giocatore massimizzante
minimax(origin, depth, TRUE)
function negamax(node, depth, color)
if depth = 0 or node is a terminal node
return color * the heuristic value of node
bestValue := -∞
foreach child of node
val := -negamax(child, depth - 1, -color)
bestValue := max( bestValue, val )
return bestValue
//chimata iniziale per il giocatore A
rootValue := negamax( rootNode, depth, 1)
//chiamata iniziale per il giocatore B
rootValue := negamax( rootNode, depth, -1)
17
2.1.2 Alpha-beta pruning
Per migliorare i tempi della ricerca, l'algoritmo minimax può essere ottimizzato
utilizzando un meccanismo che prende il nome di alpha-beta pruning ovvero potatura
alpha-beta. Il vantaggio di tale meccanismo è quello di eliminare rami dell'albero senza
che il risultato finale sia compromesso. Infatti il punteggio dei nodi che appartengono ai
rami eliminati non influisce sul valore del nodo radice. Il miglioramento che si ottiene
rispetto al semplice minimax o negamax, è quello di poter analizzare nello stesso tempo un
albero con una profondità quasi raddoppiata.
L'algoritmo mantiene due variabili, alpha e beta, che rappresentano rispettivamente il
massimo punteggio ottenibile dal giocatore massimizzante e il minimo punteggio
ottenibile dal giocatore minimizzante. Inizialmente alpha è posto uguale a meno infinito,
mentre beta a più infinito. La ricerca procede secondo l'algoritmo minimax, aggiornando i
valori di alpha e beta. Se per un nodo si ha che alpha diventa maggiore o uguale a beta, la
ricerca per quel determinato sotto-albero si interrompe (avviene la potatura) e procede
analizzando il successivo.
Lo pseudocodice relativo è il seguente.
function alphabeta(node, depth, α, β, maximizingPlayer)
if depth = 0 or node is a terminal node
return the heuristic value of node
if maximizingPlayer
for each child of node
α := max(α, alphabeta(child, depth - 1, α, β, FALSE))
if β ≤ α
break (* β cut-off *)
return α
18
else
for each child of node
β := min(β, alphabeta(child, depth - 1, α, β, TRUE))
if β ≤ α
break (* α cut-off *)
return β
//chiamata iniziale
alphabeta(origin, depth, -∞, +∞, TRUE)
Per migliorare ulteriormente la ricerca effettuata dal minimax con potatura alpha-beta si
può utilizzare la strategia iterative deepning. Essa consiste nell’eseguire ripetutamente una
ricerca depth-limited, incrementando ad ogni iterazione il livello di profondità. In questo
modo il numero di nodi che vengono esplorati è maggiore rispetto a una ricerca senza tale
strategia, ottenendo una migliore stima del valore dei nodi.
2.1.3 Tavola di Trasposizione e algoritmo di Zobrist
Gli algoritmi presentati precedentemente non hanno memoria nel senso che non tengono
traccia delle posizioni già esplorate. Pertanto una stessa posizione può essere valutata più
volte, in quanto si può arrivare ad essa utilizzando differenti combinazioni di mosse dette
trasposizioni. La tavola di trasposizione è usata proprio per evitare questo problema. Una
volta trovata la valutazione di una determinata posizione, questa viene memorizzata nella
tavola e quando, durante la ricerca, viene incontrata nuovamente si può evitare di
effettuare ex novo la valutazione utilizzando il punteggio memorizzato.
Un problema della tavola di trasposizione è che la ricerca di posizioni collocate in maniera
casuale all’interno di essa può richiedere un tempo notevole. Per risolvere questo
problema si utilizza l’algoritmo di Zobrist. Tale algoritmo consiste nel generare stringhe di
bit casuali ognuna associata a una particolare combinazione posizione-pezzo (hash-key).
19
Ulteriori stringhe servono per l’en passant e l’arrocco. La hash-key viene ottenuta
effettuando un’operazione di XOR tra i valori casuali che appartengono alla combinazione
posizione-pezzo e alla scacchiera. Essa può essere facilmente aggiornata effettuando una
XOR con il valore della combinazione posizione-pezzo di origine e un’altra operazione di
XOR con il valore della combinazione di destinazione.
L’inconveniente con le hash-key è quello di avere delle collisioni sebbene con una
probabilità molto bassa. Una collisione avviene quando almeno due posizioni sono
mappate su una stessa hash-key.
Di seguito viene riportato un esempio di pseudocodice relativo all’usa dell’algoritmo di
zobrist negli scacchi.
//costanti
white_pawn := 1
white_rook := 2
// etc.
black_king := 12
function init_zobrist():
//la tavola viene riempita con valori casuali
table := matrice di dimensione 64×12
for i from 1 to 64:
for j from 1 to 12:
//loop sulla scacchiera
//loop sui pezzi
table[i][j] = random_bitstring()
function hash(board):
h := 0
for i from 1 to 64:
if board[i] != empty:
j := pezzo contenuto in board[i]
20
h := h XOR table[i][j]
return h
2.1.4 Quiescence search
Un problema che affligge l’intelligenza artificiale in numerosi giochi tra cui gli scacchi, è
l’horizon effect. Questo problema è dovuto alla limitazione della profondità di analisi
negli algoritmi di ricerca. Poiché solo una parte dell’albero è stata analizzata, per il
sistema può sembrare che un determinato evento possa essere evitato quando in effetti ciò
è impossibile. Ad esempio, l’algoritmo rileva che può catturare un pedone con la regina,
tuttavia a casa della limitazione sulla profondità non riesce a vedere che il pedone può
essere protetto da un altro pedone, che può catturare a sua volta la regina.
La quiescence search cerca di evitare l’insorgere di tale problema. Essa consiste
nell’effettuare una ricerca a maggiore profondità a partire da quei nodi che non sono
“calmi” in modo da rivelare ed evitare trappole nascoste o mosse particolarmente
svantaggiose. La definizione di nodi calmi o turbolenti, tuttavia, non è ben definita e
dipende dalla particolare implementazione.
21
22
Capitolo 3: Da software a hardware
3.1 Micro-Max
Il programma di riferimento utilizzato è Micro-max [16]. Segue una panoramica del software.
3.1.1 Rappresentazione pezzi
I pezzi sono rappresentati con 6 bit. I 3 bit meno significativi sono utilizzati per codificare
il tipo di pezzo in accordo alla seguente rappresentazione 1=P+, 2=P-, 3=C, 4=R, 5=A,
6=T, 7=D, lo 0 indica una casa vuota, mentre i bit 3 e 4 rappresentano il colore del pezzo.
Si usa la combinazione ‘01’ per il bianco e ‘10’ per il nero. Il bit più significativo (il bit 5)
vale ‘1’, se il pezzo non è stato mosso dalla sua posizione di partenza, ‘0’ se invece, è stato
mosso.
Di seguito una rappresentazione grafica.
3.1.2 Rappresentazione tavola
La scacchiera viene rappresentata con un vettore di 129 byte, in accordo col sistema
“0x88”, di cui 64 byte sono usati per contenerne i pezzi. Un byte è usato come dummy e i
restanti 64 sono vuoti. Se tale vettore viene visto come la linearizzazione di una matrice
8x16, la scacchiera si colloca nelle prime 8 colonne. In questo modo i 4 bit meno
significativi, del numero della casella, identificano la colonna, i 4 più significativi la riga.
Pertanto, utilizzando la maschera 0x88, si può sapere se una posizione appartiene o meno
alla scacchiera.
23
3.1.3 Variabili globali e macro
Le variabili globali utilizzate sono le seguenti:
V e M sono usate come
maschere per verificare la
posizione dei pezzi. S è usato
come dummy. INF è il
valore
infinito
usato
nell’algoritmo minimax. C
viene usata per effettuare la
conversione ASCII-posizione
sulla scacchiera. RootEval è
utilizzato per il punteggio differenziale aggiornato. Nodes indica il numero di nodi
esplorati. i serve a conservare temporaneamente la valutazione di alcuni termini. Rootep è
usato per contenere l’en passant flag per la prossima mossa. InputFrom e InputTo sono
le case di partenza e arrivo.
Le macro sono utilizzate per la creazione delle chiavi di hash.
3.1.4 Descrizione del software
La funzione che realizza l’intelligenza artificiale del programma può essere divisa in tre
parti: una per il Zobrist hashing, una per il generatore di mosse e l’ultima costituita
dall’algoritmo di ricerca.
La firma di tale funzione è:
I cui parametri sono:
24
Side per identificare il giocatore bianco o nero. Alpha, Beta e Depth usati per l’algoritmo
minimax con alpha-beta pruning. Eval è usato per la valutazione corrente. HashKeyLo e
HashKeyHi usati per la tavola di hash. epSqr utilizzato per passare la casa dove può
essere effettuato l’en passant oppure l’arrocco. LastTo contiene la casella di arrivo della
mossa precedente, nella prima chiamata, tale parametro può assumere valore 8 o 9 a
seconda che la funzione sia stata invocata per effettuare una mossa o controllarne la
validità.
Le variabili locali utilizzate sono:
J è una variabile contatore per il secondo ciclo del generatore. StepVec direzione
prelevata dal vettore delle direzioni. BestScore valore del punteggio migliore. Score usato
per contenere temporaneamente alcuni termini di valutazione IterDepth contatore
utilizzato per l’iterative deepening e indica la profondità. h usata per calcolare la
profondità rimanente. i (vedi var. globali) inizializzato a 8 per scorrere la tavola di hash.
SkipSqr indica la casella saltata nel caso di doppia mossa del pedone o re. RookSqr
indica la casella della torre se viene eseguito l’arrocco altrimenti contiene il valore dummy
S. victim pezzo candidato alla cattura. PieceType contiene il tipo di pezzo che è usato per
la mossa. Piece pezzo che sta eseguendo la mossa (a differenza di PieceType comprende
anche il colore e il bit virgin). FromSqr casella d’origine della mossa attuale. ToSqr
casella di destinazione della mossa attuale. BestFrom casella di origine della mossa
migliore. BestTo casella di destinazione di mossa migliore qualora il bit di valore 8 sia
settato, se è alzato il bit S la mossa non è un arrocco. CaptSqr casa in cui è situato il
pezzo victim. Nel caso di en passant viene settato il bit di valore 16. StartSqr casella di
partenza per il generatore.
Il generatore è stato implementato tramite tre cicli innestati. Il più esterno scansiona la
scacchiera per trovare i pezzi che appartengono al giocatore di turno. Quando viene
rinvenuto un pezzo, il secondo ciclo effettua una ricerca per trovare la direzione valida
lungo la quale il pezzo si può muovere. L’ultimo ciclo verifica se lungo la direzione
considerata, il pezzo trova una casa vuota oppure occupata da un altro pezzo
25
nemico/amico, nell’ultimo caso, o quando la mossa è illegale, il questo si interrompe.
L’algoritmo di ricerca usato è il minimax con alpha-beta pruning e iterative deepening.
Per rimediare ai problemi dovuti all’horizon effect, viene utilizzata una quiescence search.
26
All’interno dell’algoritmo si determina anche il valore della mossa, usando una funzione
composta da una parte statica, che dipende dal valore del pezzo da catturare e da una parte
transitoria, alla quale contribuiscono la posizione dei pezzi, l’en passant, l’arrocco e la
disposizione dei pedoni.
27
La tavola di hash viene impiegata per conservare le mosse migliori e aumentare la
velocità della ricerca.
Il main ha il compito di inizializzare la scacchiera e la tavola di hash, di convertire la
mossa se inserita dall’utente, stampare a video lo stato della scacchiera e di richiamare la
funzione Search. In particolare, se la mossa non viene inserita, il programma invoca la
funzione di ricerca due volte. La prima con LastTo=8 e Depth=0 (serve a trovare la mossa
migliore e a settare le variabili globali InputFrom e InputTo, qualora l’utente non avesse
inserito alcuna mossa). La seconda volta che viene invocata, i parametri LastTo e Depth
vengono settati rispettivamente a 9 e 2 in modo da verificare la legalità della mossa, in
caso affermativo, questa viene effettuata.
Descrizione Hardware
In questo elaborato si fornirà solamente una descrizione di alto livello per quanto riguarda
l’implementazione hardware di un videogioco. Lo schema presentato in figura 7 può
essere utilizzato, effettuando opportune modifiche, in differenti applicazioni. Con
riferimento al gioco degli scacchi e in particolare al software Micro-Max, esso si compone
di tre blocchi fondamentali: il Controllore, il Datapath e il Controller RAM.
Il blocco Controllore è la realizzazione di una macchina a stati finiti, il cui compito è
quello di verificare che il flusso logico del programma sia eseguito correttamente. Esso
riceve in ingresso degli opportuni segnali di stato provenienti dal Datapath, che vengono
elaborati da un circuito fornendo in uscita dei segnali di controllo che abiliteranno o meno
differenti percorsi all’interno del Datapath.
Per aumentare l’efficienza della ricerca, in Micro-Max, viene utilizzata una tavola di hash.
28
Poiché la sua dimensione è dell’ordine dei MegaByte, quantità eccessiva per una singola
FPGA, bisogna prevedere l’utilizzo di una memoria esterna abbastanza capiente da
ospitarla. Per questo motivo, vi è la necessità di introdurre il controller RAM che ha il
compito di interfacciarsi con la memoria esterna fornendo le funzionalità di prelievo e
inserimento dei dati provenienti o destinati al datapath.
Figura 7: Schema di alto livello.
Il daptapath rappresenta il centro di calcolo del sistema. Esso è costituito da tre elementi:
i dati, gli operatori e i collegamenti. A seconda dei segnali di controllo provenienti dal
controllore, vengono abilitati differenti percorsi tra i dati e le operazioni. Inoltre, dal
datapath provengono anche i segnali di stato che consentono al controllore di poter
verificare l’esecuzione del programma.
In Figura viene riportato il datapath relativo alla funzione di ricerca utilizzata in MicroMax. Per ottenere il circuito, si è partiti con l’analizzare il codice, e ad ogni operazione
effettuata sui dati è stata assegnata una funzione logica. Successivamente sono stati uniti
tutti i circuiti ottenendo la rete finale.
Per poter implementare su FPGA tale datapath si è stimato l’uso di circa 3500 LTU, 232
Flip-Flop per le variabili globali, 550 Flip-Flop per le variabili locali, 8 locazioni da 32 bit,
29
32 locazioni da 8 bit e 129 locazioni da 8 bit, situate in BRAM differenti per collocare
rispettivamente il vettore PieceVal, il vettore StepVecs e il vettore Board.
Si noti come questa struttura è stata realizzata considerando solo un’esecuzione iterativa e
non ricorsiva della funzione, in quanto la traduzione in hardware di algoritmi ricorsivi è un
problema di non semplice risoluzione. Per realizzare una chiamata ricorsiva si possono
utilizzare due soluzioni differenti a seconda delle performance offerte dalla FPGA. Una
possibile soluzione alla ricorsione può essere quella di replicare n volte il datapath, ciò
permetterebbe di poter separare fisicamente i livelli delle chiamate, inoltre si potrebbe
sfruttare la capacità, offerta dalle FPGA, di effettuare calcoli in parallelo in modo da
esplorare contemporaneamente un certo numero di nodi figli di uno stesso genitore. Così
facendo i tempi di ricerca possono essere ulteriormente migliorati. Questa soluzione
tuttavia presenta l’inconveniente di essere particolarmente onerosa dal punto di vista del
numero di componenti utilizzati. Una seconda soluzione meno dispendiosa, ma che non
sfrutta appieno le potenzialità offerte dal parallelismo, consiste invece di replicare
solamente il blocco relativo alle variabili locali lasciando un’unica sezione che si occupa
di effettuare i calcoli.
Le considerazioni fino ad ora fatte, sono principalmente concetti di alto livello che
richiederebbero una fase di progettazione molto lunga. Ulteriori sviluppi che si potrebbero
fare a partire da questo elaborato, sono degli approfondimenti riguardo alla progettazione
di una macchina a stati finiti che realizzi il comportamento del programma e di una rete
che effettui l’interfacciamento con la RAM esterna. Si potrebbe inoltre effettuare la
realizzazione di un blocco di comunicazione che permetta di inserire e visualizzare le
mosse effettuate. Infine una volta ottenuto un progetto abbastanza dettagliato, la fase
finale sarebbe quella di implementazione su una FPGA reale analizzandone le prestazioni.
30
Figura 8: Datapath. In rosso sono evidenziati i dati, in blu le operazioni.
31
32
Conclusioni
Sebbene questa sia stata una sfida, per me, di sicuro è stata una grande esperienza di studio
stimolante che ha arricchito il mio bagaglio culturale e che mi ha permesso di
approfondire la conoscenza della tecnologia FPGA e degli algoritmi di intelligenza
artificiale applicati in ambito gaming. Spero, infine, che questo elaborato possa offrire
spunti interessanti per ulteriori approfondimenti, a coloro che ne siano stati incuriositi.
33
Bibliografia
[1]
Introduction to FPGA Design with Vivado High-Level Synthesis.
[2]
www.xilinx.com, http://www.xilinx.com/fpga/asic.htm.
[3]
www.xilinx.com, http://www.xilinx.com/fpga/.
[4]
www.xilinx.com, http://it.wikipedia.org/wiki/Field_Programmable_Gate_Array.
[5]
Rodolfo Zunino, Introduzione ai dispositivi FPGA.
[6]
Farooq, Umer, Marrakchi, Zied, Mehrez, Habib, Tree-based Heterogeneous FPGA
Architectures, Chapter 2 FPGA Architectures: An Overview.
[7]
en.wikipedia.org, http://en.wikipedia.org/wiki/Minimax.
[8]
en.wikipedia.org, http://en.wikipedia.org/wiki/Negamax.
[9]
en.wikipedia.org, http://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning.
[10] en.wikipedia.org, http://en.wikipedia.org/wiki/Transposition_table.
[11] en.wikipedia.org, http://en.wikipedia.org/wiki/Zobrist_hashing.
[12] en.wikipedia.org, http://en.wikipedia.org/wiki/Quiescence_search.
[13] Marc Boulé, An FPGA Move Generator for the Game of Chess.
[14] Caude E. Shannon, Programming a Computer for Playing Chess.
[15] Warren Miller, A Chess Playing FPGA.
[16] home.hccnet.nl/h.g.muller, http://home.hccnet.nl/h.g.muller/max-src2.html.
[17] Spartan-3 Generation FPGA User Guide.
[18] Using Block RAM in Spartan-3 Generation FPGAs.
34