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:

ConfigurazionePrompt (tok/s)Generazione (tok/s)Picco memoria
A — MTP spenta496,6141,813.360 MiB
B — MTP n-max 2417,0190,714.024 MiB
C — MTP n-max 3480,0171,414.204 MiB
D — MTP n-max 4480,0174,714.204 MiB
xychart-beta title "Generazione tok/s al variare dei token azzardati (MTP)" x-axis "Token azzardati per passo" ["0", "2", "3", "4"] y-axis "tok/s generazione" 120 --> 210 bar [141.8, 190.7, 171.4, 174.7]

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.

sequenceDiagram participant M as Modello Qwen3.6 participant D as Testa MTP participant C as Client API M->>D: stato corrente, genera 2 token candidati D-->>M: candidati "a" "b" M->>M: verifica i 2 candidati in un solo passaggio alt entrambi corretti M-->>C: emette 3 token (1 + 2 accettati) else uno scartato M-->>C: emette 2 token, riprova dal punto di divergenza end

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:

ContestoCache KVPrompt (tok/s)Generazione (tok/s)Picco memoria
8Kq8_0417,0190,714.024 MiB
16Kq8_0445,0191,4~14.000 MiB
32Kq8_0440,1190,2~14.000 MiB
64Kq8_0447,8192,615.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:

ContestoCache KVPrompt (tok/s)Generazione (tok/s)Picco memoriaEsito
128Kf1693,917,515.966 MiBoverflow in RAM
128Kq8_0448,0182,915.856 MiBfunzionante

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.

flowchart TD A["Richiesta contesto 128K"] --> B{"Cache KV a 16 bit o 8 bit?"} B -->|"f16 a 16 bit"| C["Cache + pesi superano i 16 GB"] C --> D["Il sistema scarica in RAM"] D --> E["17,5 tok/s, ingiocabile"] B -->|"q8_0 a 8 bit"| F["Cache dimezzata, resta in VRAM"] F --> G["182,9 tok/s, utilizzabile"]

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:

CompitoTTFTGenerazione (tok/s)Accettazione MTP
Refactor breve304 ms167,663,8% (111/174)
Metodo di estensione + test xUnit105 ms195,286,3% (252/292)
Debug di un deadlock asincrono123 ms179,172,2% (294/407)
Code review di codice Python129 ms192,282,9% (373/450)
Spiegazione di concetti C#111 ms183,376,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:

flowchart LR A["Accensione portatile"] --> B["Login Windows"] B --> C["Cartella Esecuzione automatica"] C --> D["Supervisore in background"] D --> E["llama-server profilo 128K"] E --> F["127.0.0.1 porta 30000"] F --> G["Provider custom endpoint"] G --> H["Tendina modelli VS Code"] H --> I["Qwen3.6 35B A3B RTX 5080"]

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|logs come 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.