Escalonamento de GPU e Filas de Cluster
Visão geral
Cargas de trabalho modernas de aprendizado de máquina (machine learning, ML)—treinamento distribuído, grandes varreduras de hiperparâmetros, tarefas de embeddings e inferência com suporte de GPU—competem por um recurso escasso: aceleradores. Escalonamento de GPU (GPU scheduling) e filas do cluster (cluster queues) são os mecanismos que uma plataforma usa para decidir:
- Quem recebe GPUs e quando
- Quantas GPUs um job recebe e quais GPUs/nós específicos
- Como evitar starvation (fome; alguns jobs nunca rodam)
- Como evitar fragmentação (existem GPUs, mas não no “formato” certo para rodar os jobs na fila)
Este artigo explica os principais conceitos de escalonamento, as compensações por trás de políticas comuns e abordagens práticas usadas no Kubernetes e em escalonadores de HPC (computação de alto desempenho) como o Slurm—especialmente para clusters de IA multi-tenant (multi-inquilino).
Contexto relacionado: Introdução ao Hardware, Kubernetes para ML (Visão Geral), Interconexões (NVLink/InfiniBand), Memória e Largura de Banda e Armazenamento.
Por que o escalonamento de GPU é difícil em ML
GPUs não são apenas “núcleos de CPU, só que mais rápidos”. Jobs de ML frequentemente têm restrições que tornam o escalonamento significativamente mais complexo:
- Unidades discretas e de alto valor: uma única GPU pode custar vários dólares por hora e não pode ser facilmente superalocada (overcommitted).
- Sensibilidade à topologia: treinamento multi-GPU frequentemente se beneficia de NVLink no mesmo nó ou de rede de baixa latência como InfiniBand entre nós (Interconexões (NVLink/InfiniBand)).
- Escalonamento em gangue (gang scheduling): treinamento distribuído pode exigir todas as réplicas de uma vez (por exemplo, 64 GPUs no total) ou não consegue avançar.
- Longos tempos de execução: jobs de treinamento podem rodar por dias; preempção e políticas de justiça importam.
- “Formatos” de GPU não uniformes: jobs solicitam 1, 2, 4, 8 GPUs; clusters têm nós com contagens fixas de GPUs (por exemplo, 8×A100). Isso causa fragmentação.
Um bom escalonador deve equilibrar utilização, justiça, throughput e experiência do usuário.
Terminologia central
Jobs, pods e workloads
Um job é a unidade de trabalho: uma execução de treinamento, um trial de varredura (sweep), um job de avaliação ou um serviço de inferência.
No Kubernetes, jobs são implementados como um ou mais pods (por exemplo, um Job, RayCluster, MPIJob ou um controlador customizado). No Slurm, jobs são submetidos via sbatch/srun.
Recursos: GPUs mais restrições
Escalonamento de GPU envolve mais do que “número de GPUs”:
- Quantidade e tipo de GPU (A100 vs H100 vs L4)
- Tamanho de memória da GPU
- Requisitos de CPU/RAM para pipelines de entrada
- Requisitos de SSD local (por exemplo, para datasets particionados)
- Topologia de nó/rede (mesmo nó, mesmo rack, partição com IB habilitado)
- Capacidades especiais (MIG, compartilhamento de GPU, RDMA)
Filas do cluster
Uma fila do cluster é uma fronteira de política para admissão e priorização. Em geral, ela define:
- Cota (capacidade garantida para uma equipe)
- Regras de empréstimo / cessão (usar capacidade ociosa de outras equipes)
- Política de prioridade e preempção
- Sabores de recursos (resource flavors) (por exemplo, “a100-80gb”, “h100-80gb”)
- Limites (máximo de GPUs por job, máximo de jobs em execução etc.)
Na prática, uma plataforma frequentemente tem múltiplas filas:
interactive(rápida resposta, jobs menores)batch(orientada a throughput, jobs maiores)prod-inference(alta prioridade, SLOs rigorosos)
Objetivos de escalonamento e trade-offs
Escalonadores não conseguem otimizar tudo ao mesmo tempo. Os objetivos comuns são:
Alta utilização
Manter as GPUs ocupadas, minimizando aceleradores ociosos.Justiça entre tenants
Evitar que uma equipe monopolize o cluster; respeitar cotas e prioridades.Baixa latência para jobs pequenos/interativos
Cientistas de dados esperam que notebooks e experimentos curtos iniciem rapidamente.Alto throughput para workloads batch
Concluir o maior número de jobs possível por unidade de tempo.Evitar starvation
Garantir que jobs de baixa prioridade ainda rodem eventualmente (ou ao menos falhem rapidamente com um motivo claro).Evitar fragmentação
Manter o cluster em um estado em que grandes “formatos” de GPU (por exemplo, jobs de 8 GPUs) possam rodar.
Toda política é uma escolha. Por exemplo, maximizar a utilização via empacotamento agressivo pode aumentar o tempo de espera de jobs grandes; priorizar jobs grandes pode prejudicar a latência interativa.
Fundamentos teóricos (modelos mentais úteis)
Teoria das filas (por que filas se formam)
Em alto nível, clusters de GPU se comportam como um sistema de filas:
- Jobs chegam com alguma taxa (experimentos submetidos).
- Jobs têm tempos de serviço (duração do treinamento).
- A capacidade é limitada (suprimento finito de GPUs).
À medida que a utilização se aproxima de 100%, os tempos de espera na fila podem aumentar dramaticamente. Na prática, muitos clusters miram uma utilização em regime permanente inferior a 100% para manter a latência razoável—especialmente para filas interativas.
Compartilhamento justo e DRF
Quando tenants demandam múltiplos tipos de recursos (GPU, CPU, RAM), justiça é multidimensional.
Justiça de Recurso Dominante (Dominant Resource Fairness, DRF) é um conceito amplamente usado: a “cota” de cada usuário é medida pelo recurso que ele consome mais em relação à capacidade total (seu recurso dominante). DRF ajuda a evitar casos em que um tenant usa “apenas GPUs” e expulsa outro tenant que precisa de GPU+CPU.
Muitos escalonadores e sistemas de filas modernos implementam raciocínio no estilo DRF ou variantes de compartilhamento justo ponderado.
Bin packing (posicionamento e fragmentação)
Posicionar jobs em nós é um problema de empacotamento em bins (bin packing):
- Nós são “bins” com capacidades (8 GPUs, 256 GB de RAM).
- Jobs são “itens” com requisitos (4 GPUs, 64 GB de RAM).
Bin packing é NP-difícil, então sistemas reais usam heurísticas:
- Best-fit / worst-fit
- First-fit decreasing
- Estratégias de espalhar vs empacotar (spread vs pack)
A fragmentação de GPUs frequentemente acontece quando muitos jobs pequenos ocupam GPUs em muitos nós, deixando nenhum nó com GPUs livres contíguas suficientes para um job grande.
Escalonamento em gangue (tudo-ou-nada)
Treinamento distribuído frequentemente precisa de N workers simultaneamente. Se você iniciar apenas metade dos workers, o job pode travar ou ter desempenho ruim.
Escalonadores lidam com isso via escalonamento em gangue (um job só é admitido se todas as GPUs necessárias puderem ser alocadas). Isso melhora correção/desempenho, mas pode aumentar o tempo de fila.
Backfilling
Backfilling executa jobs menores em lacunas enquanto um job grande aguarda recursos suficientes ficarem disponíveis—sem atrasar o horário esperado de início do job grande (em sistemas que conseguem estimar tempos de execução).
Backfilling melhora a utilização, mas exige cuidado:
- Se estimativas de runtime estiverem erradas, jobs grandes ainda podem ser atrasados.
- Se o backfilling não for limitado, ele pode piorar a fragmentação.
Preempção, prioridade e aging
Para garantir que trabalho urgente rode rapidamente, escalonadores usam:
- Classes/níveis de prioridade
- Preempção (desalojar jobs de menor prioridade)
- Aging (envelhecimento) (aumentar a prioridade de um job quanto mais tempo ele espera) para mitigar starvation
Preempção é poderosa, mas cara para treinamento, a menos que jobs façam checkpoints bem (Armazenamento).
Design de filas do cluster na prática
Cotas: garantida vs máxima
Um modelo típico de filas para equipes:
- Cota garantida: capacidade reservada para uma equipe (por exemplo, 64 GPUs).
- Cota máxima: teto rígido (por exemplo, 128 GPUs).
- Empréstimo: se outras equipes não estiverem usando suas garantias, você pode usá-las temporariamente.
Isso evita GPUs ociosas enquanto ainda protege o acesso mínimo de cada equipe.
Níveis de prioridade
Níveis comuns:
- P0 (inferência de produção / incidentes): deve rodar imediatamente; pode preemptar.
- P1 (caminho crítico de pesquisa): treinamento batch de alta prioridade.
- P2 (pesquisa padrão): a maioria dos experimentos.
- P3 (best-effort): sweeps, trabalho exploratório.
O essencial é manter o número de níveis pequeno e definir regras claras; muitos níveis de prioridade viram política interna e confusão.
Controle de admissão
Muitos sistemas separam:
- Admissão: decidir se um job tem permissão para começar a consumir recursos (com base em cota/compartilhamento justo).
- Posicionamento (placement): decidir onde ele roda (seleção de nó/bin packing).
Essa separação é comum em ecossistemas Kubernetes (por exemplo, controladores de enfileiramento que “admitem” pods, e então o escalonador padrão faz o posicionamento).
Evitando starvation (padrões de política)
Starvation acontece quando alguns jobs ficam na fila indefinidamente porque jobs de maior prioridade ou com formato mais favorável sempre pegam os recursos primeiro.
Técnicas que ajudam
Aging / boosts por tempo de espera
- Aumentar gradualmente a prioridade efetiva conforme o tempo de espera cresce.
- Evita “perdedores permanentes” sem eliminar prioridades.
Compartilhamento justo com janelas de enforcement
- Medir consumo em uma janela de tempo (por exemplo, últimas 24 horas).
- Usuários intensivos são despriorizados até que outros alcancem.
Limites de concorrência por fila
- Limitar o número de jobs simultaneamente em execução por usuário/equipe.
- Evita que um sweep inunde o cluster.
Preempção (seletiva)
- Preemptar apenas jobs que sejam:
- de baixa prioridade
- amigáveis a checkpoint
- de curta duração ou facilmente reiniciáveis
- Para treinamento, exigir checkpointing periódico para tornar a preempção viável.
- Preemptar apenas jobs que sejam:
Capacidade reservada para trabalho interativo
- Um pequeno pool reservado (por exemplo, 5–10% das GPUs) pode melhorar drasticamente a velocidade de desenvolvimento.
- Sem isso, workloads batch podem dominar e jobs interativos entram em starvation.
Exemplo prático: starvation via “jobs pequenos sempre executáveis”
Suponha:
- Cluster: 10 nós × 8 GPUs = 80 GPUs
- Time A roda muitos trials de sweep de 1 GPU (centenas de jobs)
- Time B submete um job de treinamento distribuído de 64 GPUs
Se o escalonador admite jobs de forma gulosa, o Time A pode ocupar GPUs em todos os nós, deixando nenhum conjunto contíguo de 64 GPUs disponível (especialmente se restrições de localidade se aplicarem). O job do Time B pode entrar em starvation mesmo enquanto “GPUs livres existem ao longo do tempo”.
Mitigações incluem cotas, escalonamento em gangue, posicionamento com consciência de topologia e controles de fragmentação (abaixo).
Evitando fragmentação (posicionamento e estratégias de formato de recurso)
Fragmentação significa que você tem GPUs livres suficientes no total, mas não no arranjo certo para rodar os jobs na fila—especialmente jobs multi-GPU que exigem posicionamento no mesmo nó.
Técnicas que ajudam
Empacotar jobs pequenos intencionalmente
- Para jobs de 1–2 GPUs, preferir empacotar no menor número possível de nós.
- Isso deixa outros nós completamente livres para jobs de 8 GPUs.
Pools de nós por formato de job
- Reservar certos nós (ou partições) para:
- inferência de GPU única
- experimentos de 2–4 GPUs
- treinamento de 8 GPUs
- Isso é simples e eficaz, mas pode reduzir flexibilidade se a demanda mudar.
- Reservar certos nós (ou partições) para:
Escalonamento com consciência de topologia
- Preferir posicionamento no mesmo nó para jobs multi-GPU quando NVLink é importante.
- Preferir nós com IB habilitado para jobs multinó.
- Isso melhora desempenho, mas adiciona restrições que podem aumentar o tempo de fila.
MIG / particionamento de GPU (com cuidado)
- NVIDIA MIG pode dividir uma GPU em instâncias menores.
- Ótimo para inferência ou modelos pequenos, mas pode aumentar fragmentação se não for gerenciado.
- Um padrão comum: dedicar algumas GPUs/nós para MIG e manter outras como GPU inteira para treinamento.
Treinamento elástico / maleável
- Alguns frameworks conseguem escalar o número de workers para cima/baixo.
- Isso reduz a pressão do escalonamento em gangue e melhora utilização, mas muda a dinâmica de treinamento e pode complicar reprodutibilidade.
Dimensionamento correto das requisições
- Super-solicitar (pedir 8 GPUs “por via das dúvidas”) piora fragmentação e tempo de fila.
- Incentivar profiling e escalonamento incremental.
Sistemas práticos: como isso é implementado
Slurm (comum em clusters HPC / pesquisa)
Slurm oferece:
- Partições (aproximadamente “filas”)
- QOS (prioridade, limites)
- Contabilização de compartilhamento justo
- Escalonamento com backfill
- Reservas e preempção
Exemplo (ilustrativo) de submissão Slurm:
sbatch \
--partition=gpu-a100 \
--gres=gpu:8 \
--cpus-per-task=32 \
--mem=256G \
--time=24:00:00 \
--qos=research \
train.sbatch
Admins do cluster então ajustam:
PriorityWeightFairshare,PriorityWeightQOS- Parâmetros de backfill
- Limites de GPU por conta
- Regras de preempção
Kubernetes + enfileiramento (comum em plataformas de ML na indústria)
O escalonamento padrão do Kubernetes é ótimo em posicionamento, mas justiça multi-tenant e cotas para GPUs frequentemente exigem componentes adicionais.
Abordagens comuns:
- Escalonador do Kubernetes + classes de prioridade + namespaces/cotas
- Escalonadores batch por cima (por exemplo, Volcano)
- Controladores de admissão baseados em fila (por exemplo, Kueue) que gerenciam filas do cluster e admitem workloads
Isso complementa o ecossistema Kubernetes descrito em Kubernetes para ML (Visão Geral).
Exemplo: admissão baseada em fila com “ClusterQueue” e “LocalQueue”
Abaixo há um exemplo simplificado de como um cluster poderia definir:
- Uma fila de cluster com cotas por “flavor” (tipo de GPU)
- Uma fila local ao namespace em que as equipes submetem
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
name: research-a100
spec:
namespaceSelector: {} # allow all namespaces (often restricted in practice)
resourceGroups:
- coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
flavors:
- name: a100
resources:
- name: "nvidia.com/gpu"
nominalQuota: 64 # guaranteed
- name: "cpu"
nominalQuota: 2048
- name: "memory"
nominalQuota: 8Ti
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
name: team-vision
namespace: vision
spec:
clusterQueue: research-a100
Uma plataforma pode então adicionar políticas como:
- limites por namespace
- regras de empréstimo
- classes de prioridade
- comportamento de preempção
(Campos e APIs exatos evoluem; trate isso como configuração conceitual, e não como YAML de produção para copiar/colar.)
Padrões de workload específicos de ML e como escaloná-los
Treinamento distribuído (por exemplo, 64–1024 GPUs)
Desafios:
- Escalonamento em gangue
- Sensibilidade à topologia/rede
- Longos tempos de execução e necessidade de checkpoints
Recomendações:
- Usar filas com cotas claras para “jobs grandes”.
- Exigir checkpointing em armazenamento compartilhado (Armazenamento) para que preempção/reinícios sejam viáveis.
- Preferir NVLink no mesmo nó para coletivas intra-nó; garantir interconexão rápida para execuções multinó (Interconexões (NVLink/InfiniBand)).
Varreduras de hiperparâmetros (muitos jobs de 1 GPU)
Desafios:
- Podem inundar o escalonador e causar starvation em outros
- Podem fragmentar o cluster
Recomendações:
- Colocar sweeps em uma fila best-effort com limites de concorrência por usuário.
- Empacotar jobs de 1 GPU de forma densa para preservar nós inteiros para jobs maiores.
- Considerar capacidade oportunista/preemptível.
Notebooks interativos e depuração
Desafios:
- Há pessoas esperando; latência importa.
Recomendações:
- Reservar um pequeno pool interativo ou dar maior prioridade a jobs interativos com limites rígidos (por exemplo, máximo de 1–2 GPUs por usuário).
- Impor tempo máximo curto de execução, a menos que seja renovado.
Serviços de inferência
Desafios:
- Precisam de alta disponibilidade e latência previsível.
- Frequentemente se beneficiam de GPUs dimensionadas corretamente ou MIG.
Recomendações:
- Separar inferência de produção em sua própria fila/partição de alta prioridade.
- Preferir posicionamento estável e orçamentos de disrupção (disruption budgets).
- Usar quantização para reduzir necessidades de GPU quando possível (Quantização).
Observabilidade e melhores práticas operacionais
Métricas para acompanhar
- Utilização de GPU (mas também por que ela é baixa: esperando dados, gargalos de CPU)
- Percentis de tempo de espera na fila por fila/equipe
- Indicadores de fragmentação (por exemplo, “GPUs livres, mas nenhum posicionamento viável”)
- Contagens de preempção e trabalho perdido (tempo desde o último checkpoint)
- “Motivos de pending” (impossível de escalonar vs não admitido por cota)
Modos de falha comuns
- Cota sem empréstimo → GPUs ociosas quando uma equipe está inativa.
- Empréstimo sem limites → vizinhos barulhentos; sensação de injustiça.
- Prioridades demais → escalada constante e churn de política.
- Sem disciplina de checkpoint → preempção fica inutilizável, então trabalho urgente não consegue entrar.
- Ignorar topologia → jobs “rodam”, mas têm desempenho dramaticamente pior devido a coletivas lentas.
Checklist prático (projetando um sistema de filas para GPU)
- Defina 2–4 níveis de prioridade com uso pretendido claro.
- Implemente cotas garantidas por equipe mais empréstimo controlado.
- Adicione limites de concorrência para workloads no estilo sweep.
- Escolha uma estratégia de fragmentação:
- empacotamento para jobs pequenos
- pools de nós por formato
- posicionamento com consciência de topologia para jobs grandes
- Estabeleça padrões de checkpoint para que preempção seja possível.
- Separe trabalho interativo e inferência de produção do treinamento batch.
- Instrumente tempos de espera na fila e motivos de pending; torne isso visível para usuários.
Resumo
Escalonamento de GPU e filas do cluster são a camada de política e mecanismos que transforma um monte de aceleradores caros em uma plataforma de ML compartilhada e produtiva. As principais tensões são:
- Justiça vs throughput
- Baixa latência vs alta utilização
- Suporte a jobs grandes vs controle de fragmentação
- Tratamento de urgência vs trabalho desperdiçado por preempção
As configurações mais bem-sucedidas combinam:
- política forte de filas multi-tenant (cotas, compartilhamento justo, prioridades)
- posicionamento inteligente (empacotamento, consciência de topologia)
- disciplina operacional (checkpointing, observabilidade, diretrizes claras para usuários)
Juntas, elas evitam starvation, reduzem fragmentação e mantêm o cluster tanto ocupado quanto utilizável.