Attenzione ai lavandini e dove nascondere i vostri oggetti una guida visuale per l’implementazione dello streaming LLM

Attenzione agli oggetti di bellezza e dove nasconderli una guida visuale per il perfetto look

Blocco generatore di testo GPT-2 Rolling Cache

Uno degli ultimi articoli sull’intelligenza artificiale che sta facendo parlare di sé è una tecnica per le architetture dei modelli Generative Pre-training Transformer (GPT) che consentono finestre di contesto illimitate e efficienti per la generazione di testi. Questo è reso possibile sfruttando una scoperta sugli “attention sinks”, ovvero che i token più precoci nella previsione del prossimo token svolgono la maggior parte del lavoro per l’auto-attenzione nella costruzione di una rappresentazione del testo. È molto pratico perché non richiede un raffinamento fine e richiede solo modifiche minime all’architettura di GPT. Questo post si concentra sulle modifiche a un livello dettagliato in modo che tu possa sentirti sicuro di sapere come metterlo in pratica.

Per ricordare l’importanza di questa tecnica, un modello LLM “vanilla” richiede una quantità di memoria e tempo di elaborazione esponenzialmente maggiore all’aumentare della lunghezza del contesto per generare il prossimo token. Inoltre, molti modelli non sono effettivamente addestrati su input molto lunghi, quindi ne risentono man mano che gli input si allungano. Ogni volta che il modello genera il prossimo token, la finestra si allunga. Immagina GPT che scrive la fine di un libro. Per capire tutto ciò che ha scritto, il modello deve mantenere una finestra di contesto molto lunga, altrimenti la conclusione del libro non racconterebbe tutti i dettagli della trama.

L’articolo:

https://arxiv.org/pdf/2309.17453v1.pdf

Il resto di questo post si concentra sulla tecnica effettiva, non sulla sua giustificazione o sui risultati ottenuti. Il testo effettivo dell’articolo sulla tecnica è relativamente ridotto. Fondamentalmente, si scelgono alcuni “attention sinks” e si ha una coda di embedding di token dopo l’attention sink che ha una dimensione fissa. Ad ogni iterazione, quando si genera un embedding del prossimo token, si mantengono gli embedding dell’attention sink e si eliminano solo gli embedding per i token alla fine della coda.

Ecco un esempio pratico per il testo che inizia con

Hmm, okay quindi questo è un input

E supponiamo di avere 3 attention sinks e una lunghezza massima dei token di soli 7. Inizialmente, attraversiamo tutti i token nei layer per produrre 7 embedding di token, e andiamo a generare l’ottavo token. Supponiamo che il prossimo token che produca sia “testo”, e in grassetto è il token che elimineremo successivamente.

[Hmm, okay, quindi, questo, è, un, input] → “testo”

Quindi, nella prossima iterazione, scorreremo la coda indietro ed elimineremo il token che compare il prima possibile dopo l’attention sink.

[Hmm, okay, quindi, è, un, input, testo] → “e”

E continueremmo a fare questo fino alla fine.

[Hmm, okay, quindi, un, input, testo, e] → “questo”

L’altro elemento da tenere presente è che gli embedding di posizione non vengono scorrimenti in avanti, ma rimangono gli stessi. Ciò significa che l’embedding di posizione associato al token cambia ad ogni iterazione.

Dettagli di visualizzazione

I passaggi di calcolo verranno mostrati visivamente utilizzando uno strumento di visualizzazione di grafi a nodi. Ogni blocco è un’operazione che prende gli input sul lato sinistro e produce i dati per le variabili di output sul lato destro. I collegamenti indicano il passaggio dei dati dagli output agli input, e i cerchi sugli input significano che i dati sono specificati sul posto e sono statici.

Le operazioni sono composte da un’icona “unbox”, che quindi si scompone in un sottografo i cui input sono gli input del genitore e i cui output sono gli output del genitore, oppure sono primitive, il che significa che non possono essere ulteriormente scomposte e corrispondono a operazioni di tensori a basso livello come quelli da NumPy o TensorFlow. I colori indicano il tipo di dato e i pattern indicano la forma dei dati. Il blu indica che il tipo di dato è un intero, mentre il viola/rosa indica che si tratta di un tipo di dato decimale, e il verde indica che si tratta di testo. I collegamenti solidi indicano che la forma dei dati è scalare, mentre i puntini nel collegamento indicano il numero di dimensioni dell’array (il numero di puntini tra i trattini). In fondo a ogni grafico c’è una tabella che caratterizza la forma, il tipo e il nome dell’operazione di ogni variabile che trasporta dati nel modello.

Ho già affrontato e utilizzato la visualizzazione in post precedenti, come la creazione di una mappa di riferimento per GPT completamente visualizzato e BERT completamente visualizzato, e per guide visive su Graph Attention Networks, il metodo di messa a punto di LoRA, e BERTScore.

Implementazione Visualizzata

Andiamo nell’implementazione. Quello che stiamo vedendo di seguito è l’inizio di un loop. Ad ogni iterazione, abbiamo il prossimo token e i token concatenati finora. Passiamo solo il nuovo token successivo a GPT perché leggeremo e scriveremo le embeddings in cache su ogni livello. Questo è un trucco di implementazione che lo rende più efficiente in modo che ad ogni nuova iterazione sia necessario solo recuperare le embeddings per l’ultimo token.

Prima di proseguire, esaminiamo alcuni iperparametri (variabili globali) del modello. Le costanti globali sono valori statici nel tuo grafico.

Leggeremo dal database pubblico contenente i pesi di GPT-2 e li memorizzeremo nella directory specificata “gpt_2_rolling_cache”. Questi percorsi cache vengono utilizzati per memorizzare i parametri di ogni peso e funzione, come i parametri del modello che sono in memoria.

Puoi vedere che impostiamo il numero di attenzioni da considerare su 3 token e il numero massimo di token su 7. Ciò significa che limiteremo il modello dal processare più di 7 token alla volta, che è piuttosto breve, ma è solo un esempio. Di solito, questo numero corrisponderebbe alle lunghezze di contesto originali utilizzate nell’addestramento, che per questo piccolo modello GPT-2 è di 32. Ogni volta che elaboriamo il prossimo token, elimineremo il token più vecchio che abbiamo memorizzato dopo le attenzioni e in totale osserveremo solo le 3 attenzioni più le ultime 4 token ad ogni iterazione.

Ma quando diciamo “osservare i token”, cosa significa veramente? Analizziamo i livelli. Guardando solo il livello 0, puoi seguire le indicazioni per vedere dove siamo all’interno dell’architettura. Qui stiamo recuperando i livelli di proiezione densa per i pesi di Key e Value.

All’interno della Key Rolling Cache, stiamo leggendo i pesi dalla cache. Nota che siamo in un blocco condizionale, quindi nella prima iterazione scriveremo solo nella cache senza leggere. La cache include le embeddings del token nell’iterazione precedente. Le embeddings hanno forma [1, 12, 7, 64].

  • La dimensione 0 è per la dimensione del batch (1),
  • La dimensione 1 è per il numero di attenzioni (12),
  • La dimensione 2 è per il numero di token (7),
  • La dimensione 3 è per la dimensione nascosta (768) divisa per il numero di attenzioni (64).
Lettura dalla cache su ogni livello

Il link in ingresso attorno al file di lettura è solo per il/i token in ingresso. Nella prima iterazione del loop dell’esempio, è [1, 12, 7, 64], e successivamente sarà eseguito solo per il nuovo token, che è [1, 12, 1, 64]. La prima cosa che faremo è separare le attenzioni (sulla dimensione 2) e quindi concatenare la nuova embedding lungo l’asse della dimensione 2. I pesi dell’attenzione si spostano in avanti per essere concatenati. All’interno del blocco di evict, rimuoveremo 1 o più token dalla fine della coda.

Rolling cache logic

All’interno del blocco di evacuazione, puoi vedere che calcoliamo quante token embeddings tagliare (cioè evacuare, sì, evacuare suona meglio) all’inizio della dimensione 2. In generale, ogni nuovo token provoca l’evacuazione di 1 token.

Evacuare

Infine, prendiamo il risultato e lo concateniamo con le embeddings di attenzione che precedono e passiamo avanti. Facciamo lo stesso per i pesi di chiave e valore per ogni livello quando recuperiamo i livelli di Query Key Value all’interno dell’operazione di Self Attention.

Recupero delle embeddings di Query Key Value

Infine, l’unico lavoro che resta è la codifica della posizione. All’interno del blocco “Creazione degli ID di posizione”, possiamo aggiornare la logica delle posizioni delle embeddings. La logica è relativamente semplice. Incrementiamo l’embedding di posizione per il prossimo token se non abbiamo ancora raggiunto la lunghezza del token, altrimenti le manteniamo uguali e recuperiamo le stesse embeddings di posizione.

Creazione degli ID di posizione

Per esempio, sto confrontando il GPT-2 senza la cache rotante e con la cache rotante per la generazione di 20 token, a partire dall’esempio che ho dato in precedenza “Hmm ok, questo è un po’ di testo in input”. Questo è ancora breve e richiede a malapena la cache rotante, ma dimostra che funziona.

GPT2 senza cache rotante:

GPT2 con cache rotante (massimo 7 token e 3 embedding di attenzione):

Sono diverse, come previsto, ma una è migliore dell’altra?

Grazie per la lettura! Il grafico completo è un JSON disponibile su Github. Cosa ne pensi? Ho commesso degli errori? Desideri vedere qualcos’altro? Fammi sapere nei commenti!