Paralelismo de Dados/Modelo/Pipeline
Visão geral
Treinar modelos modernos de aprendizado profundo (deep learning) de forma eficiente muitas vezes exige usar múltiplas GPUs (ou nós (nodes)). As estratégias de treinamento paralelo respondem a duas restrições centrais:
- Vazão de computação (compute throughput): um dispositivo é lento demais para o tempo de treinamento desejado (time-to-train).
- Capacidade de memória (memory capacity): um dispositivo não consegue armazenar o modelo, o estado do otimizador (optimizer state), as ativações (activations) ou o lote (batch).
As três estratégias mais comuns são:
- paralelismo de dados (data parallelism, DP): replicar o modelo em cada dispositivo e dividir o lote entre os dispositivos.
- paralelismo de modelo (model parallelism, MP): dividir o próprio modelo entre dispositivos (existem vários subtipos).
- paralelismo de pipeline (pipeline parallelism, PP): dividir o modelo em “estágios (stages)” sequenciais e executar diferentes microlotes (microbatches) pelos estágios como numa linha de montagem.
Na prática, o treinamento em grande escala frequentemente usa paralelismo híbrido (hybrid parallelism) (por exemplo, DP × paralelismo de modelo por tensor (tensor-model-parallel) × paralelismo de pipeline (pipeline-parallel)). Este artigo explica como cada estratégia funciona, quais trade-offs ela faz e quando é apropriada.
Contexto relacionado: Treinamento Distribuído (Distributed Training), Precisão Mista (Mixed Precision), Carregamento de Dados e Pipelines de Entrada (Data Loading & Input Pipelines), Checkpoints e Tolerância a Falhas (Checkpointing & Fault Tolerance), e mecânicas centrais de aprendizado como Descida de Gradiente (Gradient Descent) e Retropropagação (Backpropagation).
Uma base compartilhada: SGD, gradientes e comunicação
A maior parte do treinamento usa variantes de descida de gradiente estocástica (stochastic gradient descent, SGD):
- Computar gradientes em um minilote (minibatch): ( g = \nabla_\theta \mathcal{L}(\theta; \text{batch}) )
- Atualizar parâmetros usando um otimizador (optimizer) (SGD, Adam etc.)
O paralelismo muda como o lote e o modelo são distribuídos e como gradientes/parâmetros são sincronizados.
Primitivas distribuídas (distributed primitives) importantes que você verá:
- operação all-reduce (all-reduce): soma/média de tensores (tensors) entre todos os trabalhadores (workers); comumente usada para fazer a média de gradientes no paralelismo de dados.
- operação all-gather (all-gather) / operação reduce-scatter (reduce-scatter): usadas para fragmentar e reconstruir fragmentos de parâmetros/gradientes (comum em paralelismo de modelo fragmentado).
- envio/recebimento ponto a ponto (send/recv (point-to-point)): usado entre estágios de pipeline.
Um modelo mental útil: o desempenho costuma ser limitado por volume de comunicação × latência em relação ao custo de computação. Interconexões (interconnects) mais rápidas (NVLink/NVSwitch, InfiniBand) tornam viáveis mais estratégias de paralelismo.
Paralelismo de Dados (DP)
O que é
No paralelismo de dados, cada dispositivo mantém uma cópia completa do modelo. O lote global é dividido entre dispositivos:
- GPU 0 recebe amostras 0..N
- GPU 1 recebe amostras N..2N
- …
Cada GPU realiza a passagem direta e a retropropagação (forward/backward) no seu minilote local, produzindo gradientes locais. Em seguida, os gradientes são agregados (normalmente por média) entre todos os dispositivos, e cada réplica aplica a mesma atualização.
Essa é a abordagem padrão para escalar o treinamento quando o modelo cabe em um único dispositivo.
DP síncrono vs assíncrono
DP síncrono (mais comum): todos os trabalhadores executam cada passo juntos; os gradientes são agregados (por exemplo, via operação all-reduce) e então ocorre a atualização.
- Prós: convergência estável; raciocínio mais simples.
- Contras: trabalhadores mais lentos (“atrasados (stragglers)”) podem virar gargalo.
DP assíncrono (menos comum no treinamento moderno em GPU): os trabalhadores atualizam de forma independente, muitas vezes via um servidor de parâmetros (parameter server).
- Prós: pode reduzir esperas.
- Contras: gradientes defasados (stale) podem prejudicar a convergência; ajuste (tuning) mais complexo.
Padrão de comunicação
A implementação clássica usa operação all-reduce sobre os gradientes:
- Cada trabalhador computa gradientes (g_i)
- Agrega: (g = \frac{1}{W}\sum_i g_i)
- Aplica atualização idêntica em cada réplica
Implementações eficientes frequentemente usam all-reduce em anel (ring all-reduce) ou algoritmos baseados em árvore.
Exemplo prático: PyTorch DDP (DistributedDataParallel)
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
def setup():
dist.init_process_group(backend="nccl")
setup()
torch.cuda.set_device(local_rank)
model = MyModel().cuda()
model = DDP(model, device_ids=[local_rank])
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
for batch in dataloader: # use a DistributedSampler for sharding data
x, y = batch[0].cuda(), batch[1].cuda()
optimizer.zero_grad(set_to_none=True)
loss = model(x, y)
loss.backward() # DDP all-reduces gradients here (by default)
optimizer.step()
DP depende fortemente de um bom pipeline de entrada; veja Carregamento de Dados e Pipelines de Entrada.
Quando o paralelismo de dados é apropriado
Use DP quando:
- O modelo cabe na memória da GPU (parâmetros + estado do otimizador + ativações).
- Você quer o método de escalonamento mais simples e robusto.
- Você consegue aumentar o tamanho do lote global sem prejudicar a convergência.
Gargalos e ressalvas comuns em DP
- Tamanho efetivo do lote cresce com o número de trabalhadores:
(\text{global_batch} = \text{local_batch} \times W).
Lotes muito grandes podem exigir escalonamento da taxa de aprendizado, aquecimento (warmup) ou mudanças no otimizador. - Comunicação de gradientes pode dominar em grande escala, especialmente com muitos nós.
- Memória do estado do otimizador (por exemplo, Adam mantém buffers extras de momentos) pode ser o limitador real mesmo quando os parâmetros cabem.
- Acumulação de gradientes (gradient accumulation) pode emular lotes maiores sem aumentar a frequência de comunicação, mas aumenta o tempo por passo e a memória de ativações.
Paralelismo de Modelo (MP)
“Paralelismo de modelo” é um termo guarda-chuva para dividir o modelo entre dispositivos. Duas formas amplamente usadas são o paralelismo por tensor (tensor (intra-layer) parallelism) e o paralelismo fragmentado de parâmetros (sharded (parameter) parallelism). (Às vezes as pessoas também incluem o paralelismo de especialistas (expert parallelism) de MoE, mas as ideias centrais são semelhantes: particionar parâmetros e rotear/agregar dados.)
1) Paralelismo de modelo por tensor (intra-camada)
O que é
O paralelismo por tensor divide os tensores de camadas individuais entre dispositivos. Por exemplo, em um MLP de transformer (transformer):
- A matriz de pesos (W \in \mathbb{R}^{d \times 4d}) pode ser dividida por colunas entre GPUs.
- Cada GPU computa sua saída parcial e, em seguida, a comunicação coletiva combina os resultados.
Isso é comum no treinamento em larga escala de Arquitetura Transformer (Transformer Architecture) (por exemplo, paralelismo por tensor no estilo Megatron).
Padrão de comunicação
Dependendo de como você fragmenta:
- Camadas lineares paralelas por coluna frequentemente exigem uma operação all-gather mais adiante para montar ativações completas (ou mantê-las fragmentadas ao longo das operações subsequentes).
- Camadas paralelas por linha frequentemente exigem uma operação reduce-scatter ou operação all-reduce para somar resultados parciais.
O trade-off principal: o paralelismo por tensor reduz a computação e a memória por dispositivo para uma camada, mas introduz comunicação frequente de ativações dentro de cada camada.
Quando é apropriado
Use paralelismo por tensor quando:
- As camadas são tão grandes (“largas”) que não cabem em uma única GPU (por exemplo, dimensões ocultas enormes).
- Você tem links intra-nó rápidos (NVLink/NVSwitch), porque a comunicação acontece com frequência.
- Você precisa escalar computação mantendo o tamanho do lote moderado.
2) Paralelismo de modelo fragmentado (fragmentação de parâmetros/otimizador)
Essa família inclui abordagens como o Otimizador de Redundância Zero (Zero Redundancy Optimizer, ZeRO) e o Paralelismo de Dados Totalmente Fragmentado (Fully Sharded Data Parallel, FSDP). Às vezes isso é descrito como “paralelismo de dados com estados fragmentados”, mas conceitualmente também é paralelismo de modelo porque parâmetros e estado do otimizador são particionados entre dispositivos.
O que é
Em vez de replicar todos os parâmetros e estados do otimizador em cada GPU:
- Cada GPU armazena apenas um fragmento dos parâmetros e/ou do estado do otimizador.
- Durante a passagem direta/retropropagação, os parâmetros são reunidos via operação all-gather conforme necessário, e então os gradientes são redistribuídos via operação reduce-scatter de volta para os fragmentos.
Isso reduz drasticamente a pressão de memória, especialmente para otimizadores do tipo Adam.
Quando é apropriado
Use fragmentação quando:
- O modelo quase cabe, mas o estado do otimizador (ou os gradientes) estoura a memória.
- Você quer escalonamento no estilo DP, mas precisa de alívio de memória.
- Você consegue tolerar comunicação extra durante a passagem direta/retropropagação.
Exemplo prático: esboço conceitual (estilo FSDP)
# Pseudocode: exact APIs vary by framework/version
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
model = MyLargeModel().cuda()
model = FSDP(model) # shards parameters, gradients, optimizer state (depending on config)
for batch in dataloader:
loss = model(batch)
loss.backward()
optimizer.step()
Trade-offs do paralelismo de modelo
Prós:
- Permite treinar modelos que não cabem em um único dispositivo.
- Pode melhorar o escalonamento de computação sem inflar tanto o tamanho do lote global quanto em DP.
Contras:
- Implementação e depuração mais complexas.
- A comunicação se torna mais frequente e mais acoplada à estrutura do modelo.
- O desempenho depende fortemente da topologia (topology) (largura de banda intra-nó vs inter-nó).
Paralelismo de Pipeline (PP)
O que é
O paralelismo de pipeline divide o modelo por camadas (ou blocos) em estágios sequenciais:
- Estágio 0: camadas 0–k
- Estágio 1: camadas k+1–m
- Estágio 2: camadas m+1–…
Um lote é dividido em microlotes que fluem pelos estágios. Enquanto o estágio 0 processa o microlote 2, o estágio 1 pode processar o microlote 1 etc. Isso sobrepõe computação entre dispositivos.
A “bolha” do pipeline e o escalonamento
O desafio clássico é a bolha do pipeline (pipeline bubble): no início e no fim de um lote, alguns estágios ficam ociosos.
Estratégias de escalonamento reduzem o overhead da bolha:
- estilo GPipe (GPipe-style): executar todos os microlotes no forward, depois todos no backward (simples, mas pode aumentar a memória de ativações).
- 1F1B (one-forward-one-backward): após o aquecimento, cada estágio alterna forward/backward em microlotes para reduzir o tempo ocioso e a memória.
- pipelines intercalados (interleaved pipelines): atribuir múltiplos pedaços do modelo a cada dispositivo para melhorar a utilização.
Uma intuição aproximada de utilização:
- Mais microlotes → melhor utilização (menor fração de bolha)
- Microlotes demais → mais overhead e possivelmente pior eficiência de kernels
Padrão de comunicação
PP normalmente usa envio/recebimento ponto a ponto de ativações e gradientes entre estágios adjacentes. O volume de comunicação costuma ser menor do que no paralelismo por tensor (que comunica dentro de muitas camadas), mas PP introduz dependências de pipeline e sensibilidade à latência.
Quando o paralelismo de pipeline é apropriado
Use PP quando:
- O modelo é muito profundo (muitos blocos sequenciais) e pode ser particionado de forma limpa.
- Você precisa escalar para muitas GPUs, mas quer evitar frequência excessiva de all-reduce.
- A memória é limitada: PP pode reduzir a memória de ativações por GPU porque cada estágio armazena ativações apenas da sua parte (embora microlotização e escalonamento façam diferença).
PP é especialmente comum em grandes treinamentos de transformers, combinado com paralelismo por tensor e paralelismo de dados.
Exemplo prático: pipeline em PyTorch (conceitual)
PyTorch e bibliotecas como DeepSpeed fornecem abstrações de pipeline. Um esboço conceitual simplificado:
# Conceptual: actual APIs differ; shown to illustrate the idea
stages = [
EncoderBlockStack0().cuda(0),
EncoderBlockStack1().cuda(1),
EncoderBlockStack2().cuda(2),
Head().cuda(3),
]
# Split input batch into microbatches and run a pipeline schedule.
for microbatches in split_into_microbatches(batch, num_microbatches=8):
outputs = pipeline_forward(stages, microbatches) # sends activations between GPUs
losses = compute_loss(outputs)
pipeline_backward(stages, losses) # sends gradients backward
Ressalvas de PP
- Balanceamento de carga é crucial: se um estágio tem mais computação, ele vira o gargalo e todos esperam.
- Escolha de microlotes afeta memória e vazão.
- Sensibilidade à latência: PP inter-nó pode sofrer se os links forem lentos ou congestionados.
- Alguns modelos têm fronteiras incômodas (por exemplo, camadas de atenção com dependências globais), embora transformers geralmente façam pipeline bem em fronteiras de blocos.
Paralelismo híbrido (como sistemas reais escalam)
Para o treinamento de modelos grandes no estado da arte, uma receita comum é o paralelismo 3D (3D parallelism):
- paralelismo por tensor (TP) dentro de um nó (links rápidos)
- paralelismo de pipeline (PP) entre grupos de GPUs (possivelmente entre nós)
- paralelismo de dados (DP) entre réplicas do modelo TP×PP para escalar a vazão
Uma topologia típica pode ser:
- 8 GPUs/nó com NVLink:
- TP = 2 ou 4 (dentro do nó)
- PP = 2 (dois estágios de pipeline)
- DP = número de nós (réplicas)
Isso equilibra:
- TP para memória/compute por camada
- PP para particionar a profundidade
- DP para escalar a vazão
Abordagens fragmentadas (FSDP/ZeRO) frequentemente são adicionadas por cima para reduzir ainda mais a pegada de memória.
Escolhendo a estratégia certa (orientação prática)
Comece com estas perguntas
O modelo (com estado do otimizador) cabe em uma GPU?
- Sim → comece com paralelismo de dados (DDP) pela simplicidade.
- Não → você precisa de fragmentação de modelo (FSDP/ZeRO), paralelismo por tensor, paralelismo de pipeline ou um híbrido.
O modelo é “largo” (matrizes enormes) ou “profundo” (muitos blocos)?
- Largo → considere paralelismo por tensor
- Profundo → considere paralelismo de pipeline
Qual é a sua interconexão?
- Links intra-nó fortes (NVLink/NVSwitch): paralelismo por tensor fica muito mais atraente.
- Links inter-nó mais lentos: prefira menos comunicações, mas maiores (DP com agrupamento de gradientes (gradient bucketing); PP com microlotização cuidadosa; evite TP excessivamente “falante” entre nós).
Você precisa de um lote pequeno por motivos de otimização?
- Se você não consegue aumentar muito o tamanho do lote global, a escala apenas com DP é limitada; considere MP/PP para aumentar computação sem aumentar o lote de forma tão agressiva.
Recomendações práticas (regra de bolso)
- Modelos pequenos a médios (cabem com folga): DP (DDP) + Precisão Mista.
- Modelo mal cabe / estado do otimizador grande demais: DP fragmentado (FSDP/ZeRO).
- Modelos de linguagem grandes (large language models, LLMs) muito grandes:
- Comece com fragmentação (ZeRO/FSDP) por memória
- Adicione TP se as camadas forem grandes demais
- Adicione PP se a profundidade total/memória de ativações ou a escala exigirem
- Adicione DP por último para vazão
Considerações práticas de engenharia
Tamanho de lote, acumulação de gradientes e dinâmica de aprendizado
- DP aumenta o tamanho do lote global; você pode precisar:
- escalonamento de taxa de aprendizado (frequentemente escalonamento linear com warmup)
- mais passos de treinamento ou ajustes no cronograma (schedule)
- Acumulação de gradientes aumenta o tamanho efetivo do lote sem aumentar os trabalhadores em DP e reduz a frequência de comunicação (comunicar a cada k micro-passos), mas aumenta a memória de ativações, a menos que seja combinada com checkpointing de ativações (activation checkpointing).
Memória: parâmetros vs estado do otimizador vs ativações
- Parâmetros: (P)
- Gradientes: (P)
- Estados do otimizador Adam: frequentemente ~(2P) (primeiro e segundo momentos)
- Ativações: depende de lote, comprimento de sequência e checkpointing
Por isso, fragmentação e paralelismo por pipeline/tensor muitas vezes são guiados por memória do otimizador e das ativações, não apenas pelos parâmetros.
Precisão mista e comunicação
Usar BF16/FP16 reduz:
- tempo de computação (Tensor Cores)
- memória de ativações
- às vezes o custo de comunicação (se os gradientes forem comunicados em precisão reduzida, embora muitos sistemas acumulem em FP32)
Veja Precisão Mista para detalhes de estabilidade numérica.
Vazão do pipeline de entrada
Escalar o treinamento é desperdício se as GPUs ficarem ociosas esperando dados. Treinamento distribuído exige:
- fragmentação de dados por rank (por exemplo, DistributedSampler)
- armazenamento/IO e pré-processamento rápidos
- ajuste cuidadoso de workers/prefetch
Veja Carregamento de Dados e Pipelines de Entrada.
Tolerância a falhas e checkpoints em escala
Mais dispositivos → falhas viram algo normal. Para DP/MP/PP, você provavelmente vai precisar:
- checkpoints distribuídos (possivelmente fragmentados)
- retomada com configuração idêntica de paralelismo (ou uma etapa de conversão)
- validação periódica e salvamento de estado
Veja Checkpoints e Tolerância a Falhas.
Armadilhas comuns e dicas de depuração
- Perda silenciosa de desempenho por desequilíbrio
- Desequilíbrio entre estágios em PP ou particionamento desigual no paralelismo por tensor pode reduzir a utilização drasticamente.
- Deadlocks (deadlocks)
- Chamadas coletivas incompatíveis (por exemplo, um rank entra em all-reduce e outro não) ou ordem incompatível de envio/recebimento em PP.
- Divergência em escala
- Frequentemente causada por mudanças no tamanho do lote, overflow em precisão mista ou hiperparâmetros do otimizador não reajustados.
- Saturação de banda
- All-reduce de gradientes pode saturar a interconexão; técnicas incluem agrupamento de gradientes, sobreposição de comunicação com retropropagação ou reduzir a frequência de comunicação via acumulação.
- Limites de reprodutibilidade
- Coletivas (collectives) e escalonamentos paralelos podem reordenar operações de ponto flutuante; determinismo exato é difícil em escala. Veja Reprodutibilidade (Reproducibility).
Resumo
- Paralelismo de dados é a estratégia mais simples e comum: replicar o modelo, dividir dados, fazer all-reduce de gradientes. Melhor quando o modelo cabe e o tamanho do lote pode escalar.
- Paralelismo de modelo divide o modelo para superar limites de memória/compute:
- Paralelismo por tensor divide camadas grandes e é pesado em comunicação, mas é poderoso com interconexões rápidas.
- Abordagens fragmentadas (FSDP/ZeRO) reduzem memória ao particionar parâmetros/estado do otimizador, mantendo um fluxo de trabalho parecido com DP.
- Paralelismo de pipeline divide o modelo por camadas e usa microlotes para manter dispositivos ocupados; é eficaz para modelos profundos, mas exige escalonamento e balanceamento de carga cuidadosos.
- Treinamento em grande escala comumente usa híbridos (DP × TP × PP, frequentemente com fragmentação) ajustados ao formato do modelo e à topologia de hardware.
Se você já conhece o tamanho do seu modelo, o tipo de GPU e a topologia de rede, normalmente dá para chegar rapidamente a uma boa configuração inicial; a partir daí, fazer perfilamento (profiling) da sobreposição entre comunicação e computação é o caminho mais rápido para um setup eficiente.