> ## Content Index
> Fetch the complete content index at: https://adrianoamalfi.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Da Ubuntu nudo a LLM locale su una GPU NVIDIA da 16 GB
- URL: https://adrianoamalfi.com/tech-scratchpad/llm-locale-ubuntu-nvidia-16gb/
- Published: 2026-09-26T08:53:49.000Z
- Updated: 2026-09-28T12:12:04.000Z
- Description: Da Ubuntu nudo a inferenza LLM locale su GPU NVIDIA da 16 GB: driver, CUDA, llama.cpp con offload, modelli che entrano e benchmark reali.
- Author: Adriano Amalfi
- Tags: LLM, Scratchpad, Tutorial, AI in Produzione, Linux, #Tech Scratchpad: Appunti avanzati e esperimenti per esperti e amatoriali

**Ambiente:** Ubuntu 24.04.5 LTS · kernel 7.0.0-34-generic · NVIDIA 580.178.04 · CUDA 13.2.78 · cmake 3.28.3 · gcc 13.3.0 · llama.cpp 0.5.0-dev (fcc8915) · RTX 3080 Ti Laptop 16 GB

## Prerequisiti

- Ubuntu 24.04 LTS installato, con accesso `sudo`. I comandi valgono anche su 22.04, ma i pacchetti sono verificati su 24.04.
- Una GPU NVIDIA con 16 GB di VRAM. I numeri di questa guida vengono da una RTX 3080 Ti Laptop, ma l'architettura è la stessa per 4080, 4070 Ti Super, 4060 Ti 16 GB e 3090.
- Circa 25 GB liberi su disco: CUDA pesa qualche GB, i due modelli di prova altri 17 GB.
- Connessione a internet per driver, toolkit e modelli.

## 1\. Prepara il sistema e la toolchain di build

llama.cpp si compila, quindi servono compilatore, cmake e git. Su una Ubuntu appena installata non ci sono.

```bash
sudo apt update
sudo apt install -y build-essential cmake git curl wget pkg-config
```

Se ha funzionato, `cmake` e `gcc` rispondono con le versioni che userai per la build:

```bash
cmake --version | head -1
gcc --version | head -1
```

Output atteso:

```console
cmake version 3.28.3
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
```

## 2\. Installa il driver NVIDIA e verifica la GPU

Prima del driver, `nvidia-smi` non esiste o non vede la scheda. Ubuntu impacchetta i driver nel repository `graphics-drivers`; il branch 580 è quello su cui sono state misurate le prestazioni di questa guida.

```bash
ubuntu-drivers devices
sudo ubuntu-drivers install nvidia:580
sudo reboot
```

`ubuntu-drivers devices` elenca i branch disponibili per la tua scheda e marca quello consigliato. Oggi potrebbe consigliare un branch più recente (qui suggerisce il 595-open): va bene anche quello, ma i numeri che leggerai più avanti sono del 580\. Se vuoi la stessa combinazione, installa il pacchetto esatto:

```bash
sudo apt install -y nvidia-driver-580-open
sudo reboot
```

Dopo il riavvio, la verifica è una sola:

```bash
nvidia-smi
```

Devi vedere la scheda, il driver e la memoria totale. Sulla macchina di prova:

```console
Fri Sep 25 20:08:12 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.178.04             Driver Version: 580.178.04     CUDA Version: 13.0     |
+-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3080 ...    Off |   00000000:01:00.0 Off |                  N/A |
| N/A   53C    P8             12W /   90W |     223MiB /  16384MiB |      0%      Default |
+-----------------------------------------+------------------------+----------------------+
```

Il numero che conta è `16384MiB`: sono i tuoi 16 GB. Se `nvidia-smi` restituisce "command not found", il driver non è installato. Se restituisce un errore di comunicazione con il driver, spesso è il Secure Boot.

Con Secure Boot attivo il modulo NVIDIA deve essere firmato. Al primo avvio può comparire la schermata blu di MOK enrollment: scegli \`Enroll MOK\`, imposta una password e inseriscila al riavvio successivo. Se salti quel passaggio, \`nvidia-smi\` fallisce anche se il pacchetto è installato. In alternativa disabilita Secure Boot dal firmware.

## 3\. Installa CUDA Toolkit 13.2

Per compilare llama.cpp con il backend CUDA serve `nvcc`, che non arriva con il driver. NVIDIA distribuisce il toolkit via repository APT. Il pacchetto `cuda-toolkit-13-2` è presente nel repo `ubuntu2404` e la chiave `cuda-keyring` è aggiornata.

```bash
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get install -y cuda-toolkit-13-2
```

Il pacchetto è grosso, qualche GB. Alla fine aggiungi `nvcc` al PATH e verifica:

```bash
echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
nvcc --version
```

Output atteso:

```console
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2026 NVIDIA Corporation
Built on Thu_Mar_19_11:12:51_PM_PDT_2026
Cuda compilation tools, release 13.2, V13.2.78
Build cuda_13.2.r13.2/compiler.37668154_0
```

Il driver 580.178.04 dichiara come runtime massimo CUDA 13.0, mentre il toolkit installato è 13.2\. Non è un errore: la compatibilità tra minor version di CUDA permette di compilare ed eseguire con un driver un po' più vecchio del toolkit. Questa combinazione è stata testata e funziona.

## 4\. Compila llama.cpp con il backend CUDA

Il binario che molte distribuzioni installano non ha CUDA dentro. Serve la build da sorgente con `-DGGML_CUDA=ON`.

```bash
cd ~
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j$(nproc)
```

Durante la configure devi vedere che cmake trova il toolkit e la tua architettura:

```console
-- Found CUDAToolkit: /usr/local/cuda/targets/x86_64-linux/include (found version "13.2.78")
-- Using CMAKE_CUDA_ARCHITECTURES=86-real CMAKE_CUDA_ARCHITECTURES_NATIVE=86-real
-- FlashAttention K-V type combinations: f16-f16;q4_0-q4_0;q8_0-q8_0;bf16-bf16
-- Warning: NCCL not found, performance for multiple CUDA GPUs will be suboptimal
-- ggml version: 0.25.3
-- ggml commit:  fcc8915
```

Il warning su NCCL è normale con una sola GPU: NCCL serve solo per il multi-GPU. La build finisce con `[100%] Built target llama-cli` e `[100%] Built target llama-server`. A quel punto controlla che la GPU sia vista dal binario appena compilato:

```bash
./build/bin/llama-cli --list-devices
./build/bin/llama-cli --version
```

Output atteso:

```console
Available devices:
  CUDA0: NVIDIA GeForce RTX 3080 Ti Laptop GPU (15982 MiB, 15566 MiB free)

version: 0.5.0-dev (build 1, commit fcc8915)
built with GNU 13.3.0 for Linux x86_64
```

Se `--list-devices` mostra solo CPU, la build non ha preso CUDA: cancella `build/` e riparti, controllando che `nvcc` sia nel PATH.

## 5\. Scarica i modelli che entrano in 16 GB

Qui sta il vincolo vero. Non basta che i pesi entrino nella VRAM: ci deve stare **anche la KV cache**, che cresce con il contesto e con il numero di layer di attenzione. Un 8B denso in Q4 pesa meno di 5 GB e ti lascia spazio per contesti lunghi; un 20B MoE in MXFP4 ne pesa 12 e ti porta vicino al tetto; un 24B denso in Q4 sfora.

```bash
mkdir -p ~/models && cd ~/models

curl -L -o Qwen3-8B-Q4_K_M.gguf \
  "https://huggingface.co/Qwen/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.gguf"

curl -L -o gpt-oss-20b-MXFP4.gguf \
  "https://huggingface.co/ggml-org/gpt-oss-20b-GGUF/resolve/main/gpt-oss-20b-MXFP4.gguf"
```

Al termine `ls -lh` deve mostrare queste dimensioni:

```console
-rw-rw-r-- 1 adriano adriano 4,7G set 25 20:59 Qwen3-8B-Q4_K_M.gguf
-rw-rw-r-- 1 adriano adriano  12G set 25 21:05 gpt-oss-20b-MXFP4.gguf
```

Il primo è il modello di lavoro comodo: 4,68 GiB di pesi, contesto nativo 32.768 token. Il secondo è il caso interessante: 11,27 GiB di pesi, un MoE da 20B che sta in 16 GB e con meno layer di attenzione del denso, quindi consuma molta meno KV cache per token di contesto.

## 6\. Avvia il server con l'offload sulla GPU

`llama-server` espone un'API compatibile con OpenAI e un'interfaccia web. `-ngl 99` dice di caricare tutti i layer sulla GPU, cioè l'*offload* completo; `-np 1` tiene un solo slot, perché con più slot paralleli sprechi memoria; `-c` è la dimensione del contesto.

```bash
cd ~/llama.cpp
./build/bin/llama-server \
  -m ~/models/gpt-oss-20b-MXFP4.gguf \
  -ngl 99 -c 65536 -np 1 \
  --host 127.0.0.1 --port 8080
```

In pochi secondi il log conferma il caricamento e la messa in ascolto:

```console
0.00.036.645 I srv    load_model: loading model 'gpt-oss-20b-MXFP4.gguf'
0.03.999.347 I srv    load_model: initializing, n_slots = 1, n_ctx_slot = 65536, kv_unified = 'false'
0.04.002.488 I srv  llama_server: model loaded
0.04.002.499 I srv  llama_server: listening on http://127.0.0.1:8080
```

Verifica da un altro terminale:

```bash
curl -s http://127.0.0.1:8080/health
```

Deve rispondere `{"status":"ok"}`. A questo punto parla col modello:

```bash
curl -s http://127.0.0.1:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"messages":[{"role":"user","content":"Rispondi in una frase: quanti GB di VRAM ha una RTX 4080?"}],"max_tokens":1024}'
```

La risposta è un JSON con la frase in `content` e le velocità in `timings`. I valori di questo test:

```console
content: "La RTX 4080 è dotata di 16 GB di VRAM GDDR6X."
finish_reason: stop
predicted_per_second: 97.51
prompt_per_second: 437.23
```

gpt-oss-20b è un modello di reasoning: genera prima il ragionamento in \`reasoning\_content\` e solo dopo la risposta in \`content\`. Se metti \`max\_tokens\` basso, tipo 64, il budget finisce dentro il ragionamento e \`content\` resta vuoto con \`finish\_reason: "length"\`. Tieni almeno 1024 token di output, o disattiva il thinking se il tuo client lo permette.

### Se usi Qwen3-8B

Per il denso cambia solo il modello. Con `-fa on -ctk q8_0 -ctv q8_0` comprimi la KV cache e recuperi memoria:

```bash
./build/bin/llama-server \
  -m ~/models/Qwen3-8B-Q4_K_M.gguf \
  -ngl 99 -np 1 -c 32768 -fa on -ctk q8_0 -ctv q8_0 \
  --host 127.0.0.1 --port 8080
```

Qwen3-8B ha \`n\_ctx\_train\` a 40960: oltre quel valore llama.cpp riduce da solo il contesto e nel log leggi \`n\_ctx\_slot = 40960\` anche se avevi chiesto \`-c 131072\`. Per superarlo servono YaRN (\`--rope-scaling yarn --rope-scale 4 --yarn-orig-ctx 32768\`), non testato in questa guida. Su gpt-oss-20b, che dichiara 131072, il limite non si presenta.

## 7\. Misura con llama-bench e trova il tetto di memoria

`llama-bench` dà numeri riproducibili: `pp512` è la velocità di lavorazione del prompt, `tg128` quella di generazione token per token. La generazione è quella che senti mentre il testo scorre.

```bash
cd ~/llama.cpp
./build/bin/llama-bench -m ~/models/Qwen3-8B-Q4_K_M.gguf -ngl 99 -p 512 -n 128 -r 3
./build/bin/llama-bench -m ~/models/gpt-oss-20b-MXFP4.gguf -ngl 99 -p 512 -n 128 -r 3
```

Output reale sulla RTX 3080 Ti Laptop:

```console
| model                          |       size |     params | backend    | ngl |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | --: | --------------: | -------------------: |
| qwen3 8B Q4_K - Medium         |   4.68 GiB |     8.19 B | CUDA       |  99 |           pp512 |     2081.05 ± 104.72 |
| qwen3 8B Q4_K - Medium         |   4.68 GiB |     8.19 B | CUDA       |  99 |           tg128 |         47.98 ± 0.97 |

| model                          |       size |     params | backend    | ngl |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | --: | --------------: | -------------------: |
| gpt-oss 20B MXFP4 MoE          |  11.27 GiB |    20.91 B | CUDA       |  99 |           pp512 |      2260.99 ± 15.80 |
| gpt-oss 20B MXFP4 MoE          |  11.27 GiB |    20.91 B | CUDA       |  99 |           tg128 |         90.64 ± 1.42 |

build: fcc8915 (1)
```

Il MoE da 20B genera più veloce del denso da 8B: **attiva solo una frazione dei parametri per token**. Lo stesso test con `-ngl 0`, cioè solo CPU, mostra perché l'offload non è opzionale:

| Modello           | Backend        | pp512 (t/s) | tg128 (t/s) |
| ----------------- | -------------- | ----------- | ----------- |
| Qwen3-8B Q4\_K\_M | CUDA, \-ngl 99 | 2081,05     | 47,98       |
| Qwen3-8B Q4\_K\_M | CPU, \-ngl 0   | 478,65      | 6,12        |
| gpt-oss-20b MXFP4 | CUDA, \-ngl 99 | 2260,99     | 90,64       |
| gpt-oss-20b MXFP4 | CPU, \-ngl 0   | 498,86      | 19,12       |

Quanta VRAM resta per il contesto dipende dal modello. Queste sono le configurazioni testate, con `nvidia-smi --query-gpu=memory.used --format=csv,noheader`:

| Modello     | Comando                                               | VRAM usata | Esito                     |
| ----------- | ----------------------------------------------------- | ---------- | ------------------------- |
| gpt-oss-20b | \-ngl 99 -c 32768                                     | 12308 MiB  | OK                        |
| gpt-oss-20b | \-ngl 99 -np 1 -c 65536                               | 13102 MiB  | OK                        |
| gpt-oss-20b | \-ngl 99 -np 1 -c 131072 -fa on -ctk q8\_0 -ctv q8\_0 | 13454 MiB  | OK                        |
| Qwen3-8B    | \-ngl 99 -np 1 -c 32768                               | 9640 MiB   | OK                        |
| Qwen3-8B    | \-ngl 99 -np 1 -c 65536 -fa on -ctk q8\_0 -ctv q8\_0  | 10154 MiB  | OK (contesto reale 40960) |
| Qwen3-8B    | \-ngl 99 -np 1 -c 65536 (KV f16)                      | 14280 MiB  | OK (contesto reale 40960) |

Su Qwen3-8B la KV cache in f16 mangia 4 GB in più a 40960 token; con `-ctk q8_0 -ctv q8_0` scendi sotto gli 11 GB e la velocità perde poco (46,05 contro 47,98 t/s). Su gpt-oss-20b il risparmio c'è ma è minore, perché i layer di attenzione sono pochi.

## 8\. Ollama e vLLM: quando lasciare llama.cpp

llama.cpp è la scelta giusta per una singola macchina, un utente e il controllo totale dei flag. Due alternative coprono casi diversi.

**Ollama** incapsula llama.cpp e gestisce da solo download, offload e swap dei modelli. È già installato qui e si usa in un comando:

```bash
ollama run gpt-oss:20b
```

Perdi il controllo fine su `-ngl`, `-c` e KV cache, ma se il tuo problema è "voglio chattare senza pensare ai flag", è la strada più corta.

**vLLM** è un altro mestiere: serve più richieste in parallelo con throughput alto, usa PagedAttention e batching continuo. Si installa con `pip install vllm` ed espone un server OpenAI-compatible sulla porta 8000\. In cambio pretende uno stack Python, compila i suoi kernel CUDA e su una GPU da 16 GB con un modello vicino al tetto lascia meno margine di llama.cpp. Se ti serve un endpoint per un'applicazione multiutente, vale; per [inferenza personale](https://adrianoamalfi.com/local-ai-non-e-piu-hobby/), no.

## Verifica

Il setup è a posto quando tutti questi comandi danno il risultato atteso.

```bash
# 1. La GPU è visibile e il driver è caricato
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader

# 2. nvcc c'è e ha la versione giusta
nvcc --version | tail -2

# 3. llama.cpp vede la GPU
~/llama.cpp/build/bin/llama-cli --list-devices

# 4. Il modello gira sulla GPU, non sulla CPU
~/llama.cpp/build/bin/llama-bench \
  -m ~/models/gpt-oss-20b-MXFP4.gguf -ngl 99 -p 512 -n 128 -r 1

# 5. Il server risponde
curl -s http://127.0.0.1:8080/health
```

Se il punto 4 restituisce una `tg128` intorno ai 90 t/s hai la GPU in uso. Se restituisce valori a una cifra, l'offload non è attivo: manca `-ngl 99` o la build è senza CUDA.

## Limiti noti

- Questa guida non copre il multi-GPU. Il warning su NCCL in fase di build è la spia che il supporto non è configurato.
- Mistral Small 24B in Q4\_K\_M pesa 14,33 GB: non è stato misurato perché con la KV cache non entra in 16 GB con un contesto utile. Per un denso di quella taglia serve il downclock o l'offload parziale di alcuni layer in RAM, che qui non è stato testato.
- Il contesto oltre 40960 su Qwen3 richiede YaRN, non testato.
- I numeri valgono per questa GPU. Una 4060 Ti 16 GB ha la stessa VRAM ma [meno banda di memoria](https://adrianoamalfi.com/inferenza-on-device-a20-pro-memoria/), quindi si aspetta una `tg128` più bassa a parità di modello.