190 token al secondo: un modello da 35 miliardi di parametri su un portatile
Un modello di linguaggio da 35 miliardi di parametri non dovrebbe stare in 16 gigabyte di memoria video, e tanto meno generare a quasi duecento token al secondo. Eppure succede, a patto di accettare due vincoli architetturali e di trattare la cache delle chiavi come una variabile di progetto. Raccontiamo cosa abbiamo misurato, con i numeri completi e i parametri che hanno contato.
C’è un numero, in questa storia, che vale più di ogni aggettivo: 190,7 token al secondo in generazione. È la velocità con cui un modello da 35 miliardi di parametri ha scritto su un portatile, con la ventola che girava e il resto del sistema operativo che rispondeva ai clic. Non è il record di un cluster da centinaia di migliaia di euro. È una scheda video da portatile, 16 gigabyte di memoria, un alimentatore da 157 watt.
Come ci siamo arrivati merita di essere raccontato, perché il percorso non è stato lineare e le due scoperte che contano sono arrivate dai dati, non dall’intuizione.
Il problema, dichiarato onestamente
Il modello si chiama Qwen3.6-35B-A3B, quantizzato in UD-IQ3_XXS: 14.069.266.720 byte, un file da 14 gigabyte che pesa quanto metà della memoria disponibile. La GPU è una RTX 5080 Laptop, 16.303 MiB di memoria video, tetto termico di circa 157 watt. Il motore è llama.cpp b11201 con supporto CUDA nativo per architettura Blackwell (SM120).
A occhio nudo, il conto non torna. Quattordici gigabyte di pesi, più la cache che serve per il contesto, più gli attivi: su una scheda da 16 dovrebbe starci stretto, o niente.
La prima cosa da capire è perché il modello esiste in questa forma.
Perché un modello da 35 miliardi occupa 14 giga
Qwen3.6-35B-A3B non è un modello denso. È un MoE ibrido con componente SSM: 41 blocchi, 256 esperti, e un parametro che si chiama full_attention_interval impostato a 4. Significa che l’attenzione piena, quella che costruisce la cache delle chiavi e dei valori, viene applicata solo a un blocco su quattro. Gli altri tre sono gestiti da una componente ricorrente di tipo stato, che non accumula memoria al crescere del contesto.
Blocco 0 ██████ attenzione piena -> costruisce KV cache
Blocco 1 ░░░░░░ componente SSM -> niente KV cache
Blocco 2 ░░░░░░ componente SSM -> niente KV cache
Blocco 3 ░░░░░░ componente SSM -> niente KV cache
Blocco 4 ██████ attenzione piena -> costruisce KV cache
...
Il “35B” nel nome è il numero totale di parametri; la “A3B” dice che ne sono attivi circa 3 miliardi per token. È questo il motivo per cui gira veloce: ad ogni token si muovono pochi pesi, non tutti e 35 i miliardi. La quantizzazione spinta (3,4 bit per peso) porta il file a 14 giga. L’architettura ibrida è ciò che rende il tutto gestibile, e si vede nei dati: la memoria non esplode al crescere del contesto come accadrebbe in un transformer classico.
Prima di toccare qualunque cosa, abbiamo fissato una priorità esplicita, perché in un portatile i vincoli si pestano i piedi a vicenda:
correttezza > stabilità > memoria > velocità > contesto.
Chi inverte quest’ordine ottiene benchmark che mentono.
Prima scoperta: la generazione speculativa interna (MTP)
Il modello ha una testa MTP integrata (Multi-Token Prediction, un layer predittivo “next-n” già dentro il file, senza bisogno di un modello di bozza separato). L’idea è banale da enunciare: invece di generare un token alla volta, il modello ne azzarda alcuni in avanti e poi li verifica in blocco. I token azzeccati si “scontano”, quelli sbagliati si scartano.
Il parametro che la controlla, --spec-draft-n-max, dice quanti token azzardare. Lo abbiamo fatto ruotare tenendo fisso tutto il resto, contesto 8K, temperatura 0, prompt da 512 token:
| Configurazione | Prompt (tok/s) | Generazione (tok/s) | Picco memoria |
|---|---|---|---|
| A — MTP spenta | 496,6 | 141,8 | 13.360 MiB |
| B — MTP n-max 2 | 417,0 | 190,7 | 14.024 MiB |
| C — MTP n-max 3 | 480,0 | 171,4 | 14.204 MiB |
| D — MTP n-max 4 | 480,0 | 174,7 | 14.204 MiB |
Chi vince non è chi azzarda di più. n-max 2 stacca il baseline del 34 per cento. n-max 3 e 4 fanno peggio, perché il tasso di accettazione cala: più token si indovinano in avanti, più spesso si sbaglia, e il costo dello scarto mangia il vantaggio. È un risultato controintuitivo e per questo prezioso: la generosità paga fino a un punto, poi inizia a costare.
Il tasso di accettazione, letto dai contatori interni del server (draft_n_accepted su draft_n), oscilla tra il 64 e l'86 per cento a seconda del compito: altissimo sul codice boilerplate e deterministico, più basso sul testo libero. È questa percentuale a determinare il guadagno reale, e spiega perché il numero cambia di conversazione in conversazione.
Seconda scoperta: il contesto non degrada la velocità
Nei transformer classici allungare il contesto rallenta la generazione: la cache cresce, l’attenzione legge più memoria, il throughput cala. Qui non succede. Con MTP n-max 2 attivo:
| Contesto | Cache KV | Prompt (tok/s) | Generazione (tok/s) | Picco memoria |
|---|---|---|---|---|
| 8K | q8_0 | 417,0 | 190,7 | 14.024 MiB |
| 16K | q8_0 | 445,0 | 191,4 | ~14.000 MiB |
| 32K | q8_0 | 440,1 | 190,2 | ~14.000 MiB |
| 64K | q8_0 | 447,8 | 192,6 | 15.316 MiB |
Da 8 a 64 mila token di contesto, la velocità di generazione resta praticamente inchiodata a 190 token/s, e la memoria sale di poco. È la firma dell’architettura ibrida: con l’attenzione piena solo su un blocco su quattro, la cache cresce un quarto rispetto a un modello omogeneo, e la Flash Attention fa il resto.
Fino a qui, tutto bene. Poi c’è il muro.
Il muro dei 128K, e la variabile che lo abbatte
Chiedere 128.000 token di contesto a 16 gigabyte è chiedere molto. Con la cache delle chiavi nel formato standard a 16 bit (f16) il risultato è disastroso:
| Contesto | Cache KV | Prompt (tok/s) | Generazione (tok/s) | Picco memoria | Esito |
|---|---|---|---|---|---|
| 128K | f16 | 93,9 | 17,5 | 15.966 MiB | overflow in RAM |
| 128K | q8_0 | 448,0 | 182,9 | 15.856 MiB | funzionante |
Con la cache a 16 bit la memoria si satura, il sistema scarica le parti fredde nella RAM di sistema, e la generazione crolla a 17,5 token/s: undici volte più lento. Il modello è tecnicamente vivo, ma di fatto inutilizzabile.
La soluzione sta in un parametro: -ctk q8_0 -ctv q8_0, cioè quantizzare la cache delle chiavi e dei valori a 8 bit invece di 16. Metà memoria per ogni elemento di cache. A 128K la cache quantizzata ci sta nei 16 gigabyte, l’overflow sparisce, e la generazione risale a 182,9 token/s.
Questa è la variabile di progetto che i benchmark superficiali ignorano. Su una scheda da 16 gigabyte, la quantizzazione della cache non è un vezzo di ottimizzazione: è ciò che decide se i contesti lunghi esistono o no. Abbiamo quindi fissato q8_0 come default su tutti i profili, incluso quello da lavoro quotidiano a 128K.
Oltre i benchmark: cinque compiti veri
I numeri di throughput dicono poco sulla qualità. Abbiamo sottoposto il server a cinque compiti di programmazione realistici e misurato il tempo al primo token (TTFT), la velocità e l’accettazione MTP su ogni esecuzione, letti dai contatori nativi del server:
| Compito | TTFT | Generazione (tok/s) | Accettazione MTP |
|---|---|---|---|
| Refactor breve | 304 ms | 167,6 | 63,8% (111/174) |
| Metodo di estensione + test xUnit | 105 ms | 195,2 | 86,3% (252/292) |
| Debug di un deadlock asincrono | 123 ms | 179,1 | 72,2% (294/407) |
| Code review di codice Python | 129 ms | 192,2 | 82,9% (373/450) |
| Spiegazione di concetti C# | 111 ms | 183,3 | 76,4% (211/276) |
Lettura dei dati: il tempo al primo token tra 100 e 300 millisecondi è al di sotto della soglia di percezione, e rende l’esperienza in un editor paragonabile a un servizio cloud. La generazione si conferma tra 168 e 195 token/s anche attraverso l’API, quindi la speculazione MTP funziona anche a server acceso, non solo nei test offline. L’accettazione MTP si conferma più alta sui compiti strutturati (il test xUnit all'86%) e più bassa sulla revisione libera (refactor al 64%). Gli output sono risultati pertinenti e corretti su tutti e cinque i casi.
Sul fronte stabilità: 32 richieste su 32 completate, zero errori o timeout, GPU in equilibrio termico tra 67 e 75 gradi, potenza tra 135 e 147 watt, memoria stabile. Nessun degrado di throughput nel tempo, nessun leak. Il picco termico osservato in tutta la campagna è stato 73 gradi, senza throttling.
Il portatile, durante l’inferenza, è rimasto utilizzabile: dodici dei ventiquattro thread logici della CPU (un Core Ultra 9 275HX) dedicati al modello, dodici lasciati liberi per il sistema. VS Code, il browser e le videochiamate hanno risposto.
Dal benchmark alla postazione di lavoro
Un modello che va forte ma va lanciato a mano da un terminale è un giocattolo. Un modello che parte da solo all’accensione ed è disponibile nella tendina dell’editor è uno strumento. La seconda parte del lavoro è stata tutta qui.
L’architettura finale:
Il supervisore fa tre cose: avvia il server se non c’è, lo riavvia se muore (con un ritardo di quindici secondi), e rispetta uno stop manuale esplicito senza ostinarsi a riaccenderlo. La sorveglianza gira staccata dal terminale, quindi sopravvive alla chiusura della finestra da cui è stata lanciata.
Due scelte tecniche sono valse più di altre:
- Solo loopback. Il server ascolta su
127.0.0.1:30000, mai sull’interfaccia di rete. Un modello locale non deve essere raggiungibile dagli altri dispositivi sulla LAN. - Un solo punto di ingresso.
qwen36 start|stop|restart|status|logscome comando unico, con controllo di duplicato: mai due istanze che si contendono la stessa GPU.
L’integrazione con VS Code passa per un provider a endpoint compatibile OpenAI. Il modello è comparso nella tendina accanto a quelli cloud, con tool calling, streaming e modalità reasoning verificate con richieste reali, non dichiarate sulla carta. Il contesto servito, letto dall’endpoint /props, è 131.072 token, con memoria video a regime sui 15,9 giga su 16.
Cosa resta sul tavolo
La trasparenza su cosa non torna ancora:
- Il contesto a 128K consuma quasi tutta la memoria video (15,9 su 16 giga). Multitasking pesante mentre il modello genera a contesto pieno può saturare la GPU. Per le giornate piene servono i profili più piccoli (8K, 16K, 64K), che sacrificano il contesto in cambio di margine.
- Il modello è di tipo “thinking”: produce prima un ragionamento interno. Letto da un client che lo sa gestire, va bene; letto da uno che non lo sa, restituisce contenuto vuoto se i token finiscono nel ragionamento. Serve un parametro esplicito per la risposta diretta.
- Il riavvio completo del portatile va validato alla prima occasione pianificata: il supervisore è stato testato in condizioni di crash, ma il ciclo freddo di accensione merita la conferma definitiva.
Cosa ci portiamo a casa
Tre insegnamenti che valgono oltre questa macchina.
Uno. I modelli MoE ibridi con stato ricorrente stanno rendendo i grandi modelli accessibili su hardware piccolo, e i numeri di questa prova lo dimostrano: 35 miliardi di parametri, 16 giga di memoria, quasi duecento token al secondo. Il collo di bottiglia si è spostato dai pesi alla cache.
Due. La generazione speculativa è un guadagno reale ma non gratis, e ha un optimum: indovinare due token in avanti conviene, indovinarne quattro no. I parametri vanno misurati, non impostati al massimo.
Tre. La quantizzazione della cache delle chiavi è la variabile nascosta dei contesti lunghi su schede video piccole. È la differenza, letterale, tra 17 e 183 token al secondo.
Nessuno di questi risultati richiede hardware esotico. Richiede un portatile, un motore open source, e la disciplina di far decidere ai numeri anziché all’intuito.
Tutti i valori riportati provengono da log e CSV misurati durante la campagna di test. Configurazione completa, script di avvio e procedure di rollback disponibili nel repository.
