Generazione Assistita una nuova direzione verso la generazione di testi a bassa latenza
'Generazione Assistita una nuova direzione per testi a bassa latenza.'
I modelli di linguaggio di grandi dimensioni sono di gran moda in questi giorni, con molte aziende che investono risorse significative per aumentarne la scala e sbloccare nuove capacità. Tuttavia, come esseri umani con una capacità di attenzione sempre più ridotta, non ci piace nemmeno la loro lenta velocità di risposta. La latenza è fondamentale per un’esperienza utente ottimale e spesso vengono utilizzati modelli più piccoli nonostante la loro qualità inferiore (ad esempio, nel completamento del codice).
Perché la generazione di testo è così lenta? Cosa impedisce di implementare modelli di linguaggio di grandi dimensioni a bassa latenza senza andare in bancarotta? In questo post del blog, riprenderemo i collo di bottiglia per la generazione di testo autoregressiva e introdurremo un nuovo metodo di decodifica per affrontare il problema della latenza. Vedrete che utilizzando il nostro nuovo metodo, la generazione assistita, potrete ridurre la latenza fino a 10 volte su hardware di base!
Comprensione della latenza nella generazione di testo
Il cuore della generazione moderna di testo è semplice da capire. Guardiamo il pezzo centrale, il modello di apprendimento automatico (ML). Il suo input contiene una sequenza di testo, che include il testo generato finora e eventualmente altri componenti specifici del modello (ad esempio, Whisper ha anche un input audio). Il modello prende l’input ed esegue un passaggio in avanti: l’input viene alimentato al modello e passato in sequenza lungo i suoi strati fino a quando vengono predette le log-probabilità non normalizzate per il token successivo (anche chiamate logits). Un token può consistere in parole intere, sotto-parole o anche singoli caratteri, a seconda del modello. L’illustrato GPT-2 è un ottimo riferimento se desideri approfondire questa parte della generazione di testo.
Un passaggio in avanti del modello ti fornisce i logits per il token successivo, che puoi manipolare liberamente (ad esempio, impostare la probabilità di parole o sequenze indesiderate a 0). Il passaggio successivo nella generazione di testo consiste nel selezionare il token successivo da questi logits. Le strategie comuni includono la scelta del token più probabile, nota come decodifica avida, o l’assunzione casuale dalla loro distribuzione, nota anche come campionamento multinomiale. Concatenando passaggi in avanti del modello con la selezione del token successivo, ottieni iterativamente la generazione di testo. Questa spiegazione è solo la punta dell’iceberg quando si tratta di metodi di decodifica; consulta il nostro post del blog sulla generazione di testo per una spiegazione approfondita.
- Presentando RWKV – Una RNN con i vantaggi di un transformer
- Esegui un Chatbot simile a Chatgpt su una singola GPU con ROCm
- Più piccolo è meglio Q8-Chat, un’efficace esperienza di intelligenza artificiale generativa su Xeon
Dalla descrizione sopra, il collo di bottiglia nella generazione di testo è chiaro: eseguire un passaggio in avanti del modello per modelli di grandi dimensioni è lento e potresti aver bisogno di farne centinaia in sequenza. Ma approfondiamo: perché i passaggi in avanti sono lenti? I passaggi in avanti sono tipicamente dominati da moltiplicazioni di matrici e, dopo una rapida visita alla relativa sezione di Wikipedia, puoi capire che la larghezza di banda della memoria è il limite in questa operazione (ad esempio, dalla RAM della GPU ai core di calcolo della GPU). In altre parole, il collo di bottiglia nel passaggio in avanti deriva dal caricamento dei pesi del livello del modello nei core di calcolo del dispositivo, non dall’esecuzione dei calcoli stessi.
Al momento, hai tre vie principali che puoi esplorare per ottenere il massimo dalla generazione di testo, tutte incentrate sulle prestazioni del passaggio in avanti del modello. In primo luogo, hai le ottimizzazioni specifiche dell’hardware del modello. Ad esempio, il tuo dispositivo potrebbe essere compatibile con Flash Attention, che accelera lo strato di attenzione attraverso un riordino delle operazioni, o la quantizzazione INT8, che riduce la dimensione dei pesi del modello.
In secondo luogo, quando sai di ricevere richieste di generazione di testo simultanee, puoi raggruppare gli input e aumentare enormemente la capacità di throughput con una piccola penalità di latenza. I pesi del livello del modello caricati nel dispositivo vengono ora utilizzati su diverse righe di input in parallelo, il che significa che otterrai più token per approssimativamente la stessa larghezza di banda della memoria. Il problema con il raggruppamento è che hai bisogno di memoria aggiuntiva del dispositivo (o di spostare la memoria da qualche parte) – alla fine di questo spettro, puoi vedere progetti come FlexGen che ottimizzano il throughput a scapito della latenza.
# Esempio che mostra l'impatto della generazione raggruppata. Dispositivo di misurazione: RTX3090
from transformers import AutoModelForCausalLM, AutoTokenizer
import time
tokenizer = AutoTokenizer.from_pretrained("distilgpt2")
model = AutoModelForCausalLM.from_pretrained("distilgpt2").to("cuda")
inputs = tokenizer(["Ciao mondo"], return_tensors="pt").to("cuda")
def print_tokens_per_second(batch_size):
new_tokens = 100
cumulative_time = 0
# riscaldamento
model.generate(
**inputs, do_sample=True, max_new_tokens=new_tokens, num_return_sequences=batch_size
)
for _ in range(10):
start = time.time()
model.generate(
**inputs, do_sample=True, max_new_tokens=new_tokens, num_return_sequences=batch_size
)
cumulative_time += time.time() - start
print(f"Token al secondo: {new_tokens * batch_size * 10 / cumulative_time:.1f}")
print_tokens_per_second(1) # Token al secondo: 418.3
print_tokens_per_second(64) # Token al secondo: 16266.2 (~39 volte più token al secondo)
Infine, se hai più dispositivi a disposizione, puoi distribuire il carico di lavoro utilizzando la parallelizzazione del tensore e ottenere una latenza inferiore. Con la parallelizzazione del tensore, si suddivide il carico della larghezza di banda della memoria su più dispositivi, ma ora bisogna considerare anche i collo di bottiglia della comunicazione tra dispositivi, oltre al costo monetario di eseguire più dispositivi. I benefici dipendono principalmente dalla dimensione del modello: i modelli che si adattano facilmente a un singolo dispositivo per consumatori vedono benefici molto limitati. Prendendo i risultati da questo post del blog DeepSpeed, si vede che si può distribuire un modello a 17 miliardi di parametri su 4 GPU per ridurre la latenza del 1,5x (Figura 7).
Questi tre tipi di miglioramenti possono essere utilizzati in tandem, ottenendo soluzioni ad alta velocità di trasferimento. Tuttavia, dopo aver applicato ottimizzazioni specifiche dell’hardware, ci sono opzioni limitate per ridurre la latenza – e le opzioni esistenti sono costose. Risolviamo questo problema!
Passaggio in avanti del decodificatore di linguaggio, rivisitato
Hai letto sopra che ogni passaggio in avanti del modello produce i logit per il token successivo, ma in realtà questa è una descrizione incompleta. Durante la generazione di testo, l’iterazione tipica consiste nel modello che riceve in input l’ultimo token generato, più i calcoli interni memorizzati per tutti gli altri input precedenti, restituendo i logit del token successivo. La memorizzazione nella cache viene utilizzata per evitare calcoli ridondanti, ottenendo passaggi in avanti più veloci, ma non è obbligatoria (e può essere utilizzata parzialmente). Quando la memorizzazione nella cache è disabilitata, l’input contiene l’intera sequenza di token generati finora e l’output contiene i logit corrispondenti al prossimo token per tutte le posizioni nella sequenza! I logit nella posizione N corrispondono alla distribuzione per il prossimo token se l’input fosse costituito dai primi N token, ignorando tutti i token successivi nella sequenza. Nel caso particolare della decodifica greedy, se si passa la sequenza generata come input e si applica l’operatore argmax ai logit risultanti, si otterrà nuovamente la sequenza generata.
from transformers import AutoModelForCausalLM, AutoTokenizer
tok = AutoTokenizer.from_pretrained("distilgpt2")
model = AutoModelForCausalLM.from_pretrained("distilgpt2")
inputs = tok(["The"], return_tensors="pt")
generated = model.generate(**inputs, do_sample=False, max_new_tokens=10)
forward_confirmation = model(generated).logits.argmax(-1)
# Escludiamo i punti di partenza opposti da ogni sequenza: il passaggio in avanti restituisce i logit per il prossimo token, quindi è spostato di una posizione.
print(generated[0, 1:].tolist() == forward_confirmation[0, :-1].tolist()) # True
Ciò significa che è possibile utilizzare un passaggio in avanti del modello per uno scopo diverso: oltre a fornire alcuni token per prevedere il successivo, è anche possibile passare una sequenza al modello e verificare se il modello genererebbe la stessa sequenza (o parte di essa).
Consideriamo per un attimo che si ha accesso a un modello oracolo a latenza zero magico che genera la stessa sequenza del tuo modello, per qualsiasi input dato. A fini di discussione, non può essere utilizzato direttamente, è limitato ad essere un assistente per la procedura di generazione. Utilizzando la proprietà descritta sopra, si potrebbe utilizzare questo modello assistente per ottenere token di output candidati seguiti da un passaggio in avanti con il proprio modello per confermare che siano effettivamente corretti. In questo scenario utopico, la latenza della generazione di testo sarebbe ridotta da O(n)
a O(1)
, con n
che rappresenta il numero di token generati. Per generazioni lunghe, stiamo parlando di diversi ordini di grandezza.
Facendo un passo verso la realtà, supponiamo che il modello assistente abbia perso le sue proprietà oracolari. Ora è un modello a latenza zero che sbaglia alcuni dei token candidati, secondo il tuo modello. A causa della natura autoregressiva del compito, non appena l’assistente sbaglia un token, tutti i token successivi devono essere invalidati. Tuttavia, ciò non impedisce di interrogare nuovamente l’assistente, dopo aver corretto il token errato con il proprio modello, e ripetere questo processo in modo iterativo. Anche se l’assistente sbaglia alcuni token, la generazione di testo avrà una latenza di un ordine di grandezza inferiore rispetto alla sua forma originale.
Ovviamente, non esistono modelli assistenti a latenza zero. Tuttavia, è relativamente facile trovare un modello che approssima le uscite di generazione di testo di un altro modello – versioni più piccole della stessa architettura addestrate in modo simile spesso soddisfano questa proprietà. Inoltre, quando la differenza nelle dimensioni del modello diventa significativa, il costo di utilizzare il modello più piccolo come assistente diventa un dettaglio trascurabile dopo aver considerato i vantaggi di evitare alcuni passaggi in avanti! Ora comprendi il nucleo della generazione assistita.
Decodifica greedy con generazione assistita
La generazione assistita è un equilibrio. Si desidera che l’assistente generi rapidamente una sequenza candidata pur essendo il più preciso possibile. Se l’assistente ha una bassa qualità, si ottiene il costo di utilizzare il modello assistente con pochi o nessun beneficio. D’altra parte, ottimizzare la qualità delle sequenze candidate può implicare l’uso di assistenti lenti, con conseguente rallentamento netto. Sebbene non si possa automatizzare la selezione del modello assistente per te, abbiamo incluso un ulteriore requisito e un euristico per garantire che il tempo trascorso con l’assistente rimanga sotto controllo.
Prima di tutto, il requisito – l’assistente deve avere lo stesso tokenizzatore esatto del tuo modello. Se questo requisito non fosse in atto, sarebbe necessario aggiungere costose operazioni di decodifica e ricodifica dei token. Inoltre, queste operazioni aggiuntive dovrebbero avvenire sulla CPU, il che potrebbe richiedere lenti trasferimenti di dati tra dispositivi. L’uso rapido dell’assistente è fondamentale affinché i vantaggi della generazione assistita si manifestino.
Infine, l’euristica. A questo punto, avrai probabilmente notato le somiglianze tra il film Inception e la generazione assistita: stai infatti eseguendo la generazione di testo all’interno della generazione di testo. Ci sarà un passaggio in avanti del modello dell’assistente per ogni token candidato, e sappiamo che i passaggi in avanti sono costosi. Sebbene non sia possibile sapere in anticipo il numero di token che il modello dell’assistente prenderà correttamente, è possibile tenere traccia di queste informazioni e utilizzarle per limitare il numero di token candidati richiesti all’assistente: alcune sezioni dell’output sono più facili da prevedere di altre.
Riassumendo tutto, ecco la nostra implementazione originale del ciclo di generazione assistita ( codice ):
- Utilizzare la decodifica greedy per generare un certo numero di token candidati con il modello dell’assistente, producendo
candidates
. Il numero di token candidati prodotti viene inizializzato a5
la prima volta che viene chiamata la generazione assistita. - Utilizzando il nostro modello, eseguire un passaggio in avanti con
candidates
, ottenendologits
. - Utilizzare il metodo di selezione del token (
.argmax()
per la ricerca greedy o.multinomial()
per il campionamento) per ottenere inext_tokens
dalogits
. - Confrontare
next_tokens
concandidates
e ottenere il numero di token corrispondenti. Ricorda che questo confronto deve essere fatto con una causalità da sinistra a destra: dopo il primo mismatch, tutti i candidati vengono invalidati. - Utilizzare il numero di corrispondenze per suddividere le cose e scartare le variabili relative ai token candidati non confermati. In sostanza, in
next_tokens
, conserva i token corrispondenti più il primo token divergente (che il nostro modello genera da una sottosequenza di candidati validi). - Regolare il numero di token candidati da produrre nella prossima iterazione: la nostra euristica originale lo aumenta di
2
se TUTTI i token corrispondono e lo diminuisce di1
altrimenti.
Abbiamo progettato l’API in 🤗 Transformers in modo che questo processo sia senza problemi per te. Tutto ciò che devi fare è passare il modello dell’assistente tramite il nuovo argomento chiave assistant_model
e raccogliere i vantaggi di latenza! Al momento del rilascio di questo post sul blog, la generazione assistita è limitata a una dimensione batch di 1
.
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
prompt = "Alice e Bob"
checkpoint = "EleutherAI/pythia-1.4b-deduped"
assistant_checkpoint = "EleutherAI/pythia-160m-deduped"
device = "cuda" if torch.cuda.is_available() else "cpu"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
inputs = tokenizer(prompt, return_tensors="pt").to(device)
model = AutoModelForCausalLM.from_pretrained(checkpoint).to(device)
assistant_model = AutoModelForCausalLM.from_pretrained(assistant_checkpoint).to(device)
outputs = model.generate(**inputs, assistant_model=assistant_model)
print(tokenizer.batch_decode(outputs, skip_special_tokens=True))
# ['Alice e Bob sono seduti in un bar. Alice sta bevendo una birra e Bob sta bevendo un']
Vale la pena la complessità interna aggiuntiva? Diamo un’occhiata ai numeri di latenza per il caso di decodifica greedy (i risultati per il campionamento sono nella sezione successiva), considerando una dimensione batch di 1
. Questi risultati sono stati estratti direttamente da 🤗 Transformers senza ulteriori ottimizzazioni, quindi dovresti essere in grado di riprodurli nel tuo ambiente.
Osservando i numeri raccolti, vediamo che la generazione assistita può offrire significative riduzioni di latenza in diverse situazioni, ma non è una soluzione miracolosa – dovresti testarla prima di applicarla al tuo caso d’uso. Possiamo concludere che la generazione assistita:
- 🤏 Richiede l’accesso a un modello di assistente che sia almeno un ordine di grandezza più piccolo del tuo modello (più grande è la differenza, meglio è);
- 🚀 Ottiene accelerazioni fino a 3 volte in presenza di INT8 e fino a 2 volte altrimenti, quando il modello si adatta alla memoria della GPU;
- 🤯 Se stai lavorando con modelli che non si adattano alla tua GPU e ti affidi al trasferimento di memoria, puoi ottenere accelerazioni fino a 10 volte;
- 📄 Si distingue nei compiti basati sull’input, come il riconoscimento automatico del parlato o la sintesi.
Esempio con generazione assistita
La decodifica avariziosa è adatta per compiti basati sull’input (riconoscimento automatico della voce, traduzione, riassunto, …) o per la ricerca di conoscenze factuali. I compiti aperti che richiedono elevati livelli di creatività, come la maggior parte delle applicazioni di un modello di linguaggio come chatbot, dovrebbero utilizzare il campionamento. La generazione assistita è naturalmente progettata per la decodifica avariziosa, ma ciò non significa che non si possa utilizzare la generazione assistita con campionamento multinomiale!
Estrarre campioni da una distribuzione di probabilità per il token successivo farà sì che il nostro assistente avaro fallisca più spesso, riducendo i suoi vantaggi in termini di latenza. Tuttavia, possiamo controllare quanto nitida sia la distribuzione di probabilità per i token successivi, utilizzando il coefficiente di temperatura presente nella maggior parte delle applicazioni basate sul campionamento. Ad un estremo, con temperature vicine allo 0, il campionamento approssimerà la decodifica avariziosa, favorendo il token più probabile. All’altro estremo, con la temperatura impostata su valori molto superiori a 1, il campionamento sarà caotico, estratto da una distribuzione uniforme. Le basse temperature sono quindi più favorevoli al modello del tuo assistente, mantenendo la maggior parte dei vantaggi in termini di latenza della generazione assistita, come possiamo vedere di seguito.
Perché non lo vedi tu stesso, per farti un’idea della generazione assistita?
Direzioni future
La generazione assistita dimostra che le moderne strategie di generazione di testo sono pronte per l’ottimizzazione. Comprendere che attualmente è un problema legato alla memoria, non un problema legato al calcolo, ci consente di applicare semplici euristiche per ottenere il massimo dalla larghezza di banda di memoria disponibile, alleviando il collo di bottiglia. Crediamo che un ulteriore perfezionamento dell’uso dei modelli assistenti ci porterà a riduzioni ancora maggiori della latenza – ad esempio, potremmo essere in grado di saltare alcuni passaggi in avanti se chiediamo all’assistente di generare diverse continuazioni candidate. Naturalmente, rilasciare modelli piccoli di alta qualità da utilizzare come assistenti sarà fondamentale per realizzare e amplificare i benefici.
Inizialmente rilasciato all’interno della nostra libreria 🤗 Transformers, da utilizzare con la funzione .generate()
, ci aspettiamo di offrirlo in tutto l’universo di Hugging Face. La sua implementazione è anche completamente open-source, quindi se stai lavorando sulla generazione di testo e non stai utilizzando i nostri strumenti, sentiti libero di utilizzarlo come riferimento.
Infine, la generazione assistita riporta alla luce una questione cruciale nella generazione di testo. Il campo si è sviluppato con il vincolo in cui tutti i nuovi token sono il risultato di una quantità fissa di calcoli, per un dato modello. Un token per passaggio in avanti omogeneo, in modo puramente autoregressivo. Questo post del blog rafforza l’idea che non dovrebbe essere così: grandi sezioni dell’output generato possono essere ugualmente generate da modelli che sono una frazione delle dimensioni. Per questo, avremo bisogno di nuove architetture di modelli e metodi di decodifica: siamo entusiasti di vedere cosa ci riserva il futuro!
Lavori correlati
Dopo il rilascio originale di questo post del blog, mi è stato portato all’attenzione che altri lavori hanno esplorato lo stesso principio fondamentale (utilizzare un passaggio in avanti per convalidare continuità più lunghe). In particolare, dai un’occhiata ai seguenti lavori:
- Decodifica parallela a blocchi, di Google Brain
- Campionamento speculativo, di DeepMind
Citazione
@misc {gante2023assisted,
author = { {Joao Gante} },
title = { Assisted Generation: una nuova direzione verso la generazione di testo a bassa latenza },
year = 2023,
url = { https://huggingface.co/blog/assisted-generation },
doi = { 10.57967/hf/0638 },
publisher = { Hugging Face Blog }
}
Riconoscimenti
Vorrei ringraziare Sylvain Gugger, Nicolas Patry e Lewis Tunstall per aver condiviso molte preziose suggerimenti per migliorare questo post del blog. Infine, un plauso a Chunte Lee per aver progettato la splendida copertina che puoi vedere nella nostra pagina web.