vLLM e PagedAttention: serving high-throughput
- ⬜🚢 Ollama em produção: model management, Docker, monitoring(Local LLMs & Edge AI)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Em setembro de 2023, Woosuk Kwon e equipe (Sky Computing Lab, UC Berkeley) publicaram "Efficient Memory Management for Large Language Model Serving with PagedAttention" no SOSP. O paper trouxe ao mundo de LLM serving uma técnica que sistemas operacionais usam há 50 anos: memória virtual paginada. O resultado — vLLM — entrega 10-24× mais throughput que HuggingFace TGI antigo e tornou-se a infraestrutura padrão para self-hosted serving em 2026.
O problema: KV cache desperdiça memória
Em LLM serving tradicional, alocava-se KV cache como tensor contíguo de tamanho [max_seq_len × n_heads × d_head] por sequência. Problemas:
Resultado prático: HuggingFace TGI v1 conseguia ~10 sequências concorrentes em uma A100 80GB com Llama 13B em FP16. vLLM com PagedAttention atinge 50-80 sequências no mesmo hardware. 5-8× mais throughput pela mesma memória.
PagedAttention: KV cache em blocos
A inovação central: tratar KV cache como memória virtual paginada. Divide-se em blocos físicos de tamanho fixo (default 16 tokens). Cada sequência mantém uma block table mapeando posições lógicas (token 0..n) para blocos físicos arbitrários. Os blocos físicos não precisam ser contíguos na VRAM.
# Pseudocódigo de PagedAttention CUDA kernel (simplificado)
# Cada thread block processa attention para 1 sequência, varrendo seus blocos físicos
def paged_attention_kernel(
Q, # [num_seqs, num_heads, head_dim]
K_cache, # [num_blocks, num_heads, head_dim, block_size]
V_cache, # idem
block_tables, # [num_seqs, max_num_blocks_per_seq] → physical block id
seq_lens, # [num_seqs]
):
seq_idx = block_idx_x
head_idx = block_idx_y
q = Q[seq_idx, head_idx] # carrega Q em registradores
block_table = block_tables[seq_idx]
seq_len = seq_lens[seq_idx]
# online softmax (FlashAttention-style)
m = -inf; l = 0.0; o = 0.0
for logical_block_idx in range(ceil(seq_len / block_size)):
physical_block_id = block_table[logical_block_idx]
K_block = K_cache[physical_block_id, head_idx] # [head_dim, block_size]
V_block = V_cache[physical_block_id, head_idx]
scores = q @ K_block / sqrt(head_dim) # [block_size]
m_new = max(m, scores.max())
l_new = exp(m - m_new) * l + sum(exp(scores - m_new))
o = exp(m - m_new) * o + V_block @ exp(scores - m_new)
m, l = m_new, l_new
output[seq_idx, head_idx] = o / lContinuous batching + chunked prefill
PagedAttention sozinho economiza memória. O segundo pilar do throughput é o scheduler: a cada step do GPU, o vLLM scheduler reavalia qual conjunto de sequências formar o próximo batch. Sequências que terminaram saem; novas entram; prefills longos são fatiados.
Chunked prefill (vLLM v0.5+, paper "Sarathi-Serve") evita que um prompt longo de 30k tokens bloqueie todo o batch durante seu prefill. O prefill é dividido em chunks de N tokens e cada chunk roda junto de decodes de outras sequências. Resultado: latência por token (decode) cai mesmo quando há prefills enormes na fila.
Prefix caching: o segredo dos chatbots multi-tenant
# Ativar prefix caching
vllm serve meta-llama/Meta-Llama-3.1-70B-Instruct \
--tensor-parallel-size 4 \
--enable-prefix-caching \
--block-size 16 \
--max-model-len 32768 \
--gpu-memory-utilization 0.92Cada bloco físico recebe um hash do seu conteúdo lógico (tokens + hash do bloco anterior). Duas sequências que começam com o mesmo prefixo apontam para os mesmos blocos físicos — prefill é pulado para a parte compartilhada. Workloads que beneficiam:
| Workload | Padrão de prefixo | Ganho típico TTFT |
|---|---|---|
| Chatbot com system prompt 2k tokens | system prompt repetido em 100% das requests | 60-80% redução |
| RAG com mesmo doc base (FAQ) | doc base repetido em 70% das requests | 40-60% redução |
| Code assistant com prelúdio (linguagem, libs) | prelúdio repetido em sessions | 30-50% redução |
| Chat multi-turn | histórico cresce; cada turn reusa anterior | 50-90% por turn |
| Requests aleatórias sem prefixo comum | baixíssima reutilização | ~0% (overhead negligível) |
Tensor parallelism para modelos gigantes
# Llama 3.1 70B em 4× A100 80GB com TP=4
vllm serve meta-llama/Meta-Llama-3.1-70B-Instruct \
--tensor-parallel-size 4 \
--quantization awq_marlin \ # INT4 AWQ + Marlin kernel
--kv-cache-dtype fp8_e5m2 \ # KV cache em FP8 (Hopper+)
--max-model-len 32768 \
--max-num-seqs 128 \
--enable-prefix-caching \
--enable-chunked-prefill \
--gpu-memory-utilization 0.93 \
--port 8000
# Throughput esperado (chat tipico 256 in / 256 out):
# - sem otimizações: ~50 req/min
# - com PagedAttention + cont batching: ~600 req/min
# - + AWQ INT4 + FP8 KV + prefix cache: ~1200-1800 req/min| Estratégia paralelismo | Granularidade | Bandwidth crítica | Quando usar |
|---|---|---|---|
| Tensor Parallelism (TP) | Split intra-layer | NVLink alto (300+ GB/s) | Single node multi-GPU, baixa latência |
| Pipeline Parallelism (PP) | Split inter-layer | Inter-node OK (10-100 GB/s) | Multi-node, throughput-oriented |
| Data Parallelism (DP) | Replica modelo, divide batch | Mínima (só gradients em training) | Múltiplas réplicas inferência |
| Expert Parallelism (EP) | MoE experts em GPUs diferentes | All-to-all | Mixtral, DeepSeek-V3 |
| Sequence Parallelism | Split sequência longa | Inter-GPU | Long context (>128k) |
Qual desperdício de memória a paginação do cache de atenção elimina?
OpenAI API compat e SDK
# Cliente OpenAI apontando para vLLM
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed", # vLLM ignora; configure auth no proxy
)
resp = client.chat.completions.create(
model="meta-llama/Meta-Llama-3.1-70B-Instruct",
messages=[
{"role": "system", "content": "Você é tutor da FFV Academy."},
{"role": "user", "content": "Explique PagedAttention em 3 bullets."},
],
temperature=0.5,
stream=True,
extra_body={
"min_p": 0.05,
"repetition_penalty": 1.0,
"use_beam_search": False,
},
)
for chunk in resp:
print(chunk.choices[0].delta.content or "", end="", flush=True)Endpoints suportados (≈parity com OpenAI): /v1/completions, /v1/chat/completions, /v1/models e /v1/embeddings. Os extras do vLLM viajam no corpo estendido da requisição: min_p, top_k, repetition_penalty, guided_json (JSON schema), guided_regex, guided_choice.
Quando escolher cada serving engine
| Engine | Stack | Force | Use quando |
|---|---|---|---|
| vLLM | Python + CUDA/ROCm/TPU | PagedAttn, prefix cache, multi-GPU TP | Self-hosted alta concorrência, throughput >100 req/s |
| TensorRT-LLM | C++ + CUDA (NVIDIA) | Fused kernels, FP8 nativo | Datacenter NVIDIA, latência mínima absoluta |
| TGI (HuggingFace) | Python + Rust + CUDA | Integração HF Hub, AMD ROCm | Quem já vive no ecossistema HF |
| SGLang | Python + CUDA | RadixAttention (better prefix), constrained gen | Workloads agentes complexos, JSON-heavy |
| Ollama | Go + llama.cpp | UX, multi-model, CPU/GPU consumer | Dev local, multi-model com baixo QPS |
| llama.cpp | C++ puro | Edge, CPU-only, hardware misto | Edge devices, CPU-only, fallback universal |
Perguntas frequentes
❓ vLLM funciona em AMD ou só NVIDIA?
❓ Posso quantizar KV cache no vLLM?
❓ Como ativar speculative decoding no vLLM?
❓ vLLM tem benchmark padrão para comparar?
Referências
Fixando
Por que o cache de prefixo transforma o desempenho de um assistente com prompt de sistema longo?
Qual problema o fatiamento do processamento de prompts longos resolve?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…