Engenharia de inferência · 18 out 2026
Duas GPUs, vazão de sete: como servimos um modelo de 122 bilhões de parâmetros em 2× L40S
O modelo não cabe em uma placa. Dividimos cada camada entre as duas GPUs com paralelismo tensorial, enchemos as placas com dezenas de conversas ao mesmo tempo e desligamos o recurso que deixava uma conversa sozinha mais rápida. Resultado: 640 tokens por segundo somados, sete vezes os 92 tok/s que a melhor configuração entrega para um usuário só.
O problema: 74 GB de pesos, 48 GB de placa
O mangaba-titan é o Qwen3.5-122B-A10B, servido nos servidores da Mangaba no Brasil. Mesmo quantizado em 4 bits (GPTQ Int4), os pesos ocupam 74 GB em disco. Uma L40S tem 48 GB. Não existe configuração de uma placa só que carregue o modelo, e ainda falta lugar para o cache de atenção (KV cache) de cada conversa, que cresce com o contexto.
Com duas placas há duas formas clássicas de repartir o modelo:
- Paralelismo de pipeline (PP): a GPU 0 fica com a primeira metade das camadas e a GPU 1 com a segunda. Simples, pouca comunicação, mas com um usuário só uma placa espera enquanto a outra trabalha.
- Paralelismo tensorial (TP): cada camada é cortada ao meio. As duas GPUs trabalham no mesmo token ao mesmo tempo, cada uma com metade das matrizes, e somam os resultados no fim da camada.
Nosso script de serviço aceita os dois modos (servir.sh tp ou servir.sh pp). Em produção roda TP=2.
Por que TP e não pipeline
Gerar texto é, quase o tempo todo, ler pesos da memória da GPU. Para cada token novo, o modelo precisa passar pelos pesos ativos daquela etapa. Com TP, cada placa lê só a sua metade, e as duas leem em paralelo: a banda de memória efetiva dobra para cada token. No pipeline, um token atravessa primeiro uma placa e depois a outra; com um usuário só, cada GPU fica parada metade do tempo.
O custo do TP é a comunicação: no fim de cada camada as duas placas trocam e somam os resultados parciais. Como o vetor trocado é pequeno (o estado de um token, não os pesos), essa troca cabe no orçamento, e o ganho em leitura de memória compensa.
Ajuda muito o fato de o modelo ser uma mistura de especialistas (MoE): dos 122 bilhões de parâmetros, só cerca de 10 bilhões participam de cada token. A placa lê uma fração pequena dos pesos por passo, e é isso que torna viável um modelo desse tamanho em duas GPUs de 48 GB.
De 2 para 7: o que enche as placas
Dividir o modelo resolve o "cabe ou não cabe". O que multiplica a vazão é atender muitas conversas no mesmo passo. Ler os pesos custa praticamente o mesmo para 1 ou para 40 sequências; se cada leitura gera um token para 40 conversas em vez de uma, a mesma placa produz muito mais texto por segundo. As peças que fazem isso funcionar:
| Ajuste | O que faz |
|---|---|
--tensor-parallel-size 2 | Corta cada camada entre as duas L40S. |
--max-num-seqs 64 | Batching contínuo: até 64 conversas compartilham cada passo do modelo; quem termina sai, quem chega entra sem esperar o lote. |
--kv-cache-dtype fp8 | Guarda o cache de atenção em 8 bits: cabe o dobro de contexto, e portanto o dobro de conversas, na mesma memória. |
--quantization gptq_marlin | Kernel Marlin para os pesos Int4. No nosso teste, 73% mais rápido que o moe_wna16. |
--enable-prefix-caching | Reaproveita o cache de prompts repetidos (instruções de sistema, definições de ferramentas de agentes). |
--enable-chunked-prefill | Quebra prompts longos em pedaços, para que um documento grande não congele a geração das outras conversas. |
--gpu-memory-utilization 0.95 | Entrega quase toda a memória restante ao KV cache. |
A decisão contraintuitiva: desligar o MTP
O Qwen3.5 traz cabeças de MTP (multi-token prediction), um tipo de decodificação especulativa: o modelo chuta os próximos tokens e confirma vários de uma vez. Para um usuário sozinho, funciona: 92 tok/s com MTP contra 76 sem.
Sob carga a conta inverte. Cada chute ocupa espaço no lote e computação que poderia servir outra conversa. No teste de carga de 10/10/2026 (prompts de 1.000 tokens, 200 tokens de saída, até 32 conversas simultâneas):
| Configuração | 1 usuário | Sob carga (somado) | 1º token, p95 (32 simult.) |
|---|---|---|---|
| TP=2 + MTP de 2 tokens | 92 tok/s | ~280 tok/s (satura) | 21 s |
| TP=2 + MTP de 1 token | pior que as outras duas nos dois cenários | ||
| TP=2 sem MTP (produção) | 76 tok/s | 640 tok/s | 4 s |
Trocamos 18% de velocidade na conversa isolada por 2,3 vezes mais vazão e um primeiro token cinco vezes mais rápido no pico. Para uma frota que atende agentes, atendentes virtuais e times inteiros ao mesmo tempo, é a troca certa. A mudança fica num override do systemd, e voltar atrás é uma linha.
De onde vem o "sete"
A melhor velocidade que uma conversa sozinha consegue nessas placas é 92 tok/s. Sob carga, as mesmas duas GPUs produzem 640 tok/s somados: 640 ÷ 92 ≈ 7. Na prática, as duas L40S entregam o volume de texto de sete sessões rodando na velocidade máxima de uma sessão única, cada uma com o modelo inteiro de 122B.
É um número de vazão, não de latência: nenhum usuário recebe 640 tok/s. O que muda é quanta gente o mesmo par de placas atende sem fila e sem degradar o primeiro token.
O gargalo que não era a GPU
Depois de publicar o modelo, a fila continuava aparecendo. A causa estava no Mangaba Router: ao cadastrar um provedor, o limite padrão de chamadas simultâneas era 4 (pensado para servidores de CPU), e esse limite era contado somando todas as contas. Na prática, todos os clientes juntos tinham 4 vagas numa GPU preparada para 64. Subimos para 32. Com 16 usuários simultâneos passando pelo router, as respostas ficaram entre 5,1 e 14,4 s, contra 4,6 a 13,6 s chamando a GPU direto: o router quase não acrescenta tempo.
Uma armadilha de medição no caminho: o fetch do Node, com streaming, enfileira pedidos paralelos para o mesmo host e desenha uma escada de latências (3, 6, 9, 12 s) que não existe no servidor. Para medir concorrência, use curl em paralelo.
A receita
vllm serve qwen35-122b-gptq \
--served-model-name mangaba-qwen35-122b \
--tensor-parallel-size 2 \
--quantization gptq_marlin \
--gpu-memory-utilization 0.95 \
--max-model-len 196608 --kv-cache-dtype fp8 \
--enable-prefix-caching --enable-chunked-prefill \
--max-num-seqs 64 \
--reasoning-parser qwen3 \
--enable-auto-tool-choice --tool-call-parser qwen3_coder
vLLM 0.30, CUDA 12.9, duas NVIDIA L40S num servidor da Mangaba no Brasil. Contexto configurado de até 192 mil tokens por conversa, chamada de ferramentas habilitada e 7 de 7 na nossa bateria de testes agênticos. Tudo atrás do Mangaba Gateway, com a mesma API compatível com OpenAI que os clientes já usam.
O que levar daqui
- Modelo maior que a placa: paralelismo tensorial é o caminho quando a latência por token importa; pipeline sobra para quando a comunicação entre placas é lenta.
- Vazão vem do lote: KV cache em FP8, batching contínuo e um limite alto de sequências valem mais que qualquer truque de sessão única.
- Meça sob carga: decodificação especulativa ganha no teste de um usuário e perde quando a casa enche.
- Olhe a fila inteira: o limite de concorrência pode estar no proxy, não na GPU.
Mangaba AI · Inferência em servidores próprios, em datacenter no Brasil. Números medidos em 10/10/2026 no servidor de produção do mangaba-titan.
Também publicado em mangaba.ia.br. Mais da Mangaba AI no Hugging Face.