Duas máquinas de classe DGX Spark, um modelo: uma cópia em cada uma, em vez de dividi-lo

Operações

Durante sete semanas, nosso modelo de linguagem local rodou dividido em duas máquinas NVIDIA GB10 com paralelismo de tensor. Em 22 de outubro, substituímos isso por uma cópia completa do modelo em cada máquina e um proxy de menor conexão na frente. A vazão passou de cerca de 180 para 210 tokens por segundo, e qualquer uma das máquinas pode agora reiniciar sem tirar o modelo do ar.

Por que o dividimos

Cada máquina tem 128 GB de memória compartilhados entre CPU e GPU. Em setembro, o modelo cabia em uma máquina, mas seu cache mal cabia: a build do vLLM que usávamos não conseguia manter o cache KV deste modelo em FP8, então o cache estava em BF16, e uma máquina tinha espaço para 499.110 tokens dele. Isso é menos de duas requisições no contexto completo do modelo de 262.144 tokens. Dividido entre as duas máquinas com paralelismo de tensor sobre seu link de rede direto, o cache cresceu para mais de três milhões de tokens. Havíamos dividido o modelo por espaço, não por velocidade.

O que mudou

Uma build 4-bit mais recente do mesmo modelo (NVFP4, feita com destilação consciente de quantização) ocupa cerca de 71 GiB de memória, e uma build comunitária do vLLM para GB10 (eugr/spark-vllm-b12x) roda seu cache em FP8. Agora, uma máquina mantém o modelo inteiro e um cache de 0,76 a 1,08 milhão de tokens, espaço para 2,9 a 4,1 requisições no contexto completo, dependendo de quanta memória a máquina lhe dá. As duas cópias usam a decodificação especulativa própria do modelo (MTP, quatro tokens de rascunho). Antes de trocar, executamos nosso teste de chamada de ferramentas na nova build: ela pontuou 86, a build que substituiu pontuou 75.

O que medimos

Dividido em duas máquinasUma cópia em cada máquina
Cache KVmais de 3 milhões de tokens0,76 e 1,08 milhão de tokens
Requisições no contexto completocerca de 122,9 e 4,1
Vazão sob nossa cargacerca de 180 tokens/s210 tokens/s (111 + 99)
Inicializaçãocerca de 20 minutos5 a 13 minutos por cópia
Uma máquina reiniciao modelo fica fora do ara outra cópia atende

Uma cópia isolada, pelo número de requisições que atende simultaneamente: com prompts curtos, 1 requisição gerava 36 tokens por segundo, 4 geravam 82, 8 geravam 109 e 16 geravam 150. Com os prompts mais longos do nosso agente de e-mail, 4 geravam 81, 8 geravam 113 e 16 apenas 87: acima de oito requisições simultâneas, mais requisições tornavam o sistema mais lento.

Durante uma tarde, uma cópia chegou a atender no máximo 12 requisições simultâneas e a outra, 8. A primeira produziu de 91 a 93 tokens por segundo e concluiu de 26 a 27 requisições por minuto; a segunda, de 81 a 83 tokens por segundo e 22 requisições por minuto. Agora, ambas atendem 12.

O que abrimos mão

Espaço. O modelo dividido tinha um cache de mais de três milhões de tokens; as cópias têm dois caches menores, e uma requisição só pode usar o cache da cópia que a atende. Nossos agentes enviam muitas requisições curtas e médias, e para elas o throughput importava mais. Se você atende poucas instâncias de contexto muito longo simultaneamente, a divisão ainda pode ser a melhor escolha. Mantivemos a configuração dividida, e voltar para ela é apenas um script.

O que deu errado durante a migração

  1. Onze minutos sem um modelo, depois 30 segundos de erros 502. O modelo dividido precisou parar antes que uma cópia pudesse carregar nas mesmas máquinas, então nossos agentes esperaram cerca de onze minutos. Quando a primeira cópia subiu, o proxy continuou respondendo "502 no live upstreams" por cerca de 30 segundos a mais: ele havia marcado os backends como falhos enquanto estavam inativos (max_fails=3, fail_timeout=30s) e não os testou novamente até esse tempo expirar.
  2. Um cliente que ignorou o proxy. Um dos nossos jobs em lote tinha o endereço da primeira máquina em sua configuração, então toda a sua carga foi para uma cópia enquanto a outra tinha capacidade disponível. Agora, todos os clientes passam pelo proxy; os endereços das próprias máquinas são usados apenas para verificações de integridade.
  3. Métricas que saltaram. Nosso monitor de throughput lia as métricas do vLLM em /metrics através do proxy. Com dois backends, leituras consecutivas vinham de cópias diferentes, os contadores saltavam para frente e para trás, e o monitor reportava o modelo como faminto. Agora ele lê cada cópia diretamente e soma os valores.
  4. Um watchdog antigo que teria parado a nova cópia. A configuração dividida tinha um watchdog que reiniciava seu serviço quando o modelo parava de responder. O novo serviço declara Conflicts= com o antigo, então esse reinício teria parado a nova cópia. Foi necessário desativá-lo com systemctl disable --now, não apenas pará-lo, porque nossa camada de operações reinicia unidades que estão habilitadas, mas não em execução.
  5. Memória na máquina que executa outras tarefas. Uma máquina também executa nossos bancos de dados e builds do site. Com a gpu-memory-utilization do vLLM em 0,78, restavam apenas 5 GiB para todo o resto, então baixamos para 0,74 e reiniciamos apenas essa cópia enquanto a outra mantinha a carga. Nessas máquinas, a fatia que você dá ao vLLM sai da mesma memória que tudo o mais.

Se você tiver duas dessas máquinas

Factory Floor inclui o proxy e as configurações de inicialização para uma ou duas máquinas, a verificação de integridade e o teste de uso de ferramentas que executamos antes de trocar de modelo, com os watchdogs e guardas de memória para o restante da máquina.

Atualizado: