Offload degli esperti MoE con llama.cpp e quanta RAM serve davvero

Offload degli esperti MoE con llama.cpp e quanta RAM serve davvero

La domanda arriva sempre nella stessa forma: ho una scheda da 16 GB, il modello che voglio pesa 20, posso farlo girare lo stesso? Con i modelli mixture of experts la risposta di llama.cpp è sì, grazie all'opzione --n-cpu-moe, che lascia nella RAM di sistema i pesi degli esperti di un certo numero di layer e li fa calcolare alla CPU, tenendo sulla GPU tutto il resto. È una funzione preziosa e molto fraintesa, perché "gira" non dice quanto bene gira, e soprattutto non dice quanta RAM di sistema serve perché giri decentemente. Ho misurato entrambe le cose sul mio banco, una RTX 5060 Ti da 16 GB su PCIe Gen3 x8 con un Ryzen 5 4500 e 32 GB di DDR4, prendendo come cavia Ornith-1.5-35B-A3B, il mixture of experts che uso tutti i giorni in produzione alla quantizzazione IQ3_XXS. Le misure risalgono alla fine di settembre, quando la RAM del banco girava ancora a 2133 MT/s invece dei 3600 nominali: ogni numero qui sotto è quindi un limite inferiore, e lo dico prima di tutto il resto perché cambia il modo di leggere le tabelle. Quello che emerge è un quadro diverso da quello che si legge di solito: la quantità di RAM conta solo fino a una soglia, la velocità della RAM conta poco, e il prezzo vero dell'offload lo paga la lettura dei prompt.

Nota

Il banco: RTX 5060 Ti 16 GB (Blackwell, limite di 180 W) su PCIe Gen3 x8, Ryzen 5 4500 a 6 core, 32 GB di DDR4 a 2.133 MT/s, Debian 13, driver NVIDIA 615.71.09 con CUDA 13.4, llama.cpp al commit 391fac1 con --fit off, cache KV q4_0, flash attention e un solo slot. Tutti i numeri che vedi qui derivano dalla procedura di benchmark descritta nel dettaglio in come il mio harness misura i modelli locali, un articolo in inglese che spiega condizioni fissate, sonde e statistica.

Cosa fa davvero --n-cpu-moe?

Sposta nella RAM di sistema i pesi degli esperti dei primi N layer, e li fa calcolare alla CPU invece che alla GPU. In un mixture of experts ogni layer contiene molti esperti, sotto-reti specializzate, e per ogni token ne attiva solo una piccola parte. Gli esperti sono la quota più grande dei pesi, quindi tenerne una parte fuori dalla VRAM libera molta memoria. Il resto del modello, attenzione, embedding, router, resta sulla GPU.

Il punto che si perde spesso è che i pesi non viaggiano sul bus a ogni token: restano nella RAM e la CPU li usa lì, scambiando con la GPU solo le attivazioni fra un layer e l'altro. Questo distingue l'offload di llama.cpp da quello di motori come Strata, che invece trasferiscono gli esperti verso la GPU a raffiche e saturano il PCIe, come ho misurato nel test sul collegamento PCIe Gen3 x8. Con --n-cpu-moe il collo di bottiglia si sposta sulla CPU e sulla banda della memoria di sistema, non sul bus.

Un esempio di avvio, con tutti i parametri fissati e l'adattamento automatico spento:

llama-server -m Ornith-1.5-35B-A3B-Q4_K_M.gguf \
  -ngl 99 --n-cpu-moe 10 -c 8192 -fa on \
  --cache-type-k q4_0 --cache-type-v q4_0 \
  -b 512 -ub 256 -np 1 -t 6 --fit off

Il numero dopo --n-cpu-moe è quanti layer tengono gli esperti sulla CPU: più è alto, meno VRAM serve e più lavoro tocca alla CPU.

Se stai valutando un setup di inferenza locale per un team e vuoi capire prima dell'acquisto se un modello in offload regge il carico reale, nel mio hub sull'AI per le aziende trovi il metodo con cui affronto il dimensionamento dell'inferenza self-hosted.

Il prezzo dell'offload, gradino per gradino

La quantizzazione ufficiale Q4_K_M di Ornith-1.5-35B-A3B pesa 20,22 GiB e su una scheda da 16 GB non entra a nessun contesto. Per farla girare ho salito la scala di --n-cpu-moe fino al primo gradino che caricava, e poi oltre, confrontando ogni gradino con le due quantizzazioni più piccole dello stesso modello che invece entrano intere in VRAM.

Ornith-1.5-35B-A3B su RTX 5060 Ti 16 GB: intero in VRAM contro offload degli esperti
Hardware
RTX 5060 Ti 16 GB, 180 W, PCIe Gen3 x8; Ryzen 5 4500, 6 core; 32 GB DDR4 a 2133 MT/s
Sistema
Debian 13, driver NVIDIA 615.71.09, CUDA 13.4
Software
llama.cpp 391fac1, llama-bench tramite l'harness, --fit off, layer sulla GPU salvo gli esperti in offload, flash attention attiva, cache KV q4_0, sei thread CPU, un solo slot

Metodo: Generazione e lettura del prompt a profondità zero, un modello alla volta su scheda inattiva. Q4_K_M con --n-cpu-moe crescente; IQ3_XXS e IQ2_M interi in VRAM come riferimento.

Risultati (token/s, più alto è meglio)

Ornith-1.5-35B-A3B su RTX 5060 Ti 16 GB: intero in VRAM contro offload degli espertivalori in token/s
Ornith-1.5-35B-A3B su RTX 5060 Ti 16 GB: intero in VRAM contro offload degli esperti
ConfigurazioneDecode (token/s)
IQ3_XXS, intero in VRAM125,1
IQ2_M, intero in VRAM123,5
Q4_K_M, n-cpu-moe 1071,1
Q4_K_M, n-cpu-moe 1657,3
Q4_K_M, n-cpu-moe 2445,2
Q4_K_M, n-cpu-moe 3237,3
Fonte: misure dell'autore, fine settembre 2026

Il blocco qui sopra riporta la generazione; la lettura del prompt, nelle stesse prove, ha dato questi valori.

ConfigurazionePrefill, token/s
IQ3_XXS, intero in VRAM1.996,8
IQ2_M, intero in VRAM1.461,4
Q4_K_M, n-cpu-moe 10323,0
Q4_K_M, n-cpu-moe 16266,0
Q4_K_M, n-cpu-moe 24196,0
Q4_K_M, n-cpu-moe 32159,5

Al gradino migliore, dieci layer di esperti sulla CPU, l'offload costa il 43% della velocità di generazione e l'84% della lettura del prompt rispetto allo stesso modello a IQ3_XXS che entra intero. Ogni gradino in più costa altra velocità in modo quasi lineare: con trentadue layer sulla CPU il decode scende a 37 token al secondo e il prefill a 160.

Il dato che conta di più nella pratica è il prefill. Un prompt da 4.000 token, poco più di un file sorgente medio, si legge in circa due secondi con il modello intero in VRAM e in oltre dodici con l'offload al gradino migliore. Su un assistente di coding che riceve contesto a ogni richiesta, quella attesa si somma a ogni domanda.

L'offload non rallenta il modello in modo uniforme. Rallenta un po' la scrittura e moltissimo la lettura, e la lettura è quella che paghi a ogni prompt lungo.

Quanta RAM di sistema serve davvero?

Questa è la domanda su cui si legge di tutto, e la misura dà una risposta netta. Ho limitato la RAM disponibile al processo con un tetto di systemd, MemoryMax con lo swap disabilitato, così che il server non potesse appoggiarsi al disco, e ho ripetuto la misura con tetti crescenti, da 10 GiB fino a tutta la RAM della macchina. Il tetto viene riletto dal cgroup del processo e registrato con ogni misura, quindi non è un'ipotesi ma un dato.

Con trentadue layer di esperti sulla CPU, sotto i 12 GiB il prefill crolla di un fattore otto; sopra i 12 GiB altra RAM non compra niente.

Prefill del 35B MoE con 32 layer di esperti sulla CPU, per tetto di RAMvalori in token/s
Prefill del 35B MoE con 32 layer di esperti sulla CPU, per tetto di RAM
Tetto di RAMPrefill (token/s)
10 GiB19,2
12 GiB146,5
14 GiB149,1
16 GiB149,1
20 GiB148,8
24 GiB148,8
28 GiB148,5
Senza tetto149,0
Fonte: mio banco, fine settembre 2026

A 10 GiB il prefill scende al 13% del massimo e il decode al 52%: il processo non ha abbastanza memoria per tenere gli esperti residenti e li rilegge di continuo dalla cache del file. Da 12 GiB in su la curva è piatta entro l'1%, e il decode resta fra 37,1 e 37,5 token al secondo a ogni tetto.

Con meno esperti sulla CPU il fabbisogno cambia, e la soglia si sposta. Con dieci layer in offload, il gradino più veloce, a 10 GiB il prefill resta all'80% del massimo e il decode al 97%; da 12 GiB in su, di nuovo, la curva è piatta.

Tetto di RAMPrefill con n-cpu-moe 32Decode con n-cpu-moe 32Prefill con n-cpu-moe 10Decode con n-cpu-moe 10
10 GiB19,2 t/s (13%)19,5 t/s (52%)291,8 t/s (80%)70,0 t/s (97%)
12 GiB146,5 t/s (98%)37,1 t/s (99%)362,7 t/s (100%)71,3 t/s (99%)
16 GiB149,1 t/s37,2 t/s363,5 t/s72,3 t/s
Senza tetto, 31.888 MiB149,0 t/s37,1 t/s363,6 t/s71,5 t/s

Una nota sui numeri: la scala di --n-cpu-moe e la curva della RAM sono due misure distinte, la prima con il prompt standard del benchmark del motore e la seconda con un prompt da 4.096 token, e per questo il prefill a dieci layer in offload vale 323 token al secondo nella prima e circa 363 nella seconda. Ciascuna è coerente al suo interno; i confronti vanno fatti dentro la stessa tabella.

La lettura pratica è semplice: per far girare questo modello in offload su una scheda da 16 GB servono circa 12 GiB di RAM libera per il processo, oltre a quella che il sistema operativo e gli altri servizi usano già. Su una macchina da 16 GB di RAM totale è al limite, su una da 32 GB c'è margine. Comprare 64 GB per questo scopo non migliora la velocità di un solo token.

E la velocità della RAM?

Qui la risposta è meno netta, e preferisco dire esattamente cosa ho e cosa non ho. Le misure di offload sopra sono tutte a 2133 MT/s. Dopo averle prese ho attivato il profilo di memoria che porta la DDR4 ai 3600 MT/s nominali, ma non ho ripetuto la scala di --n-cpu-moe alla nuova velocità.

L'unica misura appaiata fra le due velocità che ho riguarda un altro motore, Strata, che fa anch'esso girare un mixture of experts più grande della scheda calcolando una parte degli esperti sulla CPU. Su cinquanta problemi di programmazione, passando da 2133 a 3600 MT/s, la generazione è salita da 46,1 a 50,6 token al secondo, un rapporto mediano di 1,10, con 47 problemi su 50 più veloci. Un aumento del 69% della velocità della memoria ha comprato il 10% della velocità di generazione.

È un indizio, non una misura dell'offload di llama.cpp: il motore è diverso, il modello è diverso, e quella prova è avvenuta fuori dal mio harness di misura. Dice però una cosa coerente con il resto: in un setup in offload la RAM non è l'unico limite, e accelerarla da sola non trasforma la velocità. Il calcolo degli esperti sulla CPU, sei core Zen 2 nel mio caso, pesa almeno quanto la banda di memoria.

Nota

Tutte le misure di offload di questo articolo sono state prese con la RAM a 2133 MT/s. Con la RAM a 3600 MT/s i numeri salirebbero; di quanto, per l'offload di llama.cpp, non l'ho misurato.

Conviene l'offload o una quantizzazione più piccola?

Questa è la domanda che conta davvero, e il benchmark sopra la risolve già: lo stesso modello a IQ3_XXS entra intero nella scheda, genera a 125 token al secondo e legge i prompt a quasi 2.000. Anche a IQ2_M, ancora più compresso, genera a 123 e legge a 1.461. Il Q4_K_M in offload, al gradino migliore, genera a 71 e legge a 323.

Quanto costa in qualità scendere da 4 a 3 bit su questo modello? Non ho un confronto appaiato fra il Q4_K_M in offload e l'IQ3_XXS sugli stessi compiti, quindi non posso dare un numero. Quello che posso dire è che l'IQ3_XXS è la versione che uso in produzione, e che nei miei test più impegnativi, cinquanta problemi di programmazione con test eseguibili e cinque progetti reali da correggere in un loop agentico, è risultato il più capace del gruppo quando il ragionamento è attivo, come racconto nel confronto fra Qwen3.8-27B e Ornith-1.5-35B-A3B. E sulle curve di quantizzazione che ho misurato su altri modelli, nessuna coppia di quant ha prodotto una differenza di capacità che reggesse un test appaiato.

Ornith-1.5-35B-A3B su una scheda da 16 GB: quale versione
CriterioIQ3_XXS intero in VRAMQ4_K_M in offload
Entra nella VRAM della schedaSìNo, 20,22 GiB
Decode sopra i 100 token al secondoSì, 125No, 71 al massimo
Prefill sopra i 1.000 token al secondoSì, 1.997No, 323 al massimo
Dipende dalla RAM di sistemaNoSì, almeno 12 GiB liberi
Occupa la CPU durante la generazioneNoSì
Quantizzazione a 4 bitNo, 3 bitSì
Fonte: misure dell'autore, fine settembre 2026

Su una scheda da 16 GB, per questo modello, la quantizzazione più piccola intera in VRAM batte l'offload su tutto ciò che si può misurare in velocità, e non ho evidenza che l'offload compri qualità sufficiente a giustificare il prezzo. L'offload resta la scelta giusta quando non esiste una quantizzazione che entri, o quando un compito specifico ha mostrato, misurato, che la versione a 4 bit fa meglio.

Come misurare il tuo caso prima di comprare RAM

Il procedimento che ho usato si ripete in un pomeriggio, e risponde alla domanda per il tuo modello e la tua macchina, non per i miei.

  1. Trova il primo gradino di --n-cpu-moe che carica, partendo basso.

    llama-server -m modello.gguf -ngl 99 --n-cpu-moe 8 -c 8192 -fa on --cache-type-k q4_0 --cache-type-v q4_0 --fit off

    Verifica: il server risponde e nvidia-smi mostra la VRAM sotto il totale della scheda.

  2. Misura generazione e lettura del prompt a quel gradino e a un paio di gradini sopra, con un prompt di qualche migliaio di token.

  3. Limita la RAM del processo e ripeti, scendendo finché la velocità crolla.

    systemd-run --scope -p MemoryMax=12G -p MemorySwapMax=0 llama-server -m modello.gguf -ngl 99 --n-cpu-moe 10 -c 8192 --fit off

    Verifica: il prefill resta vicino al valore senza tetto. Se crolla, il tetto è sotto la soglia del tuo modello.

  4. Confronta tutto con la quantizzazione più grande che entra intera in VRAM, sugli stessi prompt.

Senza MemorySwapMax=0 la misura mente: il processo si appoggia allo swap e la curva misura il disco invece della memoria. Se la RAM di sistema è il tuo limite, uno strumento come quale server comprare aiuta a ragionare su quanta macchina serve prima di spendere.

Cosa resta fuori da queste misure

Tre cose, dette in chiaro. La prima è la velocità della RAM: le misure sono a 2133 MT/s e sono un limite inferiore per una macchina con la RAM alla frequenza nominale. La seconda è la profondità del contesto: tutti i numeri di offload sono a profondità zero o su prompt da 4.096 token, e con un contesto pieno la generazione rallenta per tutti, in offload come in VRAM. La terza è la qualità: non ho confrontato in modo appaiato il Q4_K_M in offload con l'IQ3_XXS sugli stessi compiti, quindi non affermo né che il primo sia migliore né che sia equivalente.

La configurazione di base che uso per tutte le misure, dai parametri fissati al tipo di KV cache, è descritta nelle strategie consigliate per gli LLM locali su 16 GB, e l'opzione --n-cpu-moe è documentata nel repository di llama.cpp.

Il costo in energia e in rumore

C'è un'ultima differenza che non compare nelle tabelle di velocità. Con il modello intero in VRAM la CPU resta quasi a riposo e tutto il lavoro lo fa la GPU, che sul mio banco assorbe fra 140 e 180 W durante la generazione, a seconda del modello. In offload una parte del lavoro passa alla CPU, che lavora su più core per tutto il tempo della generazione, e la macchina nel suo complesso resta impegnata più a lungo per ogni risposta, perché ogni risposta arriva più tardi. Non ho misurato l'energia per risposta in offload, quindi non do un numero; ma a parità di lavoro, un modello che risponde in un quinto del tempo tiene la macchina sotto carico per un quinto del tempo, e questo vale anche per il rumore delle ventole.

L'offload degli esperti è una delle funzioni più utili di llama.cpp per chi ha poca VRAM, e una delle più facili da usare male. Le misure dicono che sul mio banco costa poco in RAM, una dozzina di GiB, e molto in velocità, soprattutto nella lettura dei prompt, e che una quantizzazione più piccola che entra intera nella scheda fa meglio su tutto ciò che si misura in secondi. Se stai decidendo come far girare un modello più grande della tua scheda per un team e vuoi la risposta misurata sul tuo carico prima di comprare hardware, puoi descrivermi il caso nel modulo di preventivo gratuito: due minuti, e ti dico subito se rientra in quello che faccio.

Ultima modifica: