Una GPU per l'IA condivisa tra più utenti: macchine virtuali e container su Proxmox

Una GPU NVIDIA Quadro RTX 8000 da 48 GB condivisa tra macchine virtuali e container LXC su Proxmox: perché le soluzioni standard non bastano, cosa risolve il driver «merged» e cosa costa, più un pulsante di accensione remoto con un microcontrollore ESP32.

ProxmoxNVIDIALXCESPHomeMQTT
Indice
  1. Il problema: una scheda, richieste diverse
  2. Due modi di condividere una GPU
  3. vGPU e driver «merged»
  4. Far vedere la GPU a un container
  5. Accenderlo da remoto: un ESP32 sul pulsante
  6. Cosa cambierà con il NanoKVM

In breve

  • Una scheda grafica (GPU) da 48 GB, da condividere tra più persone che lavorano con modelli di IA, senza dare a tutti accesso all'intera macchina.
  • Su Proxmox la GPU si può dividere tra macchine virtuali (ognuna riceve una «fetta» fissa) oppure condividere tra container, che la usano tutti insieme.
  • Con un driver particolare, detto «merged», si possono fare entrambe le cose sulla stessa scheda.
  • Nei container il punto critico è la versione del driver: deve essere identica a quella dell'host, fino all'ultima cifra.
  • In attesa di una console remota (NanoKVM), per accendere e spegnere il server a distanza ho collegato al pulsante di accensione un piccolo microcontrollore (ESP32). Sa se il server è acceso leggendo la linea del LED di accensione, senza chiedere niente al server.

A muHack, l'hackerspace di Brescia, abbiamo un server con una NVIDIA Quadro RTX 8000: una scheda professionale con 48 GB di memoria, abbastanza per far girare modelli linguistici di dimensioni medie o per addestrare modelli piccoli. La scheda però è una sola, e deve servire a più persone, ognuna con i propri progetti e le proprie librerie.

Il server gira con Proxmox VE, la piattaforma di virtualizzazione basata su Debian che uso anche per lavoro. Questo articolo racconta come ho reso la GPU disponibile sia alle macchine virtuali sia ai container, e come si accende il server senza doverci andare fisicamente.

Il problema: una scheda, richieste diverse

Chi usa il server non ha le stesse esigenze. Qualcuno vuole una macchina tutta sua, magari con Windows o con un kernel diverso, su cui fare quello che vuole senza disturbare gli altri. Qualcun altro vuole solo lanciare un notebook Jupyter o un modello per qualche ora, e per lui un container leggero è più comodo e spreca meno risorse. La GPU deve servire entrambi, e su Proxmox le strade standard costringono a scegliere.

  • PCI passthrough. La scheda intera viene assegnata a una sola macchina virtuale. È la soluzione più semplice e con le prestazioni migliori, ma finché quella macchina virtuale (VM) è accesa nessun altro, host compreso, può usare la GPU. Con 48 GB e un solo utente alla volta, la maggior parte della memoria resta ferma.
  • Driver normale sull'host, GPU ai container. Tutti i container vedono la scheda e la usano insieme. Funziona bene, ma le macchine virtuali restano senza GPU: chi ha bisogno di un sistema operativo diverso o di isolamento vero è escluso.
  • Driver vGPU sull'host. La scheda viene divisa in GPU virtuali, una per VM, ognuna con la sua memoria riservata. Però, come si vede sotto, l'host smette di poterla usare per il calcolo, e quindi anche i container.

Nessuna delle tre copre tutti i casi. Il driver «merged» è il modo per avere insieme la seconda e la terza.

Due modi di condividere una GPU

Proxmox offre due tipi di «ospite»: le macchine virtuali e i container LXC. Per una GPU la differenza è grande.

Macchina virtuale con vGPUContainer LXC
Come vede la GPUuna porzione virtuale, con memoria riservatala scheda intera, condivisa con gli altri container
Isolamentoforte: kernel e driver propridebole: stesso kernel dell'host
Driver nell'ospitedriver guest vGPUsolo la parte utente del driver, stessa versione dell'host
Prestazionivicine al nativo, ma dentro la propria fettanative
Quando convieneambienti separati, sistemi operativi diversitanti utenti fidati, uso intermittente della GPU

La differenza, in parole semplici. Una macchina virtuale è un computer finto completo, con il suo sistema operativo: con la vGPU riceve un pezzo fisso della scheda, come un appartamento in un condominio. Un container è più leggero: condivide il sistema operativo dell'host e vede la GPU intera, come coinquilini che condividono la stessa cucina.

vGPU e driver «merged»

La vGPU è la tecnologia NVIDIA che divide una scheda in più GPU virtuali, ognuna assegnata a una VM. Normalmente è riservata alle schede da datacenter e richiede driver e licenze specifici. La RTX 8000 è tra le schede professionali che la supportano in modo nativo, quindi non serve nessuno sblocco.

Il driver vGPU per l'host però ha un limite: trasforma la scheda in una «fabbrica» di GPU virtuali, e l'host stesso non la può più usare per calcolare. È un problema, perché i container non hanno un driver proprio: usano quello dell'host.

La soluzione che ho usato è un driver «merged», preparato dalla comunità che si occupa di vGPU su Proxmox. Unisce nello stesso pacchetto il driver vGPU per l'host e il driver normale. Con questo driver la stessa scheda può creare GPU virtuali per le VM e, allo stesso tempo, lavorare per l'host e quindi per i container.

I passi sull'host sono questi:

  1. abilitare nel BIOS e nei parametri del kernel l'IOMMU, la funzione che permette di assegnare dispositivi alle VM;
  2. installare il driver merged (versione 550.90.07) con DKMS, così il modulo viene ricompilato da solo quando si aggiorna il kernel di Proxmox;
  3. riavviare e controllare con nvidia-smi che la scheda sia visibile.
Macchina virtuale vGPU: una fetta fissa driver guest Container LXC GPU intera, condivisa driver solo utente mdev /dev/nvidia* Host Proxmox driver merged 550.90.07 vGPU per le VM + driver normale per host e container Quadro RTX 8000 48 GB, vGPU supportata
La stessa scheda serve sia le macchine virtuali, attraverso le GPU virtuali, sia i container, attraverso il driver dell'host.

Il prezzo del driver merged

È una soluzione che funziona bene, ma non è quella che NVIDIA prevede, e va gestita sapendolo:

  • Non è un pacchetto ufficiale. Lo prepara la comunità unendo due driver NVIDIA. Se qualcosa si rompe non c'è un supporto a cui rivolgersi, e ogni nuova versione arriva quando qualcuno la prepara.
  • È legato a versioni precise. Il driver deve essere compatibile con il kernel di Proxmox. Un aggiornamento del kernel può far fallire la ricompilazione del modulo, e al riavvio la GPU sparisce. Per questo gli aggiornamenti del kernel non vanno applicati alla cieca: prima si verifica che il driver compili, e nel frattempo il kernel in uso resta bloccato su quello che funziona.
  • Trascina con sé i container. Come spiegato più avanti, ogni container deve avere la stessa identica versione del driver. Cambiare driver sull'host significa aggiornare tutti i container.
  • I container non sono isolati tra loro. Le VM hanno la loro fetta di memoria, i container no: un processo in un container può occupare tutti i 48 GB e lasciare gli altri senza memoria. Serve un minimo di regole condivise tra chi usa il server e un occhio a nvidia-smi per vedere chi sta usando cosa.

Per un server condiviso in un hackerspace sono compromessi accettabili: la flessibilità vale più di un supporto ufficiale che comunque non avremmo. In un'azienda la stessa scelta andrebbe pesata diversamente.

Far vedere la GPU a un container

Un container LXC non ha un kernel proprio: usa quello dell'host. Per questo non si installano moduli del kernel nel container. Bisogna invece fare due cose: rendere visibili al container i file di dispositivo della GPU e installare dentro il container la sola parte utente del driver.

I file di dispositivo

In Linux la GPU si usa attraverso file speciali in /dev: nvidia0 per la scheda, nvidiactl per il controllo, nvidia-uvm per la memoria unificata usata da CUDA (la piattaforma di calcolo di NVIDIA), e /dev/dri/renderD128 per il rendering. Nella configurazione del container servono due tipi di righe:

  • permessi: regole che autorizzano il container ad accedere ai dispositivi con un certo numero maggiore, cioè il numero che il kernel assegna a ogni tipo di dispositivo;
  • montaggi: ogni file di dispositivo dell'host viene reso visibile nello stesso percorso dentro il container.

Il dettaglio da non sbagliare è che alcuni numeri maggiori sono fissi (195 per i dispositivi NVIDIA principali, 226 per /dev/dri) mentre altri, come quello di nvidia-uvm, vengono assegnati all'avvio e cambiano da una macchina all'altra. Vanno letti sull'host con ls -l /dev/nvidia* prima di scrivere la configurazione. Copiare le righe da una guida funziona solo se la guida è stata scritta sulla stessa macchina.

La versione del driver

Dentro il container si installa il driver normale di NVIDIA, della stessa identica versione di quello dell'host (550.90.07), con l'opzione che salta l'installazione dei moduli del kernel. Il motivo è che le librerie del driver nel container parlano con il modulo nell'host, e le due parti accettano solo la propria versione. Anche una differenza nell'ultima cifra dà un errore di version mismatch e nvidia-smi non parte.

Questo ha una conseguenza pratica: ogni aggiornamento del driver sull'host va ripetuto in tutti i container. Conviene quindi avere pochi container ben tenuti, o un modello da cui crearli.

CUDA e PyTorch

Per gran parte degli usi non serve installare il CUDA Toolkit completo. PyTorch, per esempio, si installa con il proprio runtime CUDA (qui la variante per CUDA 12.4) e ha bisogno solo del driver. Il toolkit serve a chi deve compilare codice CUDA. Per questo nel container modello non l'ho installato: ho lasciato nella home di root (/root) uno script che lo installa, e lo lancia solo chi ne ha bisogno. Il container resta più piccolo, e chi non compila non si porta dietro qualche gigabyte inutile.

Accenderlo da remoto: un ESP32 sul pulsante

Per accendere e spegnere il server da remoto la soluzione definitiva sarà un NanoKVM: una piccola console remota (un «KVM su IP», da keyboard-video-mouse) che dà accesso a schermo, tastiera e pulsante di accensione attraverso il browser, anche quando il sistema operativo non risponde. Nell'attesa di installarlo ho realizzato una soluzione temporanea.

È un ESP32-S2 (una Lolin S2 mini), un microcontrollore con Wi-Fi che costa pochi euro, programmato con ESPHome: il comportamento si descrive in un file di configurazione invece di scrivere il firmware da zero. L'ESP32 fa due cose: preme il pulsante di accensione al posto nostro e, soprattutto, sa se il server è acceso.

Telefono o script MQTT su TLS Broker MQTT Wi-Fi ESP32-S2 (ESPHome) preme legge Pannello frontale scheda madre pin del pulsante · LED di accensione
L'ESP32 è collegato in parallelo al pulsante di accensione e legge il LED che indica se il server è acceso.

Lo stato letto dal LED di accensione

La parte più interessante è come l'ESP32 capisce se il server è acceso. Non chiede niente al server e non usa la rete: legge la linea del LED di accensione sul connettore del pannello frontale, la stessa che accende la lucina sul case.

Questa linea la pilota direttamente la scheda madre, quindi dice lo stato vero dell'hardware, non quello del sistema operativo. È una differenza importante:

MetodoCosa ti dice davvero
ping o servizio di monitoraggioche il sistema operativo è acceso e la rete funziona
agente software sul serverche il sistema operativo è acceso e l'agente sta girando
LED di accensioneche la macchina è alimentata, qualunque cosa stia facendo il software

Se il server è bloccato, o si è fermato durante l'avvio, un ping direbbe «spento» mentre il LED dice «acceso». Ed è proprio l'informazione che serve prima di premere il pulsante: premere su una macchina che si crede spenta, e invece è accesa, la spegne.

Elettricamente la lettura è semplice. Sulla linea che l'ESP32 legge ci sono circa 3,3 V quando il server è spento e 0 V quando è acceso. L'ingresso è configurato senza resistenze di pull-up o pull-down, così l'ESP32 si limita ad ascoltare e non altera il circuito del LED. Due filtri rendono la lettura stabile: lo stato «acceso» deve durare 300 millisecondi prima di essere preso per buono, lo stato «spento» 2 secondi, così brevi variazioni non producono falsi cambi di stato.

In parole semplici. Invece di chiedere al server «sei acceso?», che è una domanda a cui un server bloccato non può rispondere, l'ESP32 guarda la sua spia. È quello che farebbe una persona davanti alla macchina.

Premere il pulsante senza fare danni

Il pulsante di accensione di un PC non è altro che un contatto che, quando lo si preme, collega a massa un pin della scheda madre. L'ESP32 fa la stessa cosa con un'uscita in modalità open-drain: quando «preme» collega il pin a massa, e quando non preme si scollega del tutto, invece di imporre una tensione. In questo modo non spinge mai corrente nella scheda madre, e il pulsante fisico sul case continua a funzionare come prima.

I comandi

L'ESP32 si collega a un broker MQTT privato, con connessione cifrata TLS e autenticazione. Si iscrive ad alcuni topic, cioè canali con un nome, e pubblica lo stato su un altro:

MessaggioCosa fa
pressione brevetiene premuto per 1,5 secondi: accende il server, oppure gli chiede di spegnersi in modo ordinato
pressione lungatiene premuto per 12 secondi, come tenere premuto il pulsante a mano quando il sistema non risponde
commutasceglie da solo l'azione in base allo stato letto dal LED
statopubblicato dall'ESP32 a ogni cambiamento: ON oppure OFF

Lo stato è pubblicato come messaggio retained: il broker conserva l'ultimo valore e lo consegna subito a chi si collega, senza dover aspettare il prossimo cambiamento. Chi apre sul telefono un'app MQTT vede quindi subito se il server è acceso.

Cos'è MQTT. È un sistema di messaggi molto leggero, usato spesso nei dispositivi domotici. C'è un server centrale, il broker. I dispositivi non si parlano direttamente: pubblicano messaggi su canali con un nome e ricevono quelli dei canali a cui sono iscritti. L'ESP32 non deve essere raggiungibile da internet: è lui a collegarsi al broker.

Cosa cambierà con il NanoKVM

Il NanoKVM farà tutto quello che fa l'ESP32 e in più darà accesso allo schermo e alla tastiera, quindi anche al BIOS e al boot. Anche lui si collega al pannello frontale, per il pulsante e per il LED di accensione. L'idea di leggere lo stato dal LED resta quindi la stessa: cambia solo chi la mette in pratica.

← Tutti gli articoli