Perché abbiamo ancora bisogno della CPU se le GPU sono più veloci?

Analizziamo il ruolo fondamentale di CPU e GPU nei moderni sistemi informatici, evidenziando le differenze nelle loro architetture e capacità di calcolo. Se le GPU sono diventate super potenti, che senso ha mantenere i core della CPU?

Indipendentemente dal dispositivo utilizzato, che sia un PC desktop o un moderno smartphone, il sistema poggia sempre sull’utilizzo dell’accoppiata CPU (Central Processing Unit) più GPU. Le GPU (Graphics Processing Unit) sono nate alla fine degli anni ’70 e hanno iniziato a guadagnare popolarità negli anni ’80. Inizialmente, il loro scopo principale consisteva nel migliorare la visualizzazione grafica, gestendo i calcoli necessari per il rendering delle immagini. Dagli anni 2000 in poi, le GPU sono diventate sempre più potenti e versatili, andando ben oltre i compiti grafici: sono diventate particolarmente abili nelle attività di computing generico e nel supporto multimediale.

Utilizzando tecnologie come CUDA (NVIDIA) e OpenCL, le GPU sono ora ampiamente utilizzate per calcoli scientifici, AI, machine learning e data analysis. Svolgono inoltre un ruolo cruciale per l’accelerazione video, il rendering in tempo reale e il supporto di applicazioni VR/AR.

Differenze tra CPU e GPU: potenza di calcolo

Le prestazioni dei chip moderni sono spesso misurate in TFLOPS (Tera Floating Point Operations Per Second). Si tratta di un valore che esprime la capacità di calcolo di un processore, in particolare quando si tratta di operazioni matematiche sui numeri in virgola mobile (floating point).

Le operazioni in virgola mobile sono fondamentali in molte applicazioni ad alta intensità computazionale: grafica 3D e rendering, intelligenza artificiale e machine learning, calcoli scientifici e simulazioni, modellazione fisica e animazioni. La rappresentazione in virgola mobile permette di gestire numeri molto grandi o molto piccoli con precisione.

Il numero di TFLOPS di un processore si calcola con la seguente formula:

TFLOPS = Core × Frequenza di clock × Numero operazioni per ciclo di clock

Posto che 1 TFLOP equivale a un trilione di operazioni in virgola mobile al secondo, in Italia e in Europa (territori in cui si usa la cosiddetta “scala lunga“) lo stesso valore corrisponde a 1.000 miliardi (1012) di operazioni in virgola mobile al secondo.

In termini di potenza di calcolo, una CPU Intel a 24 core fa registrare 0,33 TFLOPS mentre con una GPU NVIDIA 100, specializzata in AI, simulazioni scientifiche e applicazioni di calcolo avanzato, arriva a ben 9,7 TFLOPS. I valori dei TFLOPS dipendono dalla precisione numerica utilizzata (FP32, FP64, TensorCore,…). Nell’esempio, i valori indicati sono espressi in Double Precision (DP), ovvero con calcoli a doppia precisione (FP64, Floating Point a 64 bit).

Una GPU di oggi può rivelarsi quindi almeno 30 volte più veloce rispetto a una CPU avanzata. E allora perché non eliminare del tutto le CPU, visto che sono così “lente”?

Perché non eliminare le CPU e usare solo GPU? La risposta risiede nella natura dei programmi

Quando un programma è in esecuzione, il sistema operativo e i driver delle GPU collaborano per distribuire il carico di lavoro. Quando un’applicazione richiede calcoli complessi, il sistema può automaticamente trasferire i compiti alla GPU, quando ritenuto opportuno. Ovviamente spetta anche agli sviluppatori e ai compilatori ottimizzare il software in maniera tale che, quando possibile, possa eventualmente trarre vantaggio dalle migliori prestazioni della GPU.

Questa considerazione sposta l’attenzione sulla struttura del programma da eseguire sul sistema informatico. I programmi che hanno un’architettura sequenziale, infatti, prevedono che le istruzioni siano eseguite una dopo l’altra. Quando ogni passo dipende dal risultato del precedente, è impossibile suddividere il lavoro tra più processori.

Viceversa, i programmi paralleli permettono l’esecuzione simultanea delle operazioni previste perché esse non dipendono l’una dall’altra. Le operazioni indipendenti possono essere distribuite su più core, accelerando l’elaborazione.

Esempio di programma sequenziale

Il seguente programma Python calcola la somma di una serie di numeri. Ogni numero successivo dipende da un’operazione basata sul risultato precedente e da una condizione.

import random

def sequenziale_complesso(n):
    risultato = 0
    for i in range(n):
        if risultato % 2 == 0:  # Controllo condizionale
            valore = random.randint(1, 10) * 2
        else:
            valore = random.randint(1, 10) + 5
        risultato += valore  # Ogni passaggio dipende dal precedente
        print(f"Step {i + 1}: Risultato parziale = {risultato}")
    return risultato

print(f"Risultato finale del programma sequenziale: {sequenziale_complesso(10)}")

Com’è evidente, ogni calcolo dipende dal risultato precedente e da una condizione che lo modifica dinamicamente. Per questi motivi, non è parallelizzabile a causa delle dipendenze.

Esempio di programma parallelizzabile

Nel programma Python che segue, calcoliamo in parallelo il quadrato di numeri non reciprocamente dipendenti e poi ne sommiamo i risultati.

from concurrent.futures import ProcessPoolExecutor
import math

def calcolo_parallelo_complesso(n):
    return n ** 2  # Calcolo indipendente: il quadrato del numero

def parallelo_complesso(numeri):
    with ProcessPoolExecutor() as executor:
        quadrati = list(executor.map(calcolo_parallelo_complesso, numeri))
    
    somma_totale = sum(quadrati)  # Aggregazione dei risultati
    return quadrati, somma_totale

# Esecuzione
numeri = list(range(1, 11))  # Numeri da 1 a 10
quadrati, somma_totale = parallelo_complesso(numeri)
print(f"Quadrati calcolati in parallelo: {quadrati}")
print(f"Somma totale dei quadrati: {somma_totale}")

Una falsa dicotomia

“Nel mondo reale”, la maggior parte dei programmi non sono immediatamente riconducibili al primo o al secondo insieme. Molti dei programmi sono infatti parzialmente parallelizzabili.

Nell’esempio seguente, una parte del programma Python è parallelizzabile mentre l’altra non lo è (sequenziale):

import random

# Funzione sequenziale: calcola il totale di alcuni numeri
def calcolo_totale():
    totale = 0
    for _ in range(5):
        totale += random.randint(1, 10)
    return totale

# Funzione parallela: applica una formula a ogni numero
def applica_formula(totale):
    numeri = [random.randint(1, 10) for _ in range(10)]
    risultati = []

    # Questa parte può essere eseguita in parallelo
    for numero in numeri:
        risultati.append(numero * totale)

    return risultati

# Funzione principale
def programma_parzialmente_parallelizzabile():
    totale = calcolo_totale()  # Parte sequenziale
    risultati = applica_formula(totale)  # Parte parallela
    return totale, risultati

# Esegui il programma
totale, risultati = programma_parzialmente_parallelizzabile()
print(f"Totale (calcolato in sequenza): {totale}")
print(f"Risultati (calcolati in parallelo): {risultati}")

Differenze architetturali: CPU vs GPU

Le CPU utilizzano pochi core (potenti) che sono chiamati a gestire elaborazioni sequenziali e gestire decisioni complesse. Di contro, le GPU poggiano il loro funzionamento su migliaia di core semplici (ad esempio una NVIDA H100 consta di migliaia di core), eccellenti per svolgere operazioni in parallelo.

Nel rendering di un videogioco, ogni pixel può essere ricalcolato indipendentemente dagli altri, permettendo alla GPU di processarli simultaneamente. I programmi legati all’intelligenza artificiale e al machine learning sono a loro volta altamente parallelizzati.

Le CPU eccellono nella gestione di eventi casuali e processi complessi, come:

  • Gestione di richieste imprevedibili del sistema (avvio di app, connessioni di rete,…).
  • Adattamento rapido a situazioni inattese (ad esempio, collegamento di un nuovo dispositivo USB).
  • Coordinamento delle risorse per mantenere il sistema reattivo.

Il core della CPU è un direttore di orchestra che coordina il lavoro nel suo complesso mentre i core GPU sono specialisti chiamati ad eseguire compiti molto ripetitivi.

Perché le applicazioni AI sono altamente parallelizzate?

I programmi legati all’intelligenza artificiale e al machine learning sono altamente parallelizzabili e, di conseguenza, traggono enorme vantaggio dall’esecuzione su GPU. Ciò è dovuto alla struttura stessa delle operazioni matematiche e delle attività computazionali che caratterizzano queste applicazioni.

La fase di addestramento dei modelli, ad esempio, comporta un’enorme quantità di calcoli matematici, molti dei quali sono indipendenti l’uno dall’altro e quindi facilmente parallelizzabili.

I modelli sono composti da milioni di pesi che devono essere calcolati per ogni dato (per esempio, un’immagine o un testo). Ogni peso è calcolato separatamente, quindi possiamo distribuire il lavoro su tanti core della GPU, che possono a loro volta eseguire i calcoli tutti insieme. Si immagini ad esempio di dover fare la stessa somma per 100 numeri. Invece di eseguirla uno per uno, è possibile farla per tutti e 100 i numeri contemporaneamente.

Nel deep learning, si utilizzano matrici (tabelle di numeri) che vengono moltiplicate per calcolare le risposte del modello. Questo processo ripetitivo è eseguito un numero enorme di volte: possiamo quindi parallelizzare le operazioni, facendole su più dati contemporaneamente.

Conclusioni

Sebbene le GPU siano indubbiamente più veloci nel trattamento di operazioni in parallelo, come nel caso della grafica in ambito gaming e nella gestione di grandi matrici, le CPU si confermano insostituibili per la loro capacità di gestire programmi sequenziali complessi ed eventi imprevisti.

I processori moderni sfruttano questa sinergia, combinando la potenza di calcolo della GPU con la versatilità della CPU, per ottimizzare il rendimento generale. Quindi, nonostante i vantaggi specifici delle GPU, le CPU conservano un ruolo fondamentale nell’architettura dei sistemi informatici, gestendo la logica, l’adattamento a vari scenari e la supervisione del funzionamento complessivo del dispositivo.

Ci torna in mente il video pubblicato da NVIDIA ormai 15 anni fa. Seppur suggestiva, quella fu una dimostrazione un po’ troppo semplicistica di come stanno realmente le cose.

Credit immagine in apertura: iStock.com – MF3d

Ti consigliamo anche

Link copiato negli appunti