vLLM: servindo LLMs no Linux com alta vazão — história, instalação e primeiros passos

vLLM servindo modelos de linguagem em GPU no Linux

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/2025vLLM 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-Metal para 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ó.
  • --dtypeauto resolve bem; bfloat16 em placas Ampere ou mais novas, float16 em 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 memory na inicialização — quase sempre é o KV cache, não os pesos. Reduza --max-model-len primeiro, depois --gpu-memory-utilization, e só então troque de modelo ou parta para pesos quantizados.
  • Erro de /dev/shm ou workers morrendo em Docker — faltou --ipc=host.
  • Modelo responde bobagem em modo chat — você usou generate() em vez de chat(), 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/vllm e 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