
Implementar um LLM com o Modal: Da instalação do pip ao Endpoint de Produção
A maioria dos guias sobre auto-hospedagem de LLMs ignora a parte mais difícil: a infraestrutura. Luta-se com controladores CUDA, gerem-se imagens Docker, configura-se o escalonamento automático e, ainda assim, acaba-se por pagar por GPUs ociosas às 3 da manhã. O Modal elimina tudo isso. Escreve-se Python, implementa-se e obtém-se um URL.
Este guia acompanha-o na implementação de um LLM de código aberto no Modal com o vLLM como motor de inferência. No final, terá um endpoint de API ativo, compatível com a OpenAI, a funcionar em GPUs H100 que escala para zero quando ninguém o está a utilizar.
O que é o Modal (e porquê usá-lo para LLMs)?
O Modal é uma plataforma de computação serverless construída especificamente para cargas de trabalho de IA. Pense na AWS Lambda, mas com suporte para GPU, faturação por segundo e uma experiência de desenvolvimento nativa em Python. Não há YAML, nem Dockerfiles, nem Kubernetes; define toda a sua infraestrutura num script Python e implementa com um único comando.
Eis por que razão se tornou a escolha preferida para a implementação de LLMs:
- Faturação scale-to-zero, não paga nada quando o seu endpoint não está a processar pedidos
- Preços de GPU por segundo, H100s a ~$3,95/hora, A100 80GB a ~$2,50/hora, faturados por segundo
- Arranques a frio em menos de um segundo, os contentores iniciam rapidamente, especialmente com instantâneos de memória
- $30/mês em créditos gratuitos, suficientes para experimentar sem encargos no cartão de crédito
- Sem DevOps, sem compilações Docker, sem Terraform, sem gestão de clusters
Se tem estado a executar LLMs localmente e quer dar-lhes uma API adequada sem gerir servidores, o Modal é o caminho mais curto para lá chegar.
Modal vs. RunPod vs. Lambda
| Funcionalidade | Modal | RunPod | Lambda |
|---|---|---|---|
| Modelo de faturação | Por segundo, scale-to-zero | Por segundo, carga mínima | Por hora, sempre ativo |
| Arranque a frio | 2-4 segundos | 6-12 segundos (grande) | N/A (persistente) |
| Disponibilidade de GPU | H100, A100, L40S, T4 | A100, H100, A6000 | H100, A100 |
| Infraestrutura | Python puro, sem ficheiros de configuração | Baseado em Docker, mais controlo | Acesso total à VM |
| Nível gratuito | $30/mês em créditos | Nenhum | Nenhum |
| Ideal para | Cargas de trabalho esporádicas/dev | Tráfego de inferência constante | Treino de alta utilização |
Em resumo: O Modal vence para cargas de trabalho esporádicas e desenvolvimento. Se a sua utilização de GPU exceder consistentemente 40%, uma instância dedicada no RunPod ou Lambda é mais barata. Para todo o resto, prototipagem, APIs intermitentes, demonstrações, o modelo scale-to-zero do Modal poupa dinheiro real.
Pré-requisitos
Antes de começar, precisa de três coisas:
- Python 3.10+ instalado localmente
- Uma conta Modal, registe-se gratuitamente em modal.com
- Uma conta Hugging Face, para acesso aos modelos (a maioria dos modelos tem restrições de acesso)
É tudo. Não precisa de GPU na sua máquina local, nem do toolkit CUDA, nem do Docker.
Passo 1: Instalar o Modal e Autenticar
Abra um terminal e instale o pacote Python do Modal:
pip install modalDe seguida, execute o comando de configuração para ligar o seu ambiente local à sua conta Modal:
modal setupIsto abre uma janela do navegador para autenticação. Assim que confirmar, o Modal armazena um token localmente. Não precisará de fazer isto novamente.
Passo 2: Definir a Imagem do Contentor
Os contentores do Modal são definidos em Python. Especifica a imagem base, instala dependências e define variáveis de ambiente, tudo como código. Crie um ficheiro chamado app.py:
import modal
# Define the container image with CUDA, Python, and 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)Algumas coisas a notar. Não há Dockerfile, essa cadeia modal.Image substitui-na completamente. A imagem base inclui NVIDIA CUDA 12.8 com Ubuntu 22.04, e instalamos o vLLM e o cliente Hugging Face Hub por cima.
Passo 3: Configurar o Armazenamento do Modelo com Volumes
Os pesos dos LLMs são grandes (um modelo de 7 mil milhões de parâmetros tem ~14 GB em fp16). Não quer descarregá-los cada vez que um contentor inicia. Os Volumes do Modal oferecem armazenamento persistente que é montado diretamente nos seus contentores:
# Persistent volumes for caching model weights
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"Estamos a usar aqui o Qwen3-4B-Thinking (FP8), um modelo quantizado de 4 mil milhões de parâmetros que é rápido, capaz e cabe numa única GPU. Pode trocar este por qualquer modelo do Hugging Face: Llama 3.1 8B, Mistral 7B, ou qualquer outro suportado pelo vLLM.
Porquê FP8? Reduz o uso de memória para cerca de metade em comparação com fp16, o que significa que pode executar modelos maiores na mesma GPU, ou modelos menores em GPUs mais baratas. Se estiver curioso sobre as compensações da quantização, o nosso guia para executar LLMs localmente aborda os formatos de precisão em detalhe.
Passo 4: Criar a Função do Servidor vLLM
É aqui que a magia do Modal acontece. Decora uma função Python com requisitos de GPU, configuração de escalonamento e uma anotação de servidor web. O Modal trata de todo o 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", # Faster cold starts
]
subprocess.Popen(" ".join(cmd), shell=True)Vamos decompor os decoradores principais:
gpu="H100:1", solicita uma única GPU H100. Altere para"A100-80GB:1"para inferência mais barata, ou"H100:2"para modelos de 70B+scaledown_window=15 * MINUTES, mantém o contentor ativo durante 15 minutos após o último pedido, depois escala para zero@modal.concurrent(max_inputs=32), permite até 32 pedidos simultâneos por contentor (o vLLM gere o loteamento internamente)@modal.web_server(port=8000), expõe o servidor HTTP vLLM diretamente como um endpoint web do Modal--enforce-eager, ignora a compilação de grafos CUDA para arranques a frio mais rápidos (compensação: throughput de pico ligeiramente inferior)
A scaledown_window é a sua principal alavanca de custo. Defina-a para 5 minutos para desenvolvimento, 15-30 minutos para APIs de produção onde espera tráfego regular.
Passo 5: Implementar em Produção
Um comando. É tudo:
modal deploy app.pyO Modal constrói a imagem do contentor, envia-a para o seu registo e devolve um URL ativo:
✓ 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.runA primeira implementação demora alguns minutos porque descarrega os pesos do modelo para o volume. As implementações subsequentes (e os arranques a frio) são muito mais rápidas, pois os pesos estão em cache.
Para desenvolvimento, use modal serve app.py em vez disso; recarrega automaticamente nas alterações de ficheiros e fornece-lhe um URL temporário.
Passo 6: Chamar o Seu Endpoint (Compatível com OpenAI)
O seu servidor vLLM implementado expõe uma API compatível com a OpenAI em /v1/chat/completions. Pode usar o SDK Python oficial da OpenAI para o chamar, basta apontar o URL base para o seu endpoint do Modal:
from openai import OpenAI
client = OpenAI(
api_key="not-needed", # vLLM doesn't require auth by 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)Isto também funciona com 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
}'Qualquer ferramenta que suporte uma API compatível com a OpenAI funcionará, LangChain, LlamaIndex, a sua própria aplicação. Se estiver a encaminhar pedidos através de múltiplos endpoints LLM, uma ferramenta de gateway LLM pode ajudá-lo a gerir a failover e o balanceamento de carga.
Dicas de Otimização de Custos
A faturação por segundo do Modal já é mais eficiente do que a faturação horária, mas pode extrair ainda mais dela:
1. Usar Quantização FP8
Os modelos FP8 usam cerca de metade da VRAM das suas contrapartes fp16. Um Qwen3-8B em FP8 cabe numa única H100, enquanto a versão fp16 precisa da maior parte dos 80 GB dessa GPU. Menos VRAM significa que pode usar GPUs mais baratas (A100 40GB, L40S) para modelos menores.
2. Ajustar a Janela de Scale-down
O parâmetro scaledown_window controla quanto tempo um contentor permanece ativo após o último pedido:
| Cenário | Janela Recomendada | Porquê |
|---|---|---|
| Desenvolvimento/testes | 5 minutos | Poupar dinheiro, arranques a frio são aceitáveis |
| API interna (ocasional) | 10-15 minutos | Equilibrar custo vs latência |
| Produção (tráfego regular) | 20-30 minutos | Minimizar arranques a frio |
| Produção de alto tráfego | Use min_containers=1 | Manter um sempre ativo |
3. Escolher a GPU Certa
Não opte por defeito pela H100. Modelos menores não precisam dela:
| Tamanho do Modelo | GPU Recomendada | Custo Aprox./hora |
|---|---|---|
| 1-4B params | L4 ou T4 | $0,59 - $0,80 |
| 7-8B params | A10 ou L40S | $1,10 - $1,95 |
| 13-14B params | A100 40GB | $2,10 |
| 30-70B params | A100 80GB ou H100 | $2,50 - $3,95 |
| 70B+ params | H100 x2 | $7,90 |
4. Ativar Cache de Prompts
Se as suas cargas de trabalho envolvem prompts de sistema repetidos ou prefixos partilhados, o cache automático de prefixos do vLLM pode reduzir significativamente a latência e o computo. Pode ativá-lo adicionando --enable-prefix-caching ao comando de serve do vLLM. Para explorar mais profundamente como o cache funciona entre diferentes fornecedores, consulte o nosso guia de cache de prompts LLM.
5. Usar --enforce-eager para Otimização de Arranque a Frio
Por defeito, o vLLM compila grafos CUDA no arranque, o que demora 1-3 minutos extra. A flag --enforce-eager ignora esta compilação. Troca ~10-15% de throughput de pico por arranques a frio dramaticamente mais rápidos. Para cargas de trabalho esporádicas onde a latência importa mais do que o throughput bruto, é quase sempre a escolha certa.
Indo Além: Modelos Ajustados (Fine-Tuned)
Quando estiver confortável a implementar modelos base, o próximo passo natural é implementar a sua própria versão ajustada. O fluxo de trabalho é idêntico, basta apontar MODEL_NAME para o seu repositório Hugging Face ou um Volume do Modal contendo os seus pesos ajustados.
O Modal também suporta a execução de trabalhos de ajuste fino diretamente nas suas GPUs. Pode treinar um adaptador LoRA no Modal, guardá-lo num volume e implementar o modelo fundido, tudo sem sair da plataforma. O nosso guia de ajuste fino de LLM aborda o lado do treino em profundidade.
O app.py Completo
Aqui está o script de implementação completo num bloco pronto para copiar e colar:
import modal
# --- Image Definition ---
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")
)
# --- Volumes for Model Caching ---
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 Config ---
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)Implemente com modal deploy app.py, troque MODEL_NAME por qualquer modelo do Hugging Face, e está ativo.
Perguntas Frequentes
Quanto custa executar um LLM no Modal?
Depende da GPU e de quanto tempo o seu endpoint permanece ativo. Um Qwen3-4B numa H100 custa ~$3,95/hora de uso ativo. Com scale-to-zero e uma janela de scale-down de 15 minutos, um endpoint pouco utilizado pode custar $5-15/mês. Os $30 de crédito mensal gratuito cobrem muita experimentação.
O Modal escala para zero?
Sim, esse é um dos seus principais pontos de venda. Quando não chegam pedidos durante a duração da sua scaledown_window, o contentor encerra e deixa de pagar. O próximo pedido desencadea um arranque a frio (tipicamente 2-10 segundos, dependendo do tamanho do modelo e se usa --enforce-eager).
Posso implementar o Llama 3.1 ou Mistral no Modal?
Absolutamente. Troque a constante MODEL_NAME por qualquer modelo suportado pelo vLLM: meta-llama/Llama-3.1-8B-Instruct, mistralai/Mistral-7B-Instruct-v0.3, ou centenas de outros no Hugging Face. Para modelos de 70B+, altere N_GPU para 2 e use gpu="H100:2".
Como se comparam os arranques a frio com o RunPod?
Os arranques a frio do Modal são tipicamente 2-4 segundos para o próprio contentor, mais o tempo de carregamento do modelo. Com pesos de modelo em cache num Volume e --enforce-eager ativado, estamos a falar de 10-30 segundos no total para um modelo de 7-8B. Os arranques a frio serverless do RunPod variam de menos de 200ms (em cache) a 6-12 segundos para contentores maiores, embora o seu modelo sempre ativo evite completamente os arranques a frio.
O endpoint vLLM do Modal é verdadeiramente compatível com a OpenAI?
Sim. O vLLM implementa os mesmos endpoints /v1/chat/completions, /v1/completions e /v1/models que a OpenAI usa. Pode apontar o SDK Python oficial openai para o seu URL do Modal e funciona imediatamente. Streaming, chamada de funções e modo JSON funcionam todos.
Preciso de uma GPU na minha máquina local?
Não. A sua máquina local apenas executa a CLI do Modal. Todo o trabalho de GPU ocorre na infraestrutura cloud do Modal. Poderia implementar a partir de um Chromebook, se quisesse.
Como adiciono autenticação ao meu endpoint?
Os endpoints web do Modal são públicos por defeito. Para produção, adicione uma verificação simples de chave de API no seu código de aplicação, ou use as funcionalidades integradas de autenticação web do Modal. Também pode configurar uma camada de proxy usando um gateway LLM que trate da autenticação, limitação de taxa e encaminhamento.
Qual é a diferença entre modal serve e modal deploy?
modal serve cria um endpoint temporário que recarrega automaticamente quando edita o seu código, perfeito para desenvolvimento. modal deploy cria um endpoint persistente, pronto para produção, com um URL estável. Use serve enquanto itera, deploy quando estiver pronto para lançar.
Posso usar SGLang em vez de vLLM?
Sim. A documentação do Modal inclui exemplos de SGLang juntamente com vLLM. O SGLang tende a ter menor sobrecarga para cargas de trabalho intensivas em descodificação e modelos menores. O vLLM é geralmente melhor para cargas de trabalho mistas com pré-preenchimento intenso. Ambos produzem endpoints compatíveis com a OpenAI.
Como se compara isto à implementação no Railway ou Render?
Plataformas como Railway, Render e Fly.io são ótimas para aplicações web, mas não oferecem instâncias de GPU. O Modal foi construído de propósito para cargas de trabalho de GPU com faturação por segundo e escalonamento automático. Se precisa de servir um LLM, o Modal (ou RunPod) é a ferramenta certa, as plataformas PaaS tradicionais não conseguem fazê-lo.