Per imparare come funziona una CPU, dall'architettura ai segnali di controllo, non si contano le risorse disponibili online. Nel caso delle GPU, invece, il panorama è completamente diverso.
La documentazione per imparare a programmare le GPU non manca di certo ma non c'è quasi nulla sul funzionamento delle unità di elaborazione grafica a livello hardware. Perché? Perché il mercato è estremamente agguerrito e i dettagli tecnici a basso livello per tutte le moderne architetture rimangono proprietari.
Il motivo del progetto tiny-gpu è proprio questo: accendere una luce nella nebbia e fornire un valido aiuto a tutti coloro che volessero cimentarsi nel lavoro di progettazione e creazione di una GPU.
Come creare una GPU con il progetto tiny-gpu
Costruita utilizzando meno di 15 file di Verilog completamente documentati, la GPU tiny-gpu è un'unità ridotta all'osso che comunque può contare su di una descrizione accurata dell'architettura e dell'ISA (Instruction Set Architecture), del codice implementato con successo per eseguire operazioni di addizione e moltiplicazione tra matrici su una GPU, dell'esecuzione dei kernel – ossia delle porzioni di codice eseguite sulla GPU in parallelo su diversi thread o core.
È inoltre possibile tracciare l'esecuzione dei kernel, osservando quali istruzioni sono eseguite, in che ordine e con quali dati. L'execution tracing fornisce un dettagliato resoconto di come la GPU esegue il codice, consentendo agli sviluppatori di analizzare il comportamento di ciascun programma, identificare eventuali errori o inefficienze e ottimizzare le prestazioni.
tiny-gpu è una implementazione minimale di una GPU ottimizzata: i produttori si concentrano da un lato su GPU general-purpose (GPGPU), dall'altro su acceleratori per il machine learning come le TPU Google. Gli autori del progetto tiny-gpu evidenziano i principi generali di queste architetture, piuttosto che sui dettagli dell'hardware specifico. L'idea è quella di concentrarsi sugli elementi fondamentali che sono cruciali per tutti i moderni acceleratori hardware.
Il progetto tiny-gpu prende per mano il lettore guidandolo alla scoperta di temi come l'architettura della GPU, la parallelizzazione (com'è implementato il modello SIMD, Single Instruction, Multiple Data, nell'hardware?) e la memoria (la GPU deve svolgere le sue attività rispettando i vincoli legati alla larghezza di banda della memoria).
La sezione finale più avanzata presenta alcune importanti ottimizzazioni applicate sulle GPU immesse sul mercato con il preciso obiettivo di massimizzarne le prestazioni.
L'immagine è tratta dal repository GitHub di tiny-gpu.
L'architettura di tiny-gpu
Verilog è un linguaggio di descrizione dell'hardware (HDL, Hardware Description Language) utilizzato principalmente per la progettazione e la simulazione di circuiti digitali. È ampiamente utilizzato nell'industria dell'elettronica digitale per progettare e verificare hardware, come circuiti integrati, sistemi a logica programmabile (FPGA) e ASIC (Application-Specific Integrated Circuit).
Nel caso di tiny-gpu con meno di 15 file Verilog si realizza una GPU pienamente funzionante in grado di eseguire un singolo kernel alla volta. La GPU in sé è composta da un registro di controllo del dispositivo, un dispatcher, un numero variabile di core di calcolo, un controller di memoria per la memoria dati e la memoria del programma, della cache.
Il registro di controllo memorizza dei metadati che contengono informazioni sulle modalità con cui i kernel dovrebbero essere eseguiti sulla GPU. Il dispatcher, invece, è l'unità che si occupa della distribuzione dei thread sui diversi core di calcolo.
La GPU è costruita per interfacciarsi con una memoria globale esterna. Nel caso di tiny-gpu, la memoria dati e la memoria del programma sono separate per semplicità.
La memoria dati utilizza uno spazio di indirizzamento a 8 bit (256 posizioni di memoria totali); i dati sono a 8 bit. La memoria del programma usa anch'essa uno spazio a 8 bit ma i dati sono conservati in blocchi da 16 bit (ogni istruzione è pari a 16 bit come specificato dall'ISA).
tiny-gpu sfrutta inoltre un controller di memoria che tiene traccia di tutte le richieste in uscita verso la memoria dai core di calcolo, modula le richieste in base alla larghezza di banda effettiva della memoria esterna e trasmette le risposte dalla memoria esterna alle risorse appropriate. È inoltre prevista la gestione della cache per liberare larghezza di banda e velocizzare le elaborazioni.
In questa GPU semplificata, ogni core elabora un blocco alla volta (gruppo di thread eseguiti in parallelo su un singolo core). Per ogni thread in ciascun blocco, il core ha un'ALU (arithmetic-logic unit), un LSU (load-store unit), un PC (program-counter) e registri dedicati.
Il ruolo dello scheduler
Ogni core di tiny-gpu uitlizza un singolo scheduler incaricato di gestire l'esecuzione dei thread. Lo scheduler esegue le istruzioni di un singolo blocco fino al completamento prima di passare a un nuovo blocco, ed esegue le istruzioni per tutti i thread in modo sincrono e sequenziale. Negli scheduler più avanzati, sono utilizzate tecniche come il pipelining al fine di massimizzare l'utilizzo delle risorse.
Il principale vincolo con cui lo scheduler deve lavorare è la latenza associata al caricamento e alla memorizzazione dei dati dalla memoria globale. Mentre la maggior parte delle istruzioni può essere eseguita in modo sincrono, queste operazioni di caricamento/archiviazione sono asincrone, il che significa che il resto dell'esecuzione delle istruzioni deve essere costruito intorno a questi lunghi tempi di attesa.
La ISA di tiny-gpu
Il progetto tiny-gpu implementa una semplice ISA di 11 istruzioni progettate per consentire l'uso di kernel semplici in grado di effettuare l'addizione e la moltiplicazione di matrici. Citiamo le istruzioni sulle quali si basa questa GPU minimale:
BRnzp
: Istruzione di salto per passare a un'altra riga in memoria del programma se il contenuto del registro NZP corrisponde alla condizionenzp
nell'istruzione.CMP
: Confronta il valore di due registri e memorizza il risultato nel registro NZP da utilizzare per una successiva istruzione BRnzp.ADD, SUB, MUL, DIV
: Operazioni aritmetiche di base per consentire elaborazioni tensoriali. Un tensore è un concetto matematico generale che generalizza vettori e matrici a più dimensioni. I tensori sono spesso utilizzati per rappresentare dati multidimensionali, come ad esempio immagini, video, dati sensoriali o matrici di dati in applicazioni di apprendimento automatico e di intelligenza artificiale.LDR
: Carica dati dalla memoria globale.STR
: Memorizza dati nella memoria globale.CONST
: Carica una costante in un registro.RET
: Segnala che il thread corrente ha raggiunto la fine dell'esecuzione.
Ogni registro utilizza da 4 bit: in totale 16 registri. I primi 13 registri R0 – R12 sono registri liberi che supportano lettura/scrittura. Gli ultimi 3 registri sono registri speciali di sola lettura.
Nel complesso, tiny-gpu rappresenta solo "un assaggio" del funzionamento di una GPU. Molti aspetti critici delle moderne unità, come il supporto per le istruzioni di intelligenza artificiale, la predizione del ramo più avanzata e l'implementazione della cache, mancano ancora all'appello nell'implementazione aperta appena condivisa pubblicamente.
Tuttavia, tiny-gpu mostra i principi fondamentali che consentono l'esecuzione parallela di kernel su una GPU e si rivela quindi un progetto preziosissimo per tutti coloro che vogliono capire davvero come funzionano le GPU partendo dal livello più basso. Le GPU sono complesse ma un'iniziativa come tiny-gpu fornisce basi solide da cui partire per comprendere la logica e l'architettura che rende così potenti questi dispositivi hardware.
Credit immagine in apertura: iStock.com – CasarsaGuru