Servire un LLM: vLLM e continuous batching

Perché servire un LLM a molti utenti è un problema diverso dal farlo girare per uno. Continuous batching e PagedAttention, spiegati: come vLLM impacchetta le richieste sulla GPU senza far aspettare nessuno.

Una GPU da decine di migliaia di euro che lavora al 10% delle sue possibilità: è la situazione in cui si trova, senza saperlo, chi serve un LLM nel modo ingenuo. Il modello risponde, gli utenti sono contenti, ma la maggior parte del silicio resta ferma ad aspettare. Il motivo è controintuitivo: generare testo con un transformer non è un problema di potenza di calcolo, ma di traffico. E il traffico, come in autostrada, si gestisce con l’organizzazione, non con motori più grossi.

Servire un LLM, speculative decoding, compressione per il servizio e valutazione: il capitolo di LLMOps è nel libro.

Leggi «LLMOps: operare i grandi modelli» nel libro →

Il problema: un token alla volta

Un LLM genera testo in modo autoregressivo: produce un token, lo riaccoda all’input, e ricomincia per produrre il successivo. Una risposta di 500 token richiede 500 passaggi attraverso il modello, uno dopo l’altro, senza scorciatoie. Ogni passaggio deve caricare dalle memorie della GPU tutti i pesi del modello — decine di miliardi di parametri — per produrre un singolo token.

Ecco il punto: durante la generazione, il collo di bottiglia non è quanto velocemente la GPU calcola, ma quanto velocemente sposta i pesi dalla memoria alle unità di calcolo. Si dice che l’inferenza è memory-bound. È come un camion che attraversa la città per consegnare un solo pacco: il costo del viaggio è identico che il cassone sia pieno o vuoto. Servire una richiesta alla volta significa viaggiare col cassone vuoto.

Il batching classico e i suoi limiti

La soluzione ovvia è riempire il camion: processare più richieste insieme, in un batch. Lo stesso passaggio attraverso il modello può produrre il prossimo token per 8, 16, 64 conversazioni contemporaneamente, quasi al prezzo di una. Il throughput — token totali al secondo — cresce enormemente.

Ma il batching statico, quello classico, ha un difetto strutturale: le richieste di un batch devono partire e finire insieme. Se nel gruppo c’è chi ha chiesto una risposta di 20 token e chi una di 2000, il primo finisce quasi subito ma il suo posto resta occupato — o peggio, la GPU continua a fare calcoli inutili per lui — finché l’ultimo non ha terminato. E le richieste arrivate nel frattempo aspettano in coda che il batch si liberi. È l’ascensore che non riparte finché tutti quelli dentro non sono scesi al proprio piano, mentre al pianterreno la fila si allunga.

Continuous batching: l’ascensore che non si ferma mai

L’idea del continuous batching (o batching a livello di iterazione) ribalta la logica: la composizione del batch viene rivista a ogni token generato, non a ogni richiesta completata. Quando una sequenza finisce — ha emesso il suo token di stop — esce immediatamente dal batch, e una richiesta in coda prende il suo posto al passaggio successivo. Il batch diventa un organismo fluido: sequenze entrano ed escono di continuo, e la GPU lavora sempre a pieno carico.

Il trucco non è far correre più veloce la GPU: è non lasciarle mai un posto vuoto.

Il guadagno è duplice. Il throughput sale perché nessuno slot resta occupato da sequenze già concluse. E la latenza percepita scende perché le nuove richieste non aspettano la fine di un intero batch: entrano appena si libera un posto, tipicamente entro pochi passaggi di generazione.

Richieste impacchettate sulla GPU: statico vs continuo tempo → Batching statico richiesta A (2000 token) B (finita) slot sprecato: GPU ferma per B C (finita) slot sprecato D, E in coda: aspettano A Continuous batching richiesta A (2000 token) B D entra subito al posto di B C E entra al posto di C GPU sempre piena

Con il batching statico gli slot restano occupati (o sprecati) fino alla fine della richiesta più lunga; con il continuous batching le sequenze concluse cedono subito il posto a quelle in coda.

PagedAttention: la memoria come in un sistema operativo

Il continuous batching, da solo, non basta. Ogni sequenza in generazione porta con sé una KV cache: la memoria delle chiavi e dei valori di attention già calcolati, che cresce a ogni token e per una singola conversazione lunga può occupare gigabyte. Gestirla in modo ingenuo — riservando a ogni richiesta un blocco contiguo grande quanto la risposta più lunga possibile — spreca una quota enorme di memoria GPU in frammentazione e sovra-allocazione, limitando quante sequenze possono convivere nel batch.

vLLM, il server di inferenza open source nato all’Università di Berkeley, ha reso pratico il continuous batching proprio risolvendo questo problema con PagedAttention: la KV cache viene spezzata in blocchi di dimensione fissa, allocati solo quando servono e sparsi dove c’è posto, con una tabella che tiene traccia di quale blocco appartiene a quale sequenza. Chi ha studiato sistemi operativi riconoscerà la memoria virtuale paginata: stesso principio, applicato alla VRAM. Il risparmio di memoria si traduce direttamente in più sequenze simultanee, quindi in più throughput.

Attorno a queste due idee vLLM è diventato uno standard di fatto per il self-hosting di modelli open weight, con un’API compatibile con quella di OpenAI: si punta il client verso il proprio server e il codice applicativo non cambia. Alternative come TGI di Hugging Face o TensorRT-LLM di NVIDIA adottano tecniche analoghe: il continuous batching è ormai il modo normale di servire un LLM, non un’ottimizzazione esotica.

Le metriche che contano

Chi mette in produzione un LLM impara presto che «quanto è veloce?» è una domanda mal posta. Le metriche sono almeno tre, e tirano in direzioni diverse. Il time to first token: quanto aspetta l’utente prima di vedere apparire qualcosa. La velocità di generazione per singola richiesta: i token al secondo che l’utente vede scorrere. E il throughput complessivo del sistema: i token totali serviti al secondo, che determinano il costo per richiesta.

Il continuous batching permette di scambiare un po’ della seconda per molta della terza: ogni singola risposta scorre leggermente più lenta di quanto farebbe da sola sulla GPU, ma il sistema nel suo insieme serve molti più utenti con lo stesso hardware. Per la maggior parte delle applicazioni è un ottimo affare: un utente non nota la differenza tra 40 e 60 token al secondo, ma il conto della GPU a fine mese si nota eccome.

Perché ti riguarda

Se usi API commerciali, questa meccanica spiega cose che altrimenti sembrano arbitrarie: perché i provider fatturano a token, perché la latenza varia con il carico, perché il primo token a volte tarda mentre il resto scorre fluido. Dietro ogni chatbot c’è un tetris di richieste impacchettate su GPU condivise.

Se invece valuti il self-hosting di un modello open weight — per costi, privacy o controllo — il messaggio è pratico: non scrivere il loop di generazione a mano. Un server come vLLM, su hardware identico, può servire un ordine di grandezza in più di traffico rispetto all’approccio ingenuo. La differenza tra un progetto LLM sostenibile e uno che brucia budget spesso non sta nel modello scelto, ma in come lo si serve.

I commenti sono riservati agli iscritti.

Accedi per commentare