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