
Distribuire un LLM con Modal: da pip install all'endpoint di produzione
La maggior parte delle guide sull'auto-hosting di LLM glissa sulla parte più difficile: l'infrastruttura. Ci si batte con i driver CUDA, si gestiscono immagini Docker, si configura l'autoscaling, e alla fine si paga comunque per GPU inattive alle 3 di notte. Modal elimina tutto questo. Scrivi Python, distribuisci, ottieni un URL.
Questa guida ti accompagna passo dopo passo nella distribuzione di un LLM open source su Modal con vLLM come motore di inferenza. Alla fine avrai un endpoint API compatibile con OpenAI attivo su GPU H100 che scala a zero quando nessuno lo usa.
Cos'è Modal (e perché usarlo per gli LLM)?
Modal è una piattaforma di calcolo serverless costruita specificamente per i workload AI. Pensalo come AWS Lambda, ma con supporto GPU, fatturazione al secondo e un'esperienza sviluppatore nativa Python. Niente YAML, niente Dockerfile, niente Kubernetes — definisci tutta l'infrastruttura in uno script Python e distribuisci con un solo comando.
Ecco perché è diventato il punto di riferimento per la distribuzione di LLM:
- Fatturazione scale-to-zero — non paghi nulla quando il tuo endpoint non gestisce richieste
- Prezzi GPU al secondo — H100 a ~$3,95/ora, A100 80 GB a ~$2,50/ora, fatturati al secondo
- Cold start sub-secondo — i container si avviano velocemente, specialmente con snapshot di memoria
- $30/mese di crediti gratuiti — abbastanza per sperimentare senza addebiti sulla carta di credito
- Zero DevOps — niente build Docker, niente Terraform, niente gestione cluster
Se stai eseguendo LLM localmente e vuoi dargli una vera API senza gestire server, Modal è la via più breve.
Modal vs. RunPod vs. Lambda
| Caratteristica | Modal | RunPod | Lambda |
|---|---|---|---|
| Modello di fatturazione | Al secondo, scale-to-zero | Al secondo, addebito minimo | All'ora, sempre attivo |
| Cold start | 2-4 secondi | 6-12 secondi (grande) | N/A (persistente) |
| Disponibilità GPU | H100, A100, L40S, T4 | A100, H100, A6000 | H100, A100 |
| Infrastruttura | Python puro, nessun file di config | Docker, più controllo | Accesso VM completo |
| Livello gratuito | $30/mese di crediti | Nessuno | Nessuno |
| Ideale per | Workload a raffica/sviluppo | Traffico inferenza costante | Training ad alta intensità |
Conclusione: Modal vince per workload a raffica e sviluppo. Se l'utilizzo GPU supera costantemente il 40%, un'istanza dedicata su RunPod o Lambda è più economica. Per tutto il resto — prototipazione, API intermittenti, demo — il modello scale-to-zero di Modal fa risparmiare davvero.
Prerequisiti
Prima di iniziare, hai bisogno di tre cose:
- Python 3.10+ installato localmente
- Un account Modal — registrati gratuitamente su modal.com
- Un account Hugging Face — per accedere ai modelli (la maggior parte è ad accesso limitato)
Tutto qui. Niente GPU sulla macchina locale, nessun toolkit CUDA, niente Docker.
Passo 1: Installare Modal e autenticarsi
Apri un terminale e installa il pacchetto Python di Modal:
pip install modalPoi esegui il comando di configurazione per collegare l'ambiente locale al tuo account Modal:
modal setupQuesto apre una finestra del browser per l'autenticazione. Una volta confermato, Modal salva un token in locale. Non dovrai farlo mai più.
Passo 2: Definire l'immagine del container
I container Modal si definiscono in Python. Specifichi l'immagine base, installi le dipendenze e imposti le variabili d'ambiente — tutto come codice. Crea un file chiamato app.py:
import modal
# Definisci l'immagine container con CUDA, Python e vLLM
vllm_image = (
modal.Image.from_registry(
"nvidia/cuda:12.8.0-devel-ubuntu22.04", add_python="3.12"
)
.entrypoint([])
.pip_install(
"vllm==0.13.0",
"huggingface-hub==0.36.0",
)
)
app = modal.App("llm-endpoint", image=vllm_image)Alcune cose da notare. Non c'è Dockerfile — quella catena modal.Image lo sostituisce completamente. L'immagine base include NVIDIA CUDA 12.8 con Ubuntu 22.04, e installiamo vLLM e il client Hugging Face Hub sopra.
Passo 3: Configurare lo storage del modello con i Volume
I pesi degli LLM sono grandi (un modello da 7 miliardi di parametri pesa ~14 GB in fp16). Non vuoi scaricarli ogni volta che un container si avvia. I Volume Modal ti danno storage persistente che si monta direttamente nei tuoi container:
# Volume persistenti per il caching dei pesi del modello
hf_cache = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
vllm_cache = modal.Volume.from_name("vllm-cache", create_if_missing=True)
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-2507-FP8"
MODEL_REVISION = "953532f942706930ec4bb870569932ef63038fdf"Qui usiamo Qwen3-4B-Thinking (FP8) — un modello quantizzato da 4 miliardi di parametri che è veloce, capace e si adatta a una singola GPU. Puoi sostituirlo con qualsiasi modello Hugging Face: Llama 3.1 8B, Mistral 7B, o qualsiasi cosa supporti vLLM.
Perché FP8? Dimezza circa il consumo di memoria rispetto a fp16, il che significa che puoi eseguire modelli più grandi sulla stessa GPU — o modelli più piccoli su GPU meno costose. Se sei curioso dei compromessi della quantizzazione, la nostra guida all'esecuzione locale di LLM tratta i formati di precisione in dettaglio.
Passo 4: Creare la funzione server vLLM
Qui avviene la magia di Modal. Decori una funzione Python con i requisiti GPU, la configurazione di scaling e un'annotazione di web server. Modal si occupa del resto:
N_GPU = 1
MINUTES = 60
VLLM_PORT = 8000
@app.function(
gpu=f"H100:{N_GPU}",
scaledown_window=15 * MINUTES,
timeout=10 * MINUTES,
volumes={
"/root/.cache/huggingface": hf_cache,
"/root/.cache/vllm": vllm_cache,
},
)
@modal.concurrent(max_inputs=32)
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
def serve():
import subprocess
cmd = [
"vllm", "serve",
MODEL_NAME,
"--revision", MODEL_REVISION,
"--served-model-name", MODEL_NAME,
"--host", "0.0.0.0",
"--port", str(VLLM_PORT),
"--tensor-parallel-size", str(N_GPU),
"--enforce-eager", # Cold start più veloci
]
subprocess.Popen(" ".join(cmd), shell=True)Analizziamo i decoratori chiave:
gpu="H100:1"— richiede una singola GPU H100. Cambia in"A100-80GB:1"per inferenza più economica, o"H100:2"per modelli da 70B+scaledown_window=15 * MINUTES— mantiene il container caldo per 15 minuti dopo l'ultima richiesta, poi scala a zero@modal.concurrent(max_inputs=32)— consente fino a 32 richieste simultanee per container (vLLM gestisce il batching internamente)@modal.web_server(port=8000)— espone il server HTTP vLLM direttamente come endpoint web Modal--enforce-eager— salta la compilazione dei grafi CUDA per cold start più veloci (compromesso: throughput massimo leggermente ridotto)
Il scaledown_window è il tuo principale leva sui costi. Impostalo a 5 minuti per lo sviluppo, 15-30 minuti per le API di produzione con traffico regolare.
Passo 5: Distribuire in produzione
Un comando. Tutto qui:
modal deploy app.pyModal costruisce l'immagine container, la carica nel registro e restituisce un URL attivo:
✓ Created objects.
├── 🔨 Created mount /app.py
├── 🔨 Created volume huggingface-cache
├── 🔨 Created volume vllm-cache
└── 🔨 Created web function serve => https://your-workspace--llm-endpoint-serve.modal.runLa prima distribuzione richiede qualche minuto perché scarica i pesi del modello nel volume. Le distribuzioni successive (e i cold start) sono molto più veloci poiché i pesi sono in cache.
Per lo sviluppo, usa modal serve app.py — ricarica a caldo alle modifiche dei file e ti dà un URL temporaneo.
Passo 6: Chiamare il tuo endpoint (compatibile OpenAI)
Il tuo server vLLM distribuito espone un'API compatibile OpenAI su /v1/chat/completions. Puoi usare l'SDK Python standard di OpenAI per chiamarlo — punta semplicemente la base URL al tuo endpoint Modal:
from openai import OpenAI
client = OpenAI(
api_key="not-needed", # vLLM non richiede autenticazione di default
base_url="https://your-workspace--llm-endpoint-serve.modal.run/v1",
)
response = client.chat.completions.create(
model="Qwen/Qwen3-4B-Thinking-2507-FP8",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain what vLLM is in two sentences."},
],
temperature=0.7,
max_tokens=256,
)
print(response.choices[0].message.content)Funziona anche con curl:
curl -X POST https://your-workspace--llm-endpoint-serve.modal.run/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-4B-Thinking-2507-FP8",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 128
}'Qualsiasi strumento che supporta un'API compatibile OpenAI funzionerà — LangChain, LlamaIndex, la tua app. Se stai instradando richieste su più endpoint LLM, uno strumento gateway LLM può aiutarti a gestire failover e bilanciamento del carico.
Consigli per l'ottimizzazione dei costi
La fatturazione al secondo di Modal è già più efficiente della tariffazione oraria, ma puoi ricavarne ancora di più:
1. Usare la quantizzazione FP8
I modelli FP8 usano circa la metà della VRAM dei loro equivalenti fp16. Un Qwen3-8B in FP8 si adatta a una singola H100, mentre la versione fp16 ha bisogno della maggior parte degli 80 GB di quella GPU. Meno VRAM significa poter usare GPU meno costose (A100 40 GB, L40S) per modelli più piccoli.
2. Regolare la finestra di scale-down
Il parametro scaledown_window controlla per quanto tempo un container rimane caldo dopo l'ultima richiesta:
| Scenario | Finestra consigliata | Perché |
|---|---|---|
| Sviluppo/test | 5 minuti | Risparmiare, i cold start vanno bene |
| API interna (occasionale) | 10-15 minuti | Bilanciamento costo/latenza |
| Produzione (traffico regolare) | 20-30 minuti | Minimizzare i cold start |
| Produzione ad alto traffico | Usare min_containers=1 | Tenerne uno sempre caldo |
3. Scegliere la GPU giusta
Non scegliere sempre H100. I modelli più piccoli non ne hanno bisogno:
| Dimensione modello | GPU consigliata | Costo approssimativo/ora |
|---|---|---|
| 1-4B parametri | L4 o T4 | $0,59 – $0,80 |
| 7-8B parametri | A10 o L40S | $1,10 – $1,95 |
| 13-14B parametri | A100 40 GB | $2,10 |
| 30-70B parametri | A100 80 GB o H100 | $2,50 – $3,95 |
| 70B+ parametri | H100 x2 | $7,90 |
4. Abilitare il prompt caching
Se i tuoi workload coinvolgono system prompt ripetuti o prefissi condivisi, il caching automatico dei prefissi di vLLM può ridurre significativamente latenza e calcolo. Abilitalo aggiungendo --enable-prefix-caching al comando vLLM serve. Per un approfondimento su come funziona il caching tra diversi provider, consulta la nostra guida al prompt caching per LLM.
5. Usare --enforce-eager per l'ottimizzazione dei cold start
Per default, vLLM compila grafi CUDA all'avvio, il che richiede 1-3 minuti extra. Il flag --enforce-eager salta questa compilazione. Scambi ~10-15% di throughput massimo per cold start drasticamente più veloci. Per workload a raffica dove la latenza conta più del throughput grezzo, è quasi sempre la scelta giusta.
Oltre le basi: modelli con fine-tuning
Una volta a tuo agio nella distribuzione di modelli base, il passo naturale successivo è distribuire la tua versione con fine-tuning. Il workflow è identico — punta semplicemente MODEL_NAME al tuo repository Hugging Face o a un volume Modal contenente i tuoi pesi.
Modal supporta anche l'esecuzione di job di fine-tuning direttamente sulle sue GPU. Puoi addestrare un adattatore LoRA su Modal, salvarlo in un volume e distribuire il modello unificato — tutto senza lasciare la piattaforma. La nostra guida al fine-tuning degli LLM copre in profondità la parte di addestramento.
Il app.py completo
Ecco lo script di distribuzione completo in un unico blocco pronto da copiare e incollare:
import modal
# --- Definizione dell'immagine ---
vllm_image = (
modal.Image.from_registry(
"nvidia/cuda:12.8.0-devel-ubuntu22.04", add_python="3.12"
)
.entrypoint([])
.pip_install("vllm==0.13.0", "huggingface-hub==0.36.0")
)
# --- Volume per il caching del modello ---
hf_cache = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
vllm_cache = modal.Volume.from_name("vllm-cache", create_if_missing=True)
# --- Configurazione del modello ---
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-2507-FP8"
MODEL_REVISION = "953532f942706930ec4bb870569932ef63038fdf"
app = modal.App("llm-endpoint", image=vllm_image)
N_GPU = 1
MINUTES = 60
VLLM_PORT = 8000
@app.function(
gpu=f"H100:{N_GPU}",
scaledown_window=15 * MINUTES,
timeout=10 * MINUTES,
volumes={
"/root/.cache/huggingface": hf_cache,
"/root/.cache/vllm": vllm_cache,
},
)
@modal.concurrent(max_inputs=32)
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
def serve():
import subprocess
cmd = [
"vllm", "serve",
MODEL_NAME,
"--revision", MODEL_REVISION,
"--served-model-name", MODEL_NAME,
"--host", "0.0.0.0",
"--port", str(VLLM_PORT),
"--tensor-parallel-size", str(N_GPU),
"--enforce-eager",
]
subprocess.Popen(" ".join(cmd), shell=True)Distribuisci con modal deploy app.py, sostituisci MODEL_NAME con qualsiasi modello Hugging Face, e sei in produzione.
Domande frequenti
Quanto costa eseguire un LLM su Modal?
Dipende dalla GPU e da quanto a lungo il tuo endpoint rimane caldo. Un Qwen3-4B su H100 costa ~$3,95/ora di utilizzo attivo. Con scale-to-zero e una finestra di scale-down di 15 minuti, un endpoint poco usato potrebbe costare $5-15/mese. I $30 di crediti mensili gratuiti coprono molta sperimentazione.
Modal scala a zero?
Sì — è uno dei suoi principali punti di forza. Quando non arrivano richieste per la durata del tuo scaledown_window, il container si spegne e smetti di pagare. La prossima richiesta innesca un cold start (tipicamente 2-10 secondi a seconda della dimensione del modello e se usi --enforce-eager).
Posso distribuire Llama 3.1 o Mistral su Modal?
Assolutamente. Sostituisci la costante MODEL_NAME con qualsiasi modello supportato da vLLM: meta-llama/Llama-3.1-8B-Instruct, mistralai/Mistral-7B-Instruct-v0.3, o centinaia di altri su Hugging Face. Per i modelli da 70B+, cambia N_GPU a 2 e usa gpu="H100:2".
Come si confrontano i cold start con RunPod?
I cold start di Modal sono tipicamente 2-4 secondi per il container stesso, più il tempo di caricamento del modello. Con i pesi del modello in cache in un Volume e --enforce-eager abilitato, si parla di 10-30 secondi totali per un modello da 7-8B. I cold start serverless di RunPod vanno da meno di 200 ms (in cache) a 6-12 secondi per container più grandi — sebbene il loro modello always-on eviti del tutto i cold start.
L'endpoint vLLM di Modal è davvero compatibile con OpenAI?
Sì. vLLM implementa gli stessi endpoint /v1/chat/completions, /v1/completions e /v1/models che usa OpenAI. Puoi puntare l'SDK Python ufficiale openai al tuo URL Modal e funziona immediatamente. Streaming, function calling e modalità JSON funzionano tutti.
Ho bisogno di una GPU sulla mia macchina locale?
No. La tua macchina locale esegue solo la Modal CLI. Tutto il lavoro GPU avviene sull'infrastruttura cloud di Modal. Potresti distribuire da un Chromebook se volessi.
Come aggiungo l'autenticazione al mio endpoint?
Gli endpoint web Modal sono pubblici per default. Per la produzione, aggiungi una semplice verifica della chiave API nel codice della tua applicazione, o usa le funzionalità di autenticazione web integrate di Modal. Puoi anche configurare uno strato proxy usando un gateway LLM che gestisce autenticazione, rate limiting e routing.
Qual è la differenza tra modal serve e modal deploy?
modal serve crea un endpoint temporaneo che si ricarica a caldo quando modifichi il codice — perfetto per lo sviluppo. modal deploy crea un endpoint persistente, pronto per la produzione, con un URL stabile. Usa serve mentre iteri, deploy quando sei pronto a rilasciare.
Posso usare SGLang al posto di vLLM?
Sì. La documentazione di Modal include esempi SGLang accanto a vLLM. SGLang tende ad avere meno overhead per workload ad alta intensità di decode e modelli più piccoli. vLLM è generalmente migliore per workload misti con molto prefill. Entrambi producono endpoint compatibili OpenAI.
Come si confronta con la distribuzione su Railway o Render?
Piattaforme come Railway, Render e Fly.io sono ottime per le applicazioni web, ma non offrono istanze GPU. Modal è costruito appositamente per workload GPU con fatturazione al secondo e autoscaling. Se hai bisogno di servire un LLM, Modal (o RunPod) è lo strumento giusto — le piattaforme PaaS tradizionali non possono farlo.