
Se você já rodou um LLM local com Ollama ou llama.cpp, sabe que funciona muito bem — para uma ou duas pessoas. Quando a demanda vira uma API que precisa atender dezenas de requisições simultâneas, o jogo muda: a GPU fica ociosa esperando, a memória se fragmenta e a vazão despenca. É exatamente esse problema que o vLLM resolve, e é por isso que ele virou o motor de inferência padrão de boa parte da indústria.
Este post cobre a história do projeto, a ideia técnica que o tornou rápido, e um passo a passo executável para instalar e subir seu primeiro servidor no Linux.
De onde veio o vLLM
O vLLM nasceu no Sky Computing Lab da UC Berkeley. O repositório foi criado em fevereiro de 2023, e o lançamento público veio em 20 de junho de 2023, num post que anunciava algo difícil de acreditar na época: até 24x mais vazão que o HuggingFace Transformers e até 3,5x mais que o Text Generation Inference, sem alterar a arquitetura do modelo.
Não era marketing de laboratório. Antes do anúncio, o vLLM já rodava havia dois meses em produção servindo o Vicuna e a Chatbot Arena do LMSYS — era a tecnologia que permitia a um grupo de pesquisa pequeno bancar a conta de GPU de um chatbot público.
A ideia central foi publicada no paper “Efficient Memory Management for Large Language Model Serving with PagedAttention”, apresentado no SOSP 2023 — o que diz muito sobre a natureza do trabalho: não é um paper de machine learning, é um paper de sistemas operacionais.
A linha do tempo desde então:
- Fev/2023 — repositório criado no GitHub, sob licença Apache 2.0.
- Jun/2023 — lançamento público; já em produção no Vicuna e na Chatbot Arena.
- Out/2023 — paper do PagedAttention no SOSP.
- Jan/2025 — vLLM V1, reescrita do núcleo (scheduler, gerenciador de KV cache, worker, sampler e servidor de API), com ~1,7x de ganho. Sai em alpha na v0.7.0 e vira o motor padrão na v0.8.0.
- Mai/2025 — o projeto passa a ser hospedado pela PyTorch Foundation, um dos primeiros projetos de plataforma sob esse guarda-chuva.
- Ago/2026 — versão v0.28.0. O repositório passa de 91 mil estrelas e 21 mil forks, com centenas de contribuidores.
PagedAttention em 30 segundos
Para gerar cada token novo, o modelo precisa consultar as chaves e valores de atenção de todos os tokens anteriores. Esse material fica na VRAM e se chama KV cache. Ele é grande (chegava a 1,7 GB por sequência no LLaMA-13B) e, pior, dinâmico: o tamanho depende do comprimento da conversa, que ninguém sabe de antemão.
Os motores anteriores resolviam isso reservando um bloco contíguo do tamanho máximo possível para cada requisição. Resultado: os autores mediram 60% a 80% de desperdício de VRAM por fragmentação e reserva excessiva. Memória desperdiçada significa menos requisições simultâneas, o que significa GPU ociosa.
O PagedAttention aplica ao KV cache a mesma solução que o kernel usa para a memória do sistema há décadas — paginação:
- o KV cache de cada sequência é dividido em blocos de tamanho fixo;
- os blocos não precisam ser contíguos na VRAM, e são alocados sob demanda;
- uma block table mapeia blocos lógicos para físicos, exatamente como uma tabela de páginas.
A analogia é literal: blocos são páginas, tokens são bytes, sequências são processos. E, como em qualquer sistema com paginação, vem de brinde o compartilhamento: duas requisições com o mesmo prompt apontam para os mesmos blocos físicos, com contagem de referência e copy-on-write. O desperdício cai para menos de 4%, e sobra VRAM para agrupar muito mais sequências no mesmo lote.
Some a isso o continuous batching — em vez de esperar o lote inteiro terminar, o scheduler encaixa requisições novas assim que uma vaga abre — e você tem a explicação inteira do ganho de vazão.
vLLM, Ollama ou llama.cpp?
Os três servem LLMs, mas resolvem problemas diferentes. Escolher errado dói:
| vLLM | Ollama / llama.cpp | |
|---|---|---|
| Caso de uso | API multiusuário, produção, alta vazão | Uso pessoal e equipes pequenas; atendem concorrência (--parallel, OLLAMA_NUM_PARALLEL), mas a vazão cai rápido com a carga |
| Hardware | GPU dedicada com VRAM sobrando | Roda em CPU, GPU parcial, pouca VRAM |
| Formato do modelo | Pesos do Hugging Face (safetensors) | GGUF quantizado |
| Modelo não cabe na VRAM | Offload para a RAM é possível (--cpu-offload-gb), mas custa caro em latência |
Faz offload para a RAM e segue |
| Facilidade | Exige planejar VRAM e parâmetros | Um comando e funciona |
Regra prática: se é você sozinho no seu desktop, use Ollama ou o llama-server. Se é um serviço que várias pessoas ou vários agentes vão consumir ao mesmo tempo, vLLM.
Requisitos
- Linux (é a plataforma de primeira classe) e Python 3.10 a 3.13.
- GPU NVIDIA com driver e CUDA funcionando é o caminho mais suave. Há suporte a AMD (ROCm), Intel (XPU), TPU do Google e NPUs Ascend, além do
vLLM-Metalpara Apple Silicon. - VRAM: conte com os pesos do modelo mais o KV cache. Um modelo de 7B em FP16 pede ~14 GB só de pesos — some folga para o cache. É a partir de 24 GB que a coisa fica confortável.
Confira o ambiente antes:
nvidia-smi
python3 --version
Instalando
A forma recomendada pela documentação é com o uv, que resolve o índice correto do PyTorch para a sua versão de CUDA automaticamente:
# instala o uv, se ainda não tiver
curl -LsSf https://astral.sh/uv/install.sh | sh
# ambiente isolado + vLLM
uv venv --python 3.12 --seed
source .venv/bin/activate
uv pip install vllm --torch-backend=auto
O --torch-backend=auto inspeciona o driver CUDA instalado e escolhe a build certa. Para fixar uma versão específica, troque por --torch-backend=cu129 (ou a build que você usa — os binários do vLLM hoje saem para CUDA 12.8, 12.9 e 13.0).
Quer só experimentar, sem criar ambiente nenhum?
uv run --with vllm vllm --help
Em GPUs AMD, o índice é outro:
uv venv --python 3.12 --seed
source .venv/bin/activate
uv pip install vllm --extra-index-url https://wheels.vllm.ai/rocm/
O --python 3.12 não é detalhe: as wheels ROCm só existem para essa versão. Com 3.11 ou 3.13 o instalador cai silenciosamente na wheel CUDA do PyPI, e o erro só aparece na hora de rodar, como libcudart.so: cannot open shared object file.
E, se você prefere não instalar nada no host, a imagem oficial resolve:
docker run --runtime nvidia --gpus all \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-p 8000:8000 --ipc=host \
vllm/vllm-openai:latest \
--model Qwen/Qwen2.5-1.5B-Instruct
O --ipc=host dá ao container acesso à memória compartilhada do host. Sem ele, /dev/shm fica minúsculo e o vLLM quebra ao inicializar os workers — sobretudo com tensor parallel. A alternativa, se você não quiser compartilhar o namespace de IPC, é --shm-size=8g.
Primeiro teste: inferência em lote
Antes de subir o servidor, vale um teste offline para confirmar que a GPU está sendo usada. Crie teste.py:
from vllm import LLM, SamplingParams
prompts = [
"O kernel Linux foi criado por",
"A capital do Brasil é",
"Em uma frase, explique o que é paginação de memória:",
]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=64)
llm = LLM(model="Qwen/Qwen2.5-1.5B-Instruct")
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
print(f">> {output.prompt!r}")
print(f" {output.outputs[0].text!r}\n")
python3 teste.py
Repare que os três prompts são processados juntos, não em fila — é o batching em ação. Um detalhe que pega muita gente: llm.generate() não aplica o chat template do modelo. Para modelos instruct, use llm.chat() com a mesma estrutura de mensagens da API da OpenAI:
mensagens = [[{"role": "user", "content": p}] for p in prompts]
outputs = llm.chat(mensagens, sampling_params)
Subindo o servidor compatível com a API da OpenAI
Esta é a principal razão de ser do vLLM em produção: ele fala o protocolo da OpenAI, então qualquer aplicação, SDK ou agente que já usa a OpenAI aponta para ele trocando apenas a base_url.
vllm serve Qwen/Qwen2.5-1.5B-Instruct
O servidor sobe em http://localhost:8000. Teste:
curl http://localhost:8000/v1/models
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Explique inodes em duas frases."}],
"temperature": 0.7
}'
Do lado do cliente Python, é o SDK oficial da OpenAI sem gambiarra:
from openai import OpenAI
client = OpenAI(api_key="EMPTY", base_url="http://localhost:8000/v1")
resposta = client.chat.completions.create(
model="Qwen/Qwen2.5-1.5B-Instruct",
messages=[{"role": "user", "content": "Escreva um one-liner que conta arquivos por extensão."}],
)
print(resposta.choices[0].message.content)
Para exigir autenticação, suba com --api-key SEGREDO — ou, melhor, com a variável VLLM_API_KEY, porque chave em linha de comando aparece no ps aux. O servidor aceita múltiplas chaves, o que facilita rotação sem downtime.
Atenção: o --api-key autentica apenas os prefixos /v1, /v2 e /inference. Endpoints como /invocations (que expõe a mesma capacidade de inferência), /pooling, /classify e os de controle /pause, /resume e /abort_requests continuam abertos. Se você usa --host 0.0.0.0, ponha o vLLM atrás de um proxy reverso que libere só as rotas que você quer expor — nunca deixe a porta 8000 acessível direto da rede.
Os parâmetros que realmente importam
O padrão funciona, mas quase nunca é o que você quer em produção:
vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 --port 8000 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--tensor-parallel-size 1 \
--api-key MINHA_CHAVE
--gpu-memory-utilization— fração da VRAM que o vLLM pode ocupar (padrão 0.92 nas versões atuais; era 0.9 até a v0.11). Baixe se a GPU também roda seu monitor ou outro processo; suba se a placa é dedicada. Quanto maior, mais KV cache, mais requisições simultâneas.--max-model-len— janela de contexto máxima. É o parâmetro nº 1 para resolver erro de memória na inicialização: modelos anunciam contextos gigantes (128k) que exigem KV cache que você não tem. Corte para o que a sua aplicação de fato usa.--tensor-parallel-size— número de GPUs para dividir o modelo. Use quando o modelo não cabe em uma placa só.--dtype—autoresolve bem;bfloat16em placas Ampere ou mais novas,float16em placas antigas.--quantization— para pesos AWQ, GPTQ ou FP8, que cortam a VRAM pela metade ou mais.--served-model-name— o nome que a API expõe, útil para não vazar o caminho do Hugging Face para os clientes.--cpu-offload-gb— quantos GiB dos pesos empurrar para a RAM, por GPU. Salva o dia quando o modelo não cabe, mas cada forward pass passa pelo barramento: só use se a alternativa for não rodar.
Modelos com licença restrita (Llama, Gemma) exigem token do Hugging Face:
export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxx
vllm serve meta-llama/Llama-3.1-8B-Instruct
Rodando como serviço no systemd
Em um servidor de verdade, o vLLM tem que subir sozinho no boot. Crie /etc/systemd/system/vllm.service:
[Unit]
Description=vLLM OpenAI-compatible server
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=vllm
Group=vllm
WorkingDirectory=/opt/vllm
Environment="HF_HOME=/opt/vllm/hf"
Environment="VLLM_API_KEY=MINHA_CHAVE"
ExecStart=/opt/vllm/.venv/bin/vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 --port 8000 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192
Restart=always
RestartSec=10
TimeoutStartSec=600
[Install]
WantedBy=multi-user.target
O TimeoutStartSec alto é proposital: na primeira execução o modelo é baixado do Hugging Face e os kernels são compilados, e com o padrão de 90 segundos o systemd desistiria no meio do caminho. Repare que ele só tem efeito porque a unit usa Type=exec — com Type=simple, o systemd considera o serviço iniciado já no fork() e o timeout de partida nunca chega a valer.
O serviço roda com usuário e ambiente próprios — o .venv que você criou no seu diretório pessoal não serve aqui:
sudo useradd -r -s /usr/sbin/nologin -d /opt/vllm vllm
sudo mkdir -p /opt/vllm/hf
sudo chown -R vllm:vllm /opt/vllm
# instala o vLLM dentro de /opt/vllm, como o usuário do serviço
sudo -u vllm uv venv --python 3.12 --seed /opt/vllm/.venv
sudo -u vllm env VIRTUAL_ENV=/opt/vllm/.venv uv pip install vllm --torch-backend=auto
sudo systemctl daemon-reload
sudo systemctl enable --now vllm
journalctl -u vllm -f
Problemas comuns
CUDA out of memoryna inicialização — quase sempre é o KV cache, não os pesos. Reduza--max-model-lenprimeiro, depois--gpu-memory-utilization, e só então troque de modelo ou parta para pesos quantizados.- Erro de
/dev/shmou workers morrendo em Docker — faltou--ipc=host. - Modelo responde bobagem em modo chat — você usou
generate()em vez dechat(), e o chat template não foi aplicado. - A subida do serviço demora muito — é a compilação dos kernels e a captura dos CUDA graphs, que acontecem na inicialização do engine, não na primeira requisição. Os artefatos ficam em
~/.cache/vllme são reaproveitados nos boots seguintes; em Docker, monte um volume nesse caminho para não recompilar a cada container. Para pular a etapa, ao custo de decode mais lento, use--enforce-eager. - Download travando ou 401 — modelo com licença restrita: aceite os termos na página do Hugging Face e exporte
HF_TOKEN.
Fechando
O vLLM é o exemplo raro de projeto de pesquisa que virou infraestrutura padrão porque resolveu o problema certo: tratou a VRAM de uma GPU como o kernel trata a RAM de um servidor. A lição é velha — paginação, tabela de páginas, copy-on-write — só o hardware é novo.
Para continuar: entenda a diferença entre GPU e TPU, veja como montar um servidor de IA recondicionado com 64 GB de VRAM para hospedar tudo isso sem quebrar o orçamento, ou compare com o caminho mais simples de rodar IA local com Ollama.
Links oficiais: vllm.ai · github.com/vllm-project/vllm · documentação · paper do PagedAttention