Ho pubblicato oggi Codendum 0.1.0 sotto licenza Apache 2.0. È un servizio per condividere una macchina che fa inferenza con modelli locali open weight per il coding generativo. I prompt e i sorgenti restano sulla rete dell’organizzazione, e tutto il resto del lavoro sta sulle postazioni.

Il repository contiene gli script, la configurazione di esempio e la documentazione per installare il servizio, chiuderlo, provarlo, misurarlo e mandarlo avanti. La documentazione è un sito a parte, costruito con Sphinx e MyST.

Che cosa fa

Un solo sistema NVIDIA GB10, il DGX Spark o una macchina OEM equivalente, esegue vLLM con un modello open weight per la scrittura di codice. Sulle postazioni gira OpenCode, configurato come provider compatibile con l’API OpenAI, e parla con quella macchina in HTTPS sulla rete locale o in VPN. Il modello servito si chiama coder, è Qwen3-Coder-30B-A3B-Instruct-FP8 e ha il tool calling abilitato in modo esplicito.

Quali linguaggi e quali piattaforme si possano usare lo decide il modello che si mette dentro. Il servizio inoltra richieste a un endpoint compatibile con l’API OpenAI e resta indifferente al linguaggio che ci passa dentro, quindi la copertura è quella del modello sottostante e si sposta quando si cambia modello.

La forma del servizio è questa:

  • vLLM ascolta solo su 127.0.0.1. Non è raggiungibile da fuori.
  • Un container nginx è l’unica porta d’ingresso. Accetta HTTPS su una porta dedicata, controlla che la richiesta arrivi da una rete ammessa, verifica una chiave API per utente e inoltra soltanto /v1/chat/completions e /v1/models. Le richieste con percorsi non normalizzati vengono rifiutate.
  • Il container del proxy ha file system di sola lettura, capability ridotte al minimo e no-new-privileges.
  • Immagini e modello sono fissati per digest e per revisione, e le action della CI per commit SHA con aggiornamenti via Dependabot.

Gli script stanno sull’host, pilotano Docker e fanno da client di prova, senza installare pacchetti né servizi sul sistema.

Perché l’host fa soltanto inferenza

La scelta viene da una proprietà dell’hardware. Il GB10 ha 128 GB di memoria unificata condivisa fra CPU e GPU, non 128 GB di VRAM sommati alla memoria di sistema. Sistema operativo, Docker, nginx, i processi di vLLM, i pesi del modello e la cache KV pescano tutti dallo stesso serbatoio. È la stessa caratteristica per cui, scrivendo di inferenza medica su questa macchina, il numero da guardare era la banda di memoria.

Di conseguenza il codice degli utenti, Git, le toolchain, i compilatori e le build stanno sulle postazioni o in ambienti di sviluppo isolati. Le build di un’intera classe, lanciate sull’host del modello, competerebbero per la memoria con la cache KV, ed è il motivo per cui la documentazione dice di non farlo.

Dimensionare la memoria

I pesi FP8 occupano circa 31,2 GB su disco e più o meno altrettanto in memoria. Il resto della memoria disponibile va alla cache KV, e quella si calcola.

Il modello ha 48 layer, 4 teste KV e head size 128. Con cache KV in FP8, un byte per valore, ogni token tenuto in contesto costa:

2 (K e V) × 48 layer × 4 teste KV × 128 × 1 byte = 49.152 byte = 48 KiB per token

Da qui si legge il costo teorico di una configurazione: 16 richieste da 65.536 token vogliono 48 GiB, 24 richieste ne vogliono 72 e da lì si sale in proporzione al numero di richieste concorrenti. È un limite inferiore, prima dei pesi e dell’overhead del runtime.

Sulla macchina di prova CUDA vedeva 121,6 GiB. I profili girano con --gpu-memory-utilization a 0,80, quindi a vLLM ne restano circa 97, e tolti i pesi e l’overhead il profilo predefinito ha ottenuto 62,4 GiB di cache KV, cioè 1.362.640 token, pari a 20,8 contesti pieni da 64K simultanei. vLLM scrive questi numeri nel log all’avvio e scripts/metrics.sh li rilegge, quindi conviene leggerli sul proprio host invece che fidarsi di questi.

Tre numeri che nella documentazione tengo separati, perché è facile sovrapporli: gli utenti collegati sono tutti quelli che hanno il client aperto e per la maggior parte del tempo non mandano niente, le richieste attive sono quelle in elaborazione e le limita --max-num-seqs, le richieste in attesa stanno in coda dentro vLLM in ordine di arrivo. Quando la cache KV finisce, vLLM prerilascia una richiesta in corso e la ricalcola più tardi, e questo si vede come picco di latenza e come preemptions nelle metriche.

Le misure su un GB10

Il 29 settembre 2026 ho fatto girare il servizio su un Lenovo ThinkStation PGX con DGX OS 7.2.3 e driver 580.178.04, vLLM 0.30.0, con OpenCode 2.0.19 come client di riferimento. Gli studenti simulati stavano in Docker su una postazione remota collegata in WireGuard, con circa 100 ms di andata e ritorno, quindi le latenze comprendono anche la rete.

Prima di tutto la calibrazione su una sessione vera. Un esercizio svolto con OpenCode ha prodotto 33 chiamate al modello in 63 secondi: una per il titolo, 20 passi dell’agente principale e 12 di un subagente di esplorazione. I prompt sono cresciuti da 6,8K a 11K token. Con uno o due utenti il tempo al primo token era circa 0,4 secondi, ciascuno riceveva circa 31 token al secondo in uscita e una richiesta di scrittura di codice si chiudeva in 1,7-2,2 minuti.

Poi la classe. Una classe piena di studenti simulati, quattro richieste a testa e pause di 20-90 secondi, con il prompt di sistema e gli strumenti catturati da una sessione reale di OpenCode. La finestra per le richieste nuove era di quindici minuti, più il tempo necessario a chiudere quelle in corso. Due profili a confronto:

classroom-64k (16 attive)classroom-64k-high-concurrency (24 attive)
Uscita del server a regime~110 tok/s~140-150 tok/s
Tempo al primo token, P50 / P9517,5 / 60 s10,6 / 34 s
Velocità di uscita per utente, P507,2 tok/s6,1 tok/s
Completamento di una richiesta di codice, P50 / P9511 / 22 min8,3 / 18 min
Richieste di codice completate4564
Picco di richieste in attesa2415
Picco di uso della cache KV8,5 %11,5 %
Preemption / errori del server0 / 00 / 0
Tasso di hit della prefix cache98,2 %98,2 %

Nelle mie prove il limite è stato la generazione, non la memoria. I contesti degli agenti sono rimasti fra 7K e 19K token e la cache KV non ha mai superato il 12 %, mentre ammettere più richieste insieme ha alzato l’uscita complessiva del server. La memoria reggerebbe più di 24 richieste attive, ma quel punto non l’ho misurato.

La prefix cache pesa molto: il 98 % di sette milioni di token di prompt è arrivato dalla cache invece che dal calcolo. In una classe il prompt di sistema e le definizioni degli strumenti sono gli stessi per tutti, e si pagano una volta sola.

Capacità in richieste all’ora

Per dimensionare un laboratorio mi è servito sapere quante richieste regge la macchina in un’ora, più della velocità di picco. Ci si arriva mettendo insieme due misure.

Una richiesta di scrittura di codice produce circa 2.700 token in uscita distribuiti su una quindicina di chiamate al modello. A 110-150 token al secondo, nelle mie misure un GB10 ha chiuso circa 150-200 richieste di questo tipo all’ora per l’intera classe. Quante ne tocchino a testa dipende da quanti sono gli studenti collegati.

La simulazione, con pause sotto i 90 secondi, chiede più di così, e infatti le richieste si accodano. Una classe che chiede meno aspetta meno.

Che cosa resta scoperto

Il modello di sicurezza è scritto per intero nella documentazione, con una tabella di minacce e contromisure e la parte che resta in capo a chi gestisce il servizio. Riassumo qui i punti che mi sembra giusto dichiarare.

Non ci sono quote di token per utente né scheduling equo. nginx limita le richieste, non i token, e vLLM serve in ordine di arrivo: una richiesta da 60K token costa molto più di una da 2K e nessuno dei due strati lo sa. Le quote e le priorità vere vogliono un gateway LLM dedicato davanti a vLLM, che Codendum non include e non prova.

È il punto in cui mi piacerebbe portare Admina, il framework di governance open source a cui lavoro, così che quote, politiche e tracciamento stiano in uno strato dichiarato invece che sparsi nella configurazione di nginx. Per ora è un’ipotesi di lavoro: non c’è codice e non c’è una data.

La prefix cache è condivisa fra utenti. È quello che rende il servizio sostenibile, ed è anche un canale laterale temporale: in linea di principio i tempi di risposta possono rivelare che qualcun altro ha mandato di recente lo stesso prefisso. Si disabilita con un argomento in più, pagandola in throughput, e la documentazione dice come.

Gli agenti eseguono comandi sulle postazioni con i permessi dell’utente. Il confine sta lì, nella configurazione del client, non nel server — è lo stesso punto su cui un agente da terminale eredita l’ambiente in cui lo si lancia. Isolare repository, credenziali e ambienti di build è compito delle postazioni.

Limiti

Una sola macchina e nessuna alta disponibilità: quando il GB10 o vLLM si fermano, tutti restano senza agente.

Le misure vengono da un host, un modello e una versione del client. Host diversi, immagini diverse, modelli diversi o una release diversa di OpenCode cambiano i numeri, e per questo nel repository ci sono bench.sh e bench-classroom.sh, da rieseguire dopo ogni aggiornamento. Circa l’1 % delle richieste è fallito sul percorso VPN senza mai arrivare al proxy, quindi riguarda la rete della prova.

Il codice generato va compilato, provato e letto da persone, e un progetto si costruisce a passi con revisione dopo ciascuno. Le richieste corte e mirate sono anche quelle che tengono reattivo un servizio condiviso.

Infine i prompt contengono il codice degli utenti e i log di nginx contengono identificativi e orari. Chi gestisce il servizio risponde delle politiche applicabili, comprese quelle di protezione dei dati quando gli utenti sono studenti o dipendenti, e della conservazione dei log. La licenza Apache 2.0 copre i file del repository, mentre immagini dei container, pesi del modello e software client si scaricano a parte e mantengono le proprie licenze.