Impostazione e Verifica di NVIDIA cuTile Python in Colab

In questo tutorial ci immergiamo nel mondo della programmazione GPU con NVIDIA cuTile Python, un'interfaccia avanzata per creare efficientemente kernal CUDA in Python. Iniziamo installando le dipendenze necessarie, verificando la disponibilità del GPU, del driver e del CUDA, per poi eseguire i primi test.

Per avviare il notebook in Google Colab, segui i seguenti passaggi per preparare l’ambiente:

    • Installa PyTorch, NumPy, Pandas, e Matplotlib con il comando:

pip install torch numpy pandas matplotlib

Procediamo quindi al tentativo di installazione di cuTile Python. Esso potrebbe non funzionare in alcuni runtime di Colab a causa di problemi di compatibilità del driver NVIDIA. Per verificare la corretta installazione, esegui il comando:

pip install "cuda-tile[tileiras]"

Successivamente, verifichiamo la presenza di PyTorch e se CUDA è disponibile eseguendo i seguenti comandi:

    • python --version
    • nvidia-smi

Se il runtime supporta correttamente cuTile, i comandi dovrebbero restituire informazioni dettagliate sulla GPU e su CUDA. Se invece mancano i requisiti minimi, il notebook continuerà utilizzando una versione alternativa basata su PyTorch.

Utilità per il Timing, la Convalida e la Segnalazione delle Prestazioni

Per garantire un esecuzione corretta, testabile e confrontabile dei nostri kernal su GPU, abbiamo definito alcune utility utili:

    • sync: una funzione che sincronizza l’esecuzione della GPU.
    • benchmark: una funzione che effettua dei test ripetuti per misurare il timing dell’esecuzione.
    • showresulttable: una funzione che presenta i risultati in forma tabulare.
    • assert_close: una funzione per verificare la correttezza delle operazioni confrontandole con l'output atteso.

Esempio di utilizzo di benchmark per un’operazione:

result = benchmark(fn, label="Addizione Matriciale CuTile")

showresulttable([result], "Risultato benchmark")

Creazione di Kernal CuTile per Addizione Vettoriale

I kernal per la GPU funzionano basandosi su operazioni eseguite su tile di dati. Qui mostriamo l'implementazione per l’addizione vettoriale di due array.

Ecco il codice per il kernal diretto:

@ct.kernel

    • Carica i due vettori a e b suddividendoli in tile di dimensioni TILE.
    • Esegue l’addizione vettoriale atile + btile nel modulo GPU.
    • Salva l’output in c.
    def cutilevecadddirectkernel(a, b, c, TILE: ConstInt):
    

    bid = ct.bid(0)

    a_tile = ct.load(a, index=(bid,), shape=(TILE,))

    b_tile = ct.load(b, index=(bid,), shape=(TILE,))

    ctile = atile + b_tile

    ct.store(c, index=(bid,), tile=c_tile)

    Una variante simile, detta gather-kernel, utilizza offset precompilati per accedere direttamente ai dati:

    def cutilevecaddgatherkernel(a, b, c, TILE: ConstInt):
    

    offsets = bid * TILE + ct.arange(TILE, dtype=torch.int32)

    a_tile = ct.gather(a, offsets)

    b_tile = ct.gather(b, offsets)

    ctile = atile + b_tile

    ct.scatter(c, offsets, c_tile)

    La seconda versione potrebbe essere preferita se i dati non sono accessibili in modo diretto in base alla tile.

    Kernal CuTile per L’Addizione Matriciale

    Per l’addizione matriciale in GPU, i kernal di cuTile gestiscono le dimensioni in due direzioni (M per righe e N per colonne).

    • Le righe sono determinate da bidm e sono suddivise in tiles di dimensioni TILEM.
    • Le colonne sono determinate da bidn e divise in tiles di dimensioni TILEN.
    • Con un offset calcolato da bidm * TILEM + ct.arange(TILE_M), accediamo direttamente ai dati.

Ecco il codice per l’addizione matriciale:

def cutilematrixaddgatherkernel(a, b, c, TILEM: ConstInt, TILEN: ConstInt):

bid_m = ct.bid(0)

bids_n = ct.bids(1)

rows = bidm * TILEM + ct.arange(TILE_M, dtype=torch.int32)

cols = bidn * TILEN + ct.arange(TILE_N, dtype=torch.int32)

rows = rows[:, None]

cols = cols[None, :]

a_tile = ct.gather(a, (rows, cols))

b_tile = ct.gather(b, (rows, cols))

ctile = atile + b_tile

ct.scatter(c, (rows, cols), c_tile)

Kernal CuTile per la Moltiplicazione Matriciale (MatMul)

La moltiplicazione di matrici è un caso complesso che richiede operazioni di accumulo su un terzo asse (K). Vediamo come il modello di tiled si adatta a questa operazione:

    • Utilizziamo un loop su k per accumulare i risultati di moltiplicazioni parziali.
    • I dati delle matrici A e B vengono caricati in tiles separate.
    • Utilizziamo ct.mma (Multiply-Accumulate) per eseguire l’operazione in parallelo.
    • L’output viene immagazzinato in C in modo ordinato.

Ecco il codice:

def cutilematmulkernel(A, B, C, TM: ConstInt, TN: ConstInt, TK: ConstInt):

bid_m = ct.bid(0)

bid_n = ct.bid(1)

numtilesk = ct.num_tiles(A, axis=1, shape=(TM, TK))

acc = ct.full((TM, TN), 0, dtype=ct.float32)

compute_dtype = ct.float32 if A.dtype == ct.float32 else A.dtype

zero_pad = ct.PaddingMode.ZERO

for k in range(numtilesk):

atile = ct.load(A, index=(bidm, k), shape=(TM, TK), paddingmode=zeropad).astype(compute_dtype)

b