Phase 17 - Lesson 22
Testes de Carga em APIs de LLM — Por que o k6 e o Locust Mentem
Os testadores de carga tradicionais não foram projetados para respostas em streaming, comprimentos de saída variáveis, métricas em nível de token ou saturação de GPU. Duas armadilhas afetam a maioria das equipes. A armadilha do GIL: a medição em nível de token do Locust executa a tokenização sob o GIL do Python, o que compete com a geração de requisições sob alta concorrência; o acúmulo de tokenização então infla a latência entre tokens reportada — seu cliente é o gargalo, não o servidor. A armadilha da uniformidade de prompts: prompts idênticos em um loop testam apenas um ponto na distribuição de tokens; o tráfego real possui comprimentos variáveis e correspondências de prefixo diversas. O LLMPerf corrige isso com
--mean-input-tokens+--stddev-input-tokens. Mapeamento de ferramentas em 2026: especializadas em LLM (GenAI-Perf, LLMPerf, LLM-Locust, guidellm) para precisão em nível de token; k6 v2026.1.0 + k6 Operator 1.0 GA (Set 2025) — ciente de streaming, distribuído nativo de Kubernetes via CRDs TestRun/PrivateLoadZone, melhor para portas de CI/CD; Vegeta para saturação de taxa constante em Go; Locust 2.43.3 apenas com a extensão LLM-Locust para streaming. Padrões de carga: estado estável, rampa, pico (teste de autoescalonamento), imersão (vazamentos de memória).
Tipo: Build Linguagens: Python (stdlib, gerador de prompts realistas de brinquedo + coletor de latência) Pré-requisitos: Phase 17 · 08 (Inference Metrics), Phase 17 · 03 (GPU Autoscaling) Tempo: ~75 minutos
Objetivos de Aprendizagem
- Explicar os dois antipadrões (armadilha do GIL, armadilha da uniformidade de prompts) que fazem com que testadores de carga genéricos mintam para APIs de LLM.
- Escolher uma ferramenta para um determinado propósito: LLMPerf (execução de benchmark), k6 + extensão de streaming (porta de CI), guidellm (sintético em larga escala), GenAI-Perf (referência da NVIDIA).
- Projetar quatro padrões de carga (estável, rampa, pico, imersão) e nomear o modo de falha que cada um captura.
- Construir uma distribuição de prompts realista usando a média + desvio padrão de tokens de entrada em vez de um comprimento fixo.
O Problema
Você testou seu endpoint de LLM com o k6 com 500 usuários concorrentes. Ele aguentou. Você fez o deploy. Em produção com 200 usuários reais o serviço caiu — o TTFT P99 explodiu, as GPUs travaram.
Duas coisas aconteceram. Primeiro, o k6 enviou 500 prompts idênticos — seu agrupamento de requisições (request-coalescing) e cache de prefixo fizeram parecer que você estava processando 500 decodificações concorrentes quando, na verdade, estava processando apenas uma. Segundo, o k6 não rastreia a latência entre tokens (inter-token latency) em respostas de streaming da forma como o usuário final a experimenta; ele vê uma conexão HTTP, não 500 tokens chegando em intervalos variados.
O teste de carga para LLMs é uma disciplina própria.
O Conceito
A armadilha do GIL (Locust)
O Locust usa Python e executa a tokenização no lado do cliente sob o GIL. Sob alta concorrência, o tokenizador entra na fila atrás da geração de requisições. A latência entre tokens reportada inclui o acúmulo de tokenização no lado do cliente. Você acha que o servidor está lento; mas é o ambiente de teste.
Correção: a extensão LLM-Locust move a tokenização para processos separados, ou use um ambiente de linguagem compilada (k6, LLMPerf usando tokenizers.rs).
A armadilha da uniformidade de prompts
Todos os testadores de carga conhecidos permitem configurar um único prompt. Em um teste de loop de 10.000 iterações, o exato mesmo prompt é enviado todas as vezes. O servidor vê o mesmo prefixo sempre — as taxas de acerto do cache de prefixo (prefix cache hits) se aproximam de 100%, e a taxa de transferência parece ótima.
Correção: faça amostragem a partir de uma distribuição de prompts. O LLMPerf usa --mean-input-tokens 500 --stddev-input-tokens 150 — comprimentos diversos, conteúdos diversos.
Quatro padrões de carga
- Steady-state — RPS constante por 30-60 min. Captura: regressões de desempenho de linha de base.
- Ramp — aumenta linearmente o RPS de 0 até o alvo ao longo de 15 min. Captura: ponto de ruptura de capacidade, anomalias de aquecimento (warm-up).
- Spike — RPS repentino de 3-10x por 2 min e depois retorna ao normal. Captura: latência de autoescalonamento, saturação de fila, impacto de partida a frio (cold-start).
- Soak — steady-state por 4-8 horas. Captura: vazamentos de memória, desvio de pool de conexões (connection-pool drift), estouro de observabilidade.
Mapeamento de ferramentas para 2026
LLMPerf (Anyscale) — Python, mas com tokenização baseada em Rust. Prompts com média/desvio padrão. Ciente de streaming. Melhor opção padrão para execuções de desempenho.
NVIDIA GenAI-Perf — Referência da NVIDIA. Usa o cliente Triton; cobertura abrangente de métricas. Observe que o seu ITL exclui o TTFT; o do LLMPerf o inclui. As duas ferramentas produzem TPOT diferentes para o mesmo servidor.
LLM-Locust (TrueFoundry) — Extensão do Locust que corrige a armadilha do GIL. DSL familiar do Locust + métricas de streaming.
guidellm — benchmarking sintético de larga escala.
k6 v2026.1.0 + k6 Operator 1.0 GA (Set 2025):
- O próprio k6 (Go, compilado, sem GIL) adicionou métricas cientes de streaming.
- O k6 Operator usa CRDs TestRun / PrivateLoadZone para testes distribuídos nativos do Kubernetes.
- Melhor para portas de CI/CD e testes de SLA.
Vegeta — Go, mais simples que o k6. Saturação de HTTP com taxa constante. Não é ciente de LLM, mas é bom para testes de gateway / limite de taxa.
Locust 2.43.3 stock — possui a armadilha do GIL para LLM. Apenas com a extensão LLM-Locust.
Porta de SLA no CI
Execute o k6 no PR com:
- 30-50 iterações, cada uma no RPS de linha de base.
- Porta: TTFT P50/P95, 5xx < 5%, TPOT abaixo do limite.
- Quebre o build em caso de violação.
Distribuição de prompts realista
Construa a partir de amostras de tráfego real (se as tiver) ou de distribuições publicadas (por exemplo, prompts do ShareGPT para chat, HumanEval para código). Forneça a média + desvio padrão para o LLMPerf. Evite loops com um único prompt a todo custo.
Números que você deve lembrar
- k6 Operator 1.0 GA: Setembro de 2025.
- k6 v2026.1.0: métricas cientes de streaming.
- Execução típica do LLMPerf: 100-1000 requisições com concorrência X.
- Porta de CI típica: 30-50 iterações por PR.
- Quatro padrões: estável, rampa, pico, imersão.
Use
code/main.py simula um teste de carga com uma distribuição realista de prompts, mede o TPOT efetivo e demonstra a armadilha do prompt uniforme.
Entregue
Esta lição produz outputs/skill-load-test-plan.md. Dados o volume de trabalho e o SLA, escolhe a ferramenta e projeta os quatro padrões de carga.
Exercícios
- Execute
code/main.py. Compare a distribuição uniforme versus a realista — onde está a diferença? - Escreva o script do k6 para uma porta de CI: TTFT P95 < 800 ms com 100 concorrentes, tempo de execução de 5 minutos.
- Seu teste de imersão (soak) mostra a memória crescendo 50 MB/hora. Nomeie três causas e a instrumentação para escolher entre elas.
- Teste de pico de 10 RPS para 100 RPS. Qual é o tempo de recuperação esperado se a pilha de produção Karpenter + vLLM estiver instalada (Phase 17 · 03 + 18)?
- O GenAI-Perf reporta TPOT=6ms; o LLMPerf reporta TPOT=11ms no mesmo servidor. Explique.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| LLMPerf | "o harness de LLM" | Ferramenta de benchmark da Anyscale, ciente de streaming |
| GenAI-Perf | "ferramenta da NVIDIA" | Harness de referência da NVIDIA |
| LLM-Locust | "Locust para LLMs" | Extensão do Locust que corrige a armadilha do GIL |
| guidellm | "benchmark sintético" | Ferramenta de síntese em larga escala |
| k6 Operator | "k6 para K8s" | k6 distribuído baseado em CRD |
| GIL trap | "sobrecarga do cliente Python" | O acúmulo de tokenização infla a latência reportada |
| Prompt-uniformity trap | "a mentira do prompt único" | Loop com o mesmo prompt atinge o cache, inflando a taxa de transferência |
| Steady-state | "carga constante" | RPS estável por N minutos |
| Ramp | "subida linear" | De 0 até o alvo ao longo da duração |
| Spike | "teste de explosão" | Multiplicador repentino de carga e depois reversão |
| Soak | "teste longo" | Horas de execução para detecção de vazamentos |