Por que seu LLM trava com 5 usuários simultâneos
Entenda por que 1 usuário funciona e 5 ficam lentos: batching, limites de KV cache, prefill e filas definem quantas requisições seu servidor atende ao mesmo tempo.
Por que um LLM auto-hospedado fica mais lento quando chegam mais utilizadores?
Um LLM auto-hospedado fica bloqueado com 5 utilizadores simultâneos porque o servidor ainda está a gerar uma resposta de cada vez, enquanto os outros quatro ficam numa fila. A documentação do Ollama é clara sobre o valor predefinido: OLLAMA_NUM_PARALLEL é "o número máximo de pedidos paralelos que cada modelo processa simultaneamente, por predefinição 1." Nada está avariado. Quatro das cinco pessoas estão à espera da sua vez.
A solução raramente é um servidor mais potente. É um motor de serving que processa muitos pedidos no modelo na mesma passagem forward, além de memória livre suficiente para manter o contexto de todas as conversas durante esse processamento. As duas partes são importantes, mas a segunda é o que define efetivamente o limite.
As duas fases pelas quais cada pedido passa
O prefill lê todo o prompt de uma vez e cria a cache de atenção correspondente. Cada token do prompt passa pelo modelo em conjunto, por isso o prefill é uma grande multiplicação de matrizes e fica limitado ao débito de cálculo. O decode escreve depois a resposta um token de cada vez. Para cada token, é necessário voltar a ler da memória todos os pesos do modelo, enquanto o cálculo feito nesse único token é reduzido. O decode fica limitado à largura de banda da memória.
Esta assimetria é o motivo pelo qual o batching funciona. Ao fazer o decode para um utilizador, são lidos, por exemplo, 5 GB de pesos por token, enquanto a maior parte das unidades de cálculo fica inativa. Ao adicionar um segundo pedido, o motor lê os mesmos 5 GB uma vez e calcula depois dois tokens com esses dados. O segundo utilizador quase não acrescenta tempo. Processar os pedidos estritamente um após o outro elimina essa vantagem.
Dois valores descrevem a experiência do utilizador. TTFT (time to first token) é o tempo de espera na fila mais o prefill. ITL (inter-token latency) é o intervalo entre os tokens transmitidos e é definido pelo decode. Um servidor lento costuma ter problemas num destes valores, e as correções não são iguais.
O batching estático faz todos esperarem pela resposta mais lenta
O batching estático é a versão ingénua. É o que obtém quando agrupa pedidos manualmente no código da aplicação. O motor recolhe N pedidos, executa-os em conjunto e mantém todos os slots ocupados até terminar a geração mais longa do grupo.
Um utilizador que pede um resumo com 1,200 tokens mantém quatro respostas de uma linha bloqueadas no batch, porque o batch não liberta nenhum slot até terminar o seu membro mais lento.
Daqui resultam dois custos. As sequências concluídas continuam a ocupar slots sem fazer nenhum trabalho útil, por isso o throughput efetivo diminui à medida que variam os comprimentos das respostas, e os comprimentos das respostas de chat variam muito. Um pedido que chega um passo depois de o batch ser formado espera que todo o batch termine antes de sequer iniciar o prefill. Isso faz com que o seu TTFT seja determinado pelo ensaio de outra pessoa.
O batching contínuo admite e retira pedidos a cada token
O batching contínuo agenda pedidos ao nível de um único passo de descodificação. Depois de cada passo, o agendador remove as sequências que acabaram de emitir o seu token de paragem e admite os pedidos em espera nos slots livres. Uma resposta que termina no passo 40 liberta o seu slot no passo 40, e não no fim de um batch.
Isto não é uma funcionalidade exótica. llama-server documenta -cb, --cont-batching como "se deve ativar o batching contínuo (também designado batching dinâmico) (predefinição: ativado)", e o vLLM é desenvolvido em torno deste conceito. O Ollama também processa pedidos em paralelo. A predefinição limita o número a um, e é por isso que tantas pessoas concluem que o seu hardware não suporta concorrência, quando foi a configuração que a desativou.
Os resultados publicados sobre batching contínuo são normalmente medidos em placas de datacenter com capacidade de computação disponível e dezenas de gigabytes para a cache. O comportamento desses resultados aplica-se ao seu sistema. A escala dos resultados não, e a secção sobre memória abaixo explica porquê.
O prefill compete com o decode pela mesma capacidade de processamento
Quando chega um novo pedido enquanto quatro respostas estão a ser transmitidas, o prompt tem de passar primeiro pelo prefill, que exige muitos recursos de processamento. Se o escalonador atribuir uma etapa exclusiva a esse prefill, os quatro utilizadores que estão a receber texto não recebem nenhum token durante essa etapa. Num prompt longo, isso provoca uma pausa visível em todas as janelas abertas. É esta a interrupção a que as pessoas se referem quando dizem que o servidor dá soluços sempre que outra pessoa clica em enviar.
O prefill em blocos divide um prompt longo em partes e mistura cada parte na mesma etapa dos decodes em execução. O guia de otimização do vLLM declara diretamente o compromisso: orçamentos de blocos menores "achieve better ITL because there are fewer prefills slowing down decodes", enquanto valores mais altos "achieve better time to first token (TTFT) as you can process more prefill tokens in a batch". Está a escolher qual experiência proteger: a pessoa que espera pelo início da resposta ou as pessoas que observam o texto a ser transmitido.
O comprimento do prompt determina o impacto. Um prompt com 6,000 tokens e uma resposta com 200 tokens representa 6,000 tokens de trabalho de prefill contra 200 etapas de decode. O chat com geração aumentada por recuperação e os prompts de sistema longos colocam ambos o sistema nesse regime. Por isso, o prefill deixa de ser um detalhe desprezável e passa a ser aquilo que mantém os utilizadores à espera. O cache de prefixos ajuda quando a parte longa se repete: o vLLM expõe --enable-prefix-caching, que reutiliza o cache de um prefixo de prompt partilhado em vez de o recalcular para cada pedido.
A memória que se esgota primeiro é a cache KV
Cada token de cada conversa ativa deixa um vetor de chave e um vetor de valor em cada camada do modelo. Essa é a cache KV (cache de chave/valor) e permite que a fase de decodificação evite recalcular todo o prompt para cada token novo. O tamanho por token é definido pela arquitetura do modelo: 2 (uma chave e um valor) vezes o número de camadas, vezes o número de cabeças de chave/valor, vezes a dimensão da cabeça, vezes o número de bytes por valor. Consulte esses valores no config.json do modelo.
Faça o cálculo uma vez e o limite deixa de ser um mistério. Um modelo 8B típico, com 36 camadas, 8 cabeças de chave/valor e dimensão de cabeça 128, mantendo a cache em 16 bits, consome 2 36 8 128 2 bytes por token. Isso corresponde a 147,456 bytes, aproximadamente 144 KiB. Uma conversa com 8,192 tokens precisa, portanto, de cerca de 1.2 GB de cache. Cinco conversas precisam de cerca de 6 GB, além dos pesos. Esse é o valor que determina quantos utilizadores cabem.
A concorrência multiplica o contexto, e as ferramentas mostram isso diretamente. A FAQ do Ollama diz: "O processamento paralelo de pedidos para um determinado modelo aumenta o tamanho do contexto pelo número de pedidos paralelos. Por exemplo, um contexto de 2K com 4 pedidos paralelos resulta num contexto de 8K e numa alocação adicional de memória." A RAM necessária aumenta na proporção de OLLAMA_NUM_PARALLEL multiplicado por OLLAMA_CONTEXT_LENGTH. Em llama-server, o contexto pedido com -c é distribuído pelos -np slots. Por isso, aumentar apenas o número de slots reduz o que cada pedido pode armazenar. Consulte o contexto por slot no log de arranque, em vez de o presumir.
O vLLM faz a pré-alocação. --gpu-memory-utilization (predefinição: 0.92) é "a fração da memória da GPU a utilizar pelo executor do modelo". O que sobra depois de carregar os pesos torna-se o pool paginado da cache KV. Quando esse pool fica sem espaço, o scheduler remove um pedido em vez de o fazer falhar:
WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.No motor V1 do vLLM, o modo de preempção predefinido é RECOMPUTE. Assim, um pedido removido perde a cache e volta a executar o prefill quando é readmitido. Esse trabalho é executado duas vezes. A documentação avisa que "a preempção e o recálculo podem afetar negativamente a latência de ponta a ponta". Esta linha do log é a melhor explicação para um utilizador ter esperado muito mais do que os outros enquanto a média permaneceu normal. Defina disable_log_stats=False para registar a contagem acumulada ou leia o contador de preempções nas métricas Prometheus expostas pelo vLLM.
O que muda com 2, 5 e 20 utilizadores simultâneos
Dois utilizadores. Num GPU com cache disponível, o impacto é quase impercetível, porque o segundo fluxo de decodificação acompanha o primeiro com muito pouco tempo adicional. Num VPS apenas com CPU e 4 a 8 GB de RAM, não é gratuito: os dois fluxos partilham o mesmo conjunto reduzido de vCPUs e a mesma largura de banda da RAM. Assim, cada utilizador obtém aproximadamente metade dos tokens por segundo, enquanto a necessidade de cache duplica face a um orçamento muito menor.
Cinco utilizadores. É neste ponto que os valores predefinidos deixam de ser suficientes, e o problema começa por ser uma questão de fila. Com OLLAMA_NUM_PARALLEL definido como 1, quatro pessoas ficam à espera de quem pediu a resposta longa, e cada uma vê a velocidade normal assim que chega a sua vez. Se aumentar o número de execuções paralelas, o problema muda de natureza: cinco slots com contexto de 8K exigem encontrar uma cache de 40K tokens. Se ela não couber na VRAM, o motor descarrega camadas para a RAM do sistema. Se não couber na RAM, o sistema começa a usar swap e os tokens por segundo entram em colapso.
Vinte utilizadores. Vinte pessoas numa interface de chat normalmente não correspondem a vinte pedidos simultâneos. Esta é a informação mais importante antes de comprar hardware. Uma pessoa lê uma resposta e demora 20 a 60 segundos entre interações, pelo que a maior parte da sessão permanece inativa. Vinte agentes ou vinte tarefas de resumo de documentos correspondem a vinte fluxos reais, sem qualquer tempo de inatividade. Trata-se de uma máquina diferente.
Os seus utilizadores são concorrentes ou apenas estão ligados?
Calcule os pedidos em curso antes de dimensionar o sistema. A aritmética é simples: pedidos em curso são iguais ao número de utilizadores, multiplicado pelos segundos gastos a gerar cada turno, dividido pelos segundos entre turnos.
- Meça primeiro a velocidade do seu próprio fluxo único, tanto no prefill como no decode. Não use um valor da placa de outra pessoa: meça os tokens por segundo no seu próprio sistema e use o resultado obtido.
- Estime o ciclo de atividade. Vinte utilizadores de chat, 12 segundos de geração por turno e um turno a cada 90 segundos resultam em 20 * 12 / 90, ou cerca de 2.7 pedidos em curso.
- Defina o número de slots ligeiramente acima desse valor e confirme-o na memória: o número de slots multiplicado pelo contexto por pedido tem de caber nos tokens de cache que realmente tem disponíveis.
- Mantenha a fila curta para que o excesso falhe rapidamente e de forma visível.
Os tokens de cache disponíveis correspondem à memória livre depois de carregar os pesos, dividida pelo custo por token indicado na secção anterior. Uma placa de 24 GB que execute um modelo 8B em 16-bit gasta cerca de 16 GB com os pesos e tem aproximadamente 6 GB de cache utilizável com a utilização predefinida, o que corresponde a cerca de cinco conversas de 8K. Para acomodar mais conversas, reduza o contexto por pedido ou armazene a cache em 8-bit (llama-server ocupa --cache-type-k q8_0). Ambas as opções aumentam a concorrência, mas implicam uma cedência. Vale a pena ler a descrição honesta desse compromisso antes de investir em hardware: quando um GPU VPS compensa face aos tokens de uma API.
Quando os valores predefinidos do Ollama deixam de ser suficientes
Aumente o número de execuções paralelas através da unidade do serviço, porque uma exportação feita na shell não chega a um daemon gerido pelo systemd.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama pssystemctl show deve mostrar as três variáveis que acabou de definir. Se isso não acontecer, o drop-in não foi guardado e nada do que fizer a seguir terá efeito. ollama ps lista depois o modelo carregado com um tamanho superior ao dos pesos, porque quatro slots com 8,192 tokens reservam 32,768 tokens de cache adicionais. Uma coluna PROCESSOR que mostre parte do modelo na CPU quando esperava que todo ele estivesse na GPU significa que pediu mais cache do que a memória disponível na placa permitia. Reduza um dos dois valores.
O valor predefinido da fila merece uma segunda análise. O Ollama coloca em fila até OLLAMA_MAX_QUEUE pedidos, e "o valor predefinido é 512". Depois disso, responde "com um erro 503 que indica que o servidor está sobrecarregado". Uma fila com 512 posições num servidor que processa quatro pedidos de cada vez é uma promessa que não pode cumprir, porque o cliente na posição 300 excede o tempo limite muito antes de chegar a sua vez. Uma fila curta devolve um erro que a aplicação pode repetir ou comunicar, o que é melhor do que um indicador de carregamento que nunca termina.
Teste na prática. Envie dois pedidos exatamente ao mesmo tempo a partir de dois terminais e monitorize ambos. Se o segundo não produzir qualquer resultado até o primeiro terminar, a configuração de execução paralela não entrou em vigor.
Quando um motor de serving real começa a compensar
O vLLM compensa a configuração adicional quando tem uma GPU com capacidade disponível e mais de aproximadamente quatro pedidos efetivamente em processamento. O agendador funciona por token, a cache é paginada para reutilizar fragmentos livres e a VRAM disponível é convertida em concorrência, em vez de ficar ociosa. Em agosto de 2026, a instalação e o arranque documentados são feitos com dois comandos:
uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Say hello."}]
}'Uma resposta que contenha um array choices significa que o servidor está ativo e que o modelo foi carregado. Sob carga, os dois parâmetros relevantes são --max-num-seqs, o "número máximo de sequências a processar numa única iteração", e --max-num-batched-tokens, o "número máximo de tokens que podem ser processados numa única iteração". O primeiro limita a concorrência. O segundo define o orçamento de prefill em blocos descrito anteriormente.
Abaixo de aproximadamente quatro pedidos em processamento, ou em qualquer máquina sem uma GPU compatível, o vLLM acrescenta complexidade e oferece poucos benefícios. Espera uma placa de classe CUDA e reserva a maior parte da memória no arranque, o que é uma escolha inadequada numa VPS com 4 a 8 GB. Nesse caso, a solução é um modelo mais pequeno, com um contexto mais curto e uma fila que possa controlar. como o Ollama e o vLLM diferem enquanto motores de serving explica a escolha em detalhe, e executar o Qwen 3 8B numa VPS mostra os recursos exigidos por um modelo de tamanho intermédio antes de adicionar um único utilizador extra.
O custo que a sabedoria popular oculta
O continuous batching aumenta o throughput total e, normalmente, também melhora a latência mediana, porque um pedido em fila começa mais cedo. A latência de cauda evolui no sentido oposto, e essa parte raramente é mencionada.
Cada sequência adicional numa etapa acrescenta algum trabalho, por isso o ITL aumenta para todos à medida que o batch fica cheio. O prefill de uma nova chegada ocupa parte de uma etapa que, de outro modo, estaria disponível para os utilizadores com streaming. Sob pressão de cache, o scheduler faz preempção, o que devolve um pedido parcialmente gerado ao início do prefill.
Uma interface de chat mostra as caudas, não as médias. Um stream que pausa durante dois segundos a meio de uma frase parece estar avariado, mesmo quando o tempo total até à conclusão é bom. Meça o p95 de TTFT e o p95 de ITL sob a carga esperada e trate a média de tokens por segundo como um valor de capacidade, não como uma descrição da experiência.
A configuração prática resulta daí. Limite a concorrência ligeiramente abaixo do que a memória permite, para que o motor nunca precise de fazer preempção. Uma fila curta e previsível é melhor do que um batch profundo que entra em thrashing, porque um utilizador que espera quatro segundos e depois recebe um stream fluido fica mais satisfeito do que um utilizador que começa imediatamente e sofre duas paragens.
O que verificar quando está lento
Cada utilizador tem um desempenho normal, mas a espera é longa. Isso é uma fila, não um problema de velocidade. Verifique primeiro a configuração de paralelismo. O modelo está a responder corretamente, uma requisição de cada vez.
HTTP 503 do Ollama. A fila está cheia. O servidor atingiu realmente a capacidade máxima ou OLLAMA_MAX_QUEUE está configurado com um valor baixo de propósito para rejeitar carga, que é precisamente o comportamento esperado.
Os tokens por segundo caem sob carga num servidor com CPU. Execute vmstat 1 enquanto isso acontece. Valores diferentes de zero nas colunas si e so significam que a máquina está a usar swap. Os pesos estão a ser lidos do disco a cada token. Nenhuma alteração de configuração resolve isso. Reduza o tamanho do modelo ou o número de slots.
Um em cada dez utilizadores espera muito mais do que os restantes. Procure preempted no log do vLLM. A preempção e o respetivo recompute são normalmente a causa. Isso significa que a cache está sobrecarregada para o comprimento de contexto permitido.
O TTFT é elevado mesmo quando o servidor está inativo. Isso é prefill, não concorrência. Prompts longos consomem tempo real antes de surgir o primeiro token. Por isso, verifique o tamanho do prompt e o prefix caching antes de analisar o hardware.
FAQ
Por que o meu LLM auto-hospedado fica mais lento quando uma segunda pessoa o utiliza?
Na maioria dos casos, ele não fica mais lento. A segunda solicitação entra numa fila. O Ollama é distribuído com OLLAMA_NUM_PARALLEL definido como 1, por isso a segunda solicitação espera que a primeira emita o token final. Distinga os dois casos medindo o fluxo de uma pessoa enquanto outra espera: se a velocidade em tokens por segundo for normal depois de começar, existe uma fila, e aumentar a contagem de execução paralela resolve o problema. Se os dois fluxos funcionarem à metade da velocidade, está a partilhar efetivamente a largura de banda da memória, que é uma limitação do hardware.
Quantos utilizadores simultâneos uma GPU pequena consegue servir?
Conte a memória, não os utilizadores. Primeiro vêm os pesos. Depois vem a cache KV, que custa 2 vezes o número de camadas vezes o número de cabeças de chave/valor vezes a dimensão da cabeça vezes o número de bytes, por token e por conversa ativa. Um modelo 8B típico com 36 camadas, 8 cabeças de chave/valor e dimensão da cabeça 128 custa cerca de 144 KiB por token em 16-bit. Por isso, uma conversa com 8,192 tokens precisa de aproximadamente 1.2 GB. Uma placa de 24 GB que mantenha esse modelo em 16-bit fica com cerca de 6 GB para a cache. Isso corresponde a aproximadamente cinco conversas com o contexto completo, ou a mais conversas se reduzir o contexto.
O batching contínuo torna a resposta de cada utilizador mais lenta?
A latência mediana normalmente melhora, porque as solicitações deixam de esperar que um lote inteiro termine. A latência de cauda piora. Cada sequência adicional acrescenta trabalho a cada passo de decodificação. A fase de prefill de uma nova chegada ocupa parte de um passo que seria usado pelos utilizadores em streaming. Uma solicitação interrompida precisa de executar o prefill duas vezes. Meça a latência p95 entre tokens, não a média, porque uma janela de chat torna as pausas evidentes de uma forma que as médias ocultam.
Devo aumentar OLLAMA_NUM_PARALLEL ou mudar para vLLM?
Aumente primeiro a contagem de execução paralela. Não tem custo e requer apenas um ficheiro de substituição. Também resolve o caso comum em que quatro pessoas ficam numa fila atrás de uma resposta longa. A memória é o limite: as solicitações paralelas multiplicam o contexto que tem de manter, por isso monitorize a transferência de camadas para a CPU. Mude para vLLM quando tiver uma GPU com VRAM disponível e mais de aproximadamente quatro solicitações realmente em execução, porque é nesse ponto que a cache paginada e o agendamento por token compensam mais do que custam.
Mais núcleos de CPU corrigem um servidor LLM lento?
Não na parte que os utilizadores mais notam. A decodificação lê o modelo inteiro da memória para cada token, por isso fica limitada pela largura de banda da RAM. Os núcleos adicionais deixam de ajudar quando a largura de banda fica saturada. O prefill escala com o número de núcleos, portanto mais núcleos reduzem o tempo até ao primeiro token em prompts longos. Numa VPS com 4 a 8 GB, a limitação principal costuma ser a capacidade de memória. A solução efetiva é normalmente usar um modelo menor ou um contexto mais curto, e não mais vCPUs.