Otimização de Inferência
Visão geral
Otimização de inferência é o conjunto de técnicas usadas para fazer um modelo treinado rodar mais rápido, mais barato e/ou dentro de orçamentos mais restritos de memória e energia no momento do deployment, mantendo a acurácia (ou a qualidade da saída) dentro de limites aceitáveis. Isso importa tanto em sistemas clássicos de aprendizado de máquina (machine learning) quanto em sistemas modernos de modelos de linguagem grandes (LLMs), onde o custo de inferência muitas vezes domina o custo total quando um modelo está em produção.
Objetivos comuns incluem:
- Menor latência (tempo até o primeiro token, tempo de requisição de ponta a ponta)
- Maior throughput (requisições/seg ou tokens/seg por dispositivo)
- Menor pegada de memória (caber em GPUs/CPUs menores ou no dispositivo)
- Menor energia/custo (gasto em nuvem, uso de bateria)
- Atender aos SLOs do produto (veja SLOs para Funcionalidades de IA)
Técnicas centrais cobertas aqui:
- Quantização (quantization) (pesos/ativações de menor precisão)
- Destilação (distillation) (treinar um “aluno” menor/mais rápido para imitar um “professor” maior)
- Poda / esparsidade (pruning / sparsity) (remover pesos, canais ou componentes inteiros)
- Otimizações em nível de sistemas (systems-level optimizations) (cache KV, kernels otimizados, compilação, batching, decodificação especulativa etc.)
Este artigo foca em acelerações em nível de modelo e em nível de runtime; para padrões de arquitetura de serving, veja Serving de LLM e Serving de Modelos. Para direcionadores de custo e mentalidade de benchmarking, veja Custo/Desempenho.
O que de fato torna a inferência lenta?
O tempo de inferência normalmente é dominado por um (ou mais) de:
- Trabalho limitado por computação (compute-bound work): muitos FLOPs (por exemplo, grandes multiplicações de matrizes em transformers).
- Largura de banda de memória (memory bandwidth): mover pesos/ativações da memória para unidades de computação (frequentemente o gargalo em modelos grandes).
- Overheads de lançamento de kernel (kernel launch / overheads): overhead de Python, muitas operações pequenas, pouca fusão.
- Dependências sequenciais (sequential dependencies): a decodificação autorregressiva gera tokens um a um (limita paralelismo).
- Fila e contenção (queueing and contention): formação de lote (batch), GPUs multi-tenant, gargalos de CPU na tokenização.
Um modelo mental útil:
- Prefill (processar o prompt) tende a ser mais paralelizável e se beneficia de batching.
- Decode (geração token a token) tende a ser sensível à latência e frequentemente se torna limitado por memória (memory-bound) devido a leituras/escritas do cache KV.
A otimização de inferência funciona reduzindo:
- a quantidade de computação,
- a quantidade de tráfego de memória,
- ou o overhead de orquestrar o trabalho.
Quantização
O que é
Quantização representa números (pesos, ativações, cache KV) usando menos bits do que FP32 — comumente FP16/BF16, INT8, ou até INT4. Os principais benefícios:
- Modelo menor (menos uso de VRAM/DRAM)
- Maior largura de banda efetiva (mais pesos cabem em cache; menos bytes são movidos)
- Multiplicações de matrizes potencialmente mais rápidas se o hardware tiver unidades rápidas de baixa precisão
O trade-off é o erro de quantização (quantization error), que pode reduzir a acurácia ou alterar saídas de LLMs.
Tipos comuns de quantização
Quantização apenas de pesos (weight-only quantization)
- Quantiza pesos (por exemplo, INT8/INT4), mantém ativações em FP16/BF16.
- Muitas vezes é boa para LLMs porque quantizar ativações pode ser mais complexo.
- Melhora a pegada de memória e pode melhorar a velocidade se os kernels forem otimizados.
Quantização de pesos + ativação (weight + activation quantization) (INT8)
- Quantiza pesos e ativações.
- Comum em visão/CNNs e em muitos modelos encoder.
- Em geral precisa de calibração ou treinamento ciente de quantização (QAT) para manter a acurácia.
Quantização do cache KV (KV cache quantization) (LLMs)
- O cache KV pode dominar a memória durante decodificação com contextos longos.
- Quantizar KV (por exemplo, FP16 → INT8) pode reduzir memória e melhorar throughput em sequências longas, com potencial impacto na qualidade.
FP16/BF16/FP8
- Não é “quantização inteira”, mas ainda é de menor precisão que FP32.
- FP16/BF16 é padrão em GPUs; FP8 é cada vez mais usado em GPUs da classe Hopper para velocidade/throughput em algumas pilhas.
Quantização pós-treinamento (PTQ) vs treinamento ciente de quantização (QAT)
PTQ: quantizar após o treinamento usando um conjunto de dados de calibração.
- Caminho mais rápido para deployment; frequentemente “bom o suficiente”.
- Risco: maior queda de acurácia para modelos sensíveis.
QAT: simular quantização durante o treinamento/ajuste fino.
- Tipicamente melhor retenção de acurácia.
- Mais engenharia e custo de treinamento.
Granularidade e escalonamento
A quantização normalmente mapeia floats para inteiros usando uma escala (e, opcionalmente, um zero-point):
- Escalonamento por tensor (per-tensor scaling): uma escala para um tensor inteiro (rápido, menos preciso).
- Escalonamento por canal (per-channel scaling): escala separada por canal de saída (frequentemente melhor para pesos).
- Quantização por grupos (group-wise quantization): usada com frequência em métodos INT4 apenas de pesos para LLMs (equilibra acurácia e overhead de metadados).
Esses detalhes importam porque determinam quanto erro é introduzido e quão eficientemente o runtime consegue computar.
Exemplo prático: quantização dinâmica do PyTorch (CPU)
A quantização dinâmica frequentemente é eficaz para modelos do tipo transformer em CPU porque mira camadas nn.Linear.
import torch
from torch import nn
# Suppose you have a trained model on CPU (e.g., a small encoder).
model = ... # nn.Module
model.eval()
# Dynamic quantization: weights become int8, activations quantized on the fly.
qmodel = torch.quantization.quantize_dynamic(
model,
{nn.Linear},
dtype=torch.qint8
)
# Run inference
with torch.no_grad():
out = qmodel(torch.randn(1, 128))
Notas:
- Os ganhos de velocidade dependem fortemente do suporte a instruções da CPU (por exemplo, AVX512-VNNI) e da estrutura do modelo.
- A quantização dinâmica é menos comum para grandes LLMs decoder-only no modo eager do PyTorch; runtimes especializados são típicos.
Exemplo prático: quantização 4-bit apenas de pesos para LLMs (GPU)
Muitos deployments de LLMs usam quantização 8-bit ou 4-bit apenas de pesos com kernels otimizados.
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "your-llm"
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
device_map="auto",
load_in_4bit=True, # requires bitsandbytes-compatible setup
torch_dtype="auto"
)
prompt = "Explain quantization in one paragraph."
inputs = tok(prompt, return_tensors="pt").to(model.device)
out = model.generate(**inputs, max_new_tokens=120)
print(tok.decode(out[0], skip_special_tokens=True))
Na prática, throughput e qualidade dependem de:
- esquema de quantização (por exemplo, tamanho do grupo, simétrico vs assimétrico),
- implementação do kernel,
- e se você está limitado por memória (comum no decode).
Calibração e avaliação
Para PTQ, os dados de calibração (calibration data) devem corresponder às distribuições de produção (comprimentos de entrada, domínios, modalidades). Após quantizar, avalie com:
- métricas de tarefa (acurácia/F1 etc.),
- e métricas comportamentais para LLMs (win-rate, pontuação por rubrica), veja Avaliação em Produção.
Também monitore regressões pós-deploy (latência e qualidade), veja Monitoramento.
Quando a quantização mais ajuda
A quantização tende a brilhar quando a inferência é limitada por largura de banda de memória:
- Modelos grandes cujos pesos não cabem em cache
- Workloads de LLMs com muito decode
- Deployments na borda com restrições de memória apertadas (veja Inferência na Borda / No Dispositivo)
A quantização pode ajudar menos (ou até prejudicar) quando:
- os kernels não são otimizados para o seu formato de quantização,
- você se torna limitado por computação sem aceleração de baixa precisão,
- a sensibilidade de acurácia é alta e você não pode arcar com QAT/destilação.
Poda e esparsidade
O que é
Poda (pruning) remove parâmetros ou estruturas de um modelo para reduzir computação e/ou memória:
- Poda não estruturada (unstructured pruning): zerar pesos individuais (esparsidade irregular).
- Poda estruturada (structured pruning): remover canais, heads, neurônios ou blocos inteiros (amigável ao hardware).
- Esparsidade N:M (N:M sparsity): padrões estruturados como “2 de 4 pesos são não-zero” (suportado por algumas GPUs da NVIDIA).
A ideia central: muitos modelos são superparametrizados; você pode remover partes com perda limitada de qualidade se fizer ajuste fino depois.
Não estruturada vs estruturada: o ponto prático chave
- Esparsidade não estruturada pode produzir altas taxas de esparsidade (por exemplo, 80–90%), mas ganhos de velocidade exigem kernels esparsos e frequentemente hardware/runtime especializado. Caso contrário, você só tem uma multiplicação de matrizes densa com muitos zeros (sem ganho real de velocidade).
- Poda estruturada geralmente gera melhorias mais confiáveis de latência porque reduz dimensões de matrizes — kernels densos padrão ficam mais rápidos.
Alvos típicos de poda em transformers
- Heads de atenção (remover heads de baixa importância)
- Dimensão intermediária de MLP (reduzir o tamanho do FFN)
- Camadas inteiras (layer dropping)
- Especialistas de MoE (reduzir especialistas ativos, ou destilar para denso)
Fluxo de trabalho
- Escolher uma estratégia de poda e a esparsidade/tamanho alvo.
- Podar com base em magnitude, análise de sensibilidade ou máscaras aprendidas.
- Ajustar fino (fine-tune) para recuperar qualidade (frequentemente essencial).
- Validar latência com o runtime real (não assuma esparsidade ⇒ velocidade).
Esparsidade N:M (consciente de hardware)
Alguns aceleradores oferecem ganhos para padrões específicos (por exemplo, 2:4). Isso pode trazer ganhos reais se:
- sua poda impõe o padrão correto,
- e sua pilha de inferência usa kernels esparsos.
Este é um bom exemplo de por que otimização de inferência é co-design de sistemas + modelo.
Destilação (knowledge distillation)
O que é
Destilação treina um modelo aluno menor/mais rápido para corresponder a um modelo professor maior. Em vez de treinar apenas com rótulos “duros”, o aluno aprende com as saídas do professor (frequentemente chamadas de “alvos suaves”), capturando informação mais rica.
A destilação é especialmente valiosa quando:
- você quer grandes ganhos de velocidade (por exemplo, modelo 10× menor),
- a quantização sozinha não é suficiente,
- ou você precisa de um modelo que caiba no dispositivo.
A destilação também é uma estratégia comum para criar LLMs “mini” ajustados para um domínio estreito.
Objetivos de destilação
Componentes comuns de loss:
- Correspondência de logits (logit matching) (classificação): o aluno corresponde aos logits do professor, frequentemente com escalonamento por temperatura.
- Divergência KL (KL divergence) entre distribuições do professor e do aluno.
- Correspondência de estado oculto (hidden-state matching): corresponder representações intermediárias.
- Destilação em nível de sequência (sequence-level distillation) (LLMs): o professor gera saídas; o aluno treina nelas (frequentemente combinado com ajuste fino supervisionado).
Uma forma conceitual simples (classificação):
[ \mathcal{L} = \alpha \cdot \mathcal{L}_{hard}(y, p_s) + (1-\alpha) \cdot T^2 \cdot KL(p_t^{(T)} \Vert p_s^{(T)}) ]
Onde (T) é a temperatura; (p_t, p_s) são probabilidades do professor/aluno.
Exemplo prático: destilando um classificador de texto (esboço)
# Pseudocode sketch (not runnable end-to-end)
for batch in dataloader:
x, y = batch
with torch.no_grad():
teacher_logits = teacher(x)
student_logits = student(x)
hard_loss = cross_entropy(student_logits, y)
soft_loss = kl_div(
log_softmax(student_logits / T),
softmax(teacher_logits / T)
) * (T * T)
loss = alpha * hard_loss + (1 - alpha) * soft_loss
loss.backward()
optimizer.step()
Destilação para LLMs na prática
Padrões comuns:
- Destilação de instruções (instruction distillation): o professor produz respostas de alta qualidade para prompts curados; o aluno faz ajuste fino nisso.
- Destilação de domínio (domain distillation): o professor gera perguntas e respostas, resumos ou saídas estruturadas específicas de domínio.
- Destilação de política (policy distillation): comprimir um modelo “com uso de ferramentas” ou “alinhado” em um menor.
O controle de qualidade importa: se o professor estiver errado ou for inseguro, o aluno herdará esse comportamento (muitas vezes com mais confiança). Em ambientes de produção, trate dados sintéticos do professor como um dataset que requer validação e governança (veja Validação de Dados e Privacidade em Logs quando prompts contêm conteúdo sensível).
Destilação vs quantização vs poda
- A destilação pode gerar as maiores reduções de latência porque muda tamanho/arquitetura do modelo.
- A quantização geralmente é a mais rápida de implementar e não requer treinamento (PTQ).
- A poda fica no meio; a poda estruturada pode ser muito eficaz se você puder ajustar fino e sua arquitetura for adequada.
Na prática, equipes frequentemente combinam:
- Destilar → depois quantizar o aluno menor.
- Podar → ajustar fino → quantizar para deployment.
Outras técnicas de alto impacto para velocidade de inferência
Quantização/destilação/poda são centrais, mas deployments modernos (especialmente LLMs) dependem fortemente de otimizações de runtime e algorítmicas.
Fusão de operadores e compilação
Muitos modelos em frameworks eager rodam como muitas operações pequenas. Compiladores e otimizadores de grafo podem fundir operações e escolher kernels melhores.
Abordagens comuns:
- Pilhas de torch compile (por exemplo, Inductor)
- Compilação no estilo XLA
- Exportação ONNX + otimizações de grafo do ONNX Runtime
- Construção de engine no estilo TensorRT
Orientação prática:
- Meça a latência de ponta a ponta após compilação, não apenas o tempo de kernel.
- Cuidado com dinamismo de shape (comprimentos variáveis de sequência), que pode reduzir oportunidades de fusão.
Otimizações de atenção (transformers)
A atenção pode ser cara; kernels otimizados podem acelerar significativamente.
- Kernels no estilo FlashAttention: reduzem tráfego de memória com tiling e recomputação quando necessário (trocando computação por largura de banda).
- Atenção paginada (paged attention) / paginação de cache KV (KV cache paging): gerencia o cache KV de forma mais eficiente para muitas sequências concorrentes.
- Atenção multi-query (multi-query attention, MQA) / atenção grouped-query (grouped-query attention, GQA): reduz tamanho do cache KV ao compartilhar chaves/valores entre heads (escolha arquitetural; frequentemente presente em LLMs modernos).
Contexto relacionado: Arquitetura Transformer.
Gerenciamento do cache KV
Para LLMs decoder-only, o cache KV é central:
- Reusar o cache KV entre passos de decode (padrão).
- Quantizar o cache KV (redução de memória; pode melhorar throughput em contextos longos).
- Limitar contexto máximo ou usar recuperação (retrieval) para evitar contextos enormes (tema de design de sistema; veja Padrões de Design de Sistemas de LLM).
Batching e agendamento
Mesmo um modelo perfeitamente otimizado pode ter desempenho ruim se o serving for ineficiente:
- Batching dinâmico / batching contínuo (dynamic batching / continuous batching): combinar requisições que chegam próximas no tempo.
- Micro-batching: trocar um pouco de latência por maior throughput.
- Separação de prefill/decode: agendar processamento do prompt e geração de token de forma diferente para melhorar a utilização de GPU.
Isso normalmente é implementado na camada de serving; veja Serving de LLM e Cache & Rate Limiting.
Decodificação especulativa (LLMs)
Decodificação especulativa (speculative decoding) usa um modelo pequeno rascunho para propor múltiplos tokens, e então o modelo maior alvo os verifica. Se muitos tokens forem aceitos, você reduz o número de forward passes caros do modelo alvo.
Quando ajuda:
- O modelo rascunho é muito mais rápido
- A taxa de aceitação é alta (as propostas do rascunho batem com frequência com as saídas do alvo)
- Seu workload tem muito decode
Trade-offs:
- Mais complexidade (dois modelos, orquestração)
- Benefícios dependem da distribuição de prompts e de configurações de geração
Saída antecipada e cascatas
Para algumas tarefas, você pode parar cedo quando a confiança é alta:
- Redes com saída antecipada (early-exit networks) (o classificador sai em camadas intermediárias)
- Cascatas/roteadores de modelo (model cascades/routers): tentar um modelo pequeno primeiro, e recorrer a um maior se necessário
Isso costuma ser uma otimização de produto/sistema em vez de puramente de modelo; veja Padrões de Design de Sistemas de LLM.
Restrições de entrada e saída
Às vezes, o token mais barato é o que você não gera:
- Reduzir o máximo de tokens de saída quando aceitável
- Usar saídas estruturadas (schemas JSON) para reduzir retries
- Melhorar prompts para encurtar respostas (apps com LLM)
- Usar melhor tokenização ou normalizar entradas para reduzir comprimento de sequência (específico de domínio)
Estas são “otimizações de inferência” em termos de custo mesmo se o modelo não mudar.
Colocando em prática: um fluxo de trabalho de otimização
1) Defina o que você está otimizando
Seja explícito sobre:
- Percentis de latência (p50/p95/p99)
- Throughput (tokens/seg, req/seg)
- Teto de memória (VRAM/DRAM)
- Limiares de qualidade (métrica de tarefa, win-rate, restrições de segurança)
Conecte isso a objetivos operacionais e orçamentos; veja Custo/Desempenho.
2) Faça profiling do baseline
Colete:
- Decomposição de latência de ponta a ponta (tokenização, prefill, decode, pós-processamento)
- Utilização e memória de GPU
- Tamanho de batch e atrasos de fila
- Operadores quentes (matmul, attention, layernorm, softmax)
3) Escolha a técnica mais simples que atenda aos objetivos
Heurísticas típicas de decisão:
- Precisa caber em hardware menor: quantização (INT8/INT4), destilação, otimizações de cache KV.
- Precisa de menor latência p95: compilação, otimizações de kernel, reduzir comprimento de saída, decodificação especulativa.
- Precisa de maior throughput: batching, quantização, atenção otimizada, melhor agendamento.
4) Valide a qualidade corretamente
Quantização e poda podem causar regressões sutis:
- Mudanças no comportamento de tokens raros
- Falhas de segurança/formatação
- Degradação específica de domínio
Use avaliação offline e depois rollout em fases/avaliação online; veja Avaliação em Produção.
5) Faça deploy com monitoramento e rollback
Acompanhe:
- percentis de latência,
- taxas de OOM,
- fragmentação de memória de GPU,
- proxies de qualidade (feedback do usuário, checagens automatizadas),
- e drift (mudanças na distribuição de entrada podem invalidar a calibração).
Veja Monitoramento e Registro de Modelos para práticas seguras de promoção/rollback.
Armadilhas comuns
- Assumir que compressão ⇒ velocidade: poda ou INT4 sem kernels otimizados pode apenas reduzir memória, não latência.
- Benchmarking da coisa errada: microbenchmarks podem parecer ótimos enquanto o p95 de ponta a ponta mal muda por causa de filas ou gargalos de CPU.
- Ignorar decode vs prefill: uma otimização pode ajudar prefill mas não decode (ou vice-versa).
- Dados de calibração ruins: PTQ pode falhar se a calibração não corresponder a entradas reais.
- Otimizar demais antes de arrumar o serving: batching, agendamento e caching frequentemente entregam ganhos maiores do que ajustes de baixo nível nas primeiras iterações.
Resumo
A otimização de inferência é uma caixa de ferramentas que abrange métodos numéricos (quantização), compressão de modelo (poda, destilação) e melhorias de sistemas/runtime (compilação, kernels, batching, algoritmos de decodificação). Os melhores resultados geralmente vêm da combinação de abordagens e da validação com workloads realistas e alvos de qualidade.
Se você está otimizando LLMs em produção, um caminho comum de alto ROI é:
- Ajustar corretamente a pilha de serving (batching/agendamento/cache KV).
- Usar kernels de atenção otimizados e compilação onde estiver estável.
- Aplicar quantização apenas de pesos (INT8/INT4) com avaliação cuidadosa.
- Considerar destilação ou cascatas quando você precisa de melhorias em degrau de custo/latência.