Buckets:

hf-doc-build/doc / diffusers /main /fr /optimization /context_parallelism.md
mishig's picture
|
download
raw
13.1 kB

Parallélisme de contexte

Le parallélisme de contexte répartit la séquence d’entrée sur plusieurs GPU, et chaque GPU traite sa propre portion. Cela réduit la mémoire par GPU, ce qui est particulièrement utile pour les séquences longues, comme les images haute résolution et les vidéos. Pour le comparer aux autres stratégies, consultez Inférence distribuée.

Le parallélisme de contexte s’exécute à l’intérieur du backend d’attention, et seuls certains backends le prennent en charge. Si le vôtre ne le fait pas, enable_parallelism lève une erreur qui énumère les backends compatibles. Changez de backend avec set_attention_backend() et consultez les backends disponibles pour la liste complète.

Configurez chaque variante ci-dessous avec ContextParallelConfig.

Variante Configuration Prérequis À utiliser lorsque
Ring Attention ring_degree > 1 la longueur de la séquence doit être divisible par ring_degree les séquences sont longues et la bande passante d'interconnexion est limitée
Attention Ulysses ulysses_degree > 1 la longueur de la séquence et le nombre de têtes d'attention doivent être divisibles par ulysses_degree vos GPU disposent d'un interconnect rapide
Attention unifiée ring_degree > 1 et ulysses_degree > 1 au moins 4 GPU vous disposez de plus d'appareils que l'attention Ulysses peut utiliser seul
Ulysses Anything Attention ulysses_degree > 1, ulysses_anything=True ring_degree doit être 1 la longueur de la séquence ou le nombre de têtes ne se divise pas exactement
Ring Anything Attention ring_degree > 1, ring_anything=True ulysses_degree doit être 1, inférence uniquement, attn_mask doit être None la longueur de la séquence n'est pas divisible

Ring Attention

Ring Attention transmet les blocs de clés (K) et de valeurs (V) entre des GPU disposés en anneau. Chaque GPU calcule l'attention par rapport au bloc K/V qu'il détient, puis transmet ce bloc au GPU suivant. Après un tour complet, chaque GPU a vu les K/V de tous les jetons, et aucun GPU ne détient jamais la séquence complète.

Définissez ring_degree sur le nombre de GPU et passez la configuration à la méthode enable_parallelism() du transformeur.

import os
import torch
from torch import distributed as dist
from diffusers import DiffusionPipeline, ContextParallelConfig

def setup_distributed():
    if not dist.is_initialized():
        dist.init_process_group(backend="nccl")
    # torchrun sets LOCAL_RANK to this process's GPU index on its node
    local_rank = int(os.environ["LOCAL_RANK"])
    device = torch.device(f"cuda:{local_rank}")
    torch.cuda.set_device(device)
    return device

def main():
    device = setup_distributed()
    world_size = dist.get_world_size()

    pipeline = DiffusionPipeline.from_pretrained(
        "black-forest-labs/FLUX.1-dev", dtype=torch.bfloat16
    ).to(device)
    pipeline.transformer.set_attention_backend("_native_cudnn")

    cp_config = ContextParallelConfig(ring_degree=world_size)
    pipeline.transformer.enable_parallelism(config=cp_config)

    prompt = """
    cinematic film still of a cat sipping a margarita in a pool in Palm Springs, California
    highly detailed, high budget hollywood movie, cinemascope, moody, epic, gorgeous, film grain
    """

    # Must specify generator so all ranks start with same latents (or pass your own)
    generator = torch.Generator().manual_seed(42)
    image = pipeline(
        prompt,
        guidance_scale=3.5,
        num_inference_steps=50,
        generator=generator,
    ).images[0]

    if dist.get_rank() == 0:
        image.save("output.png")

    if dist.is_initialized():
        dist.destroy_process_group()

if __name__ == "__main__":
    main()

Enregistrez le script sous le nom ring_attention.py et lancez-le avec torchrun ou un autre lanceur compatible PyTorch. Définissez --nproc-per-node sur le nombre de GPU.

torchrun --nproc-per-node 2 ring_attention.py

Pour essayer les autres variantes, remplacez l'appel enable_parallelism dans ring_attention.py par celui de leur section.

Ulysses Attention

Ulysses Attention utilise la communication all-to-all, où chaque GPU échange des données avec tous les autres GPU. Le premier all-to-all regroupe les données de sorte que chaque GPU détient tous les jetons pour un sous-ensemble de têtes d'attention. Chaque GPU calcule l'attention pour ses têtes, et un second all-to-all regroupe les résultats par jeton pour la couche suivante.

Définissez ulysses_degree sur le nombre de GPU et passez la configuration à enable_parallelism().

pipeline.transformer.enable_parallelism(config=ContextParallelConfig(ulysses_degree=2))

Attention unifiée

Unified Sequence Parallelism combine Ring Attention et Ulysses Attention. Il exécute un all-to-all pour redistribuer les têtes et les jetons, applique Ring Attention au résultat, puis inverse l'all-to-all pour restaurer la disposition d'origine. Ulysses parallélise sur les têtes d'attention et Ring sur la séquence, ce qui leur permet, ensemble, d'utiliser plus de GPU qu'Ulysses seul.

Utilisez Unified Attention lorsque vous disposez d’au moins 4 GPU. Définissez à la fois ulysses_degree et ring_degree sur une valeur supérieure à 1. Les GPU forment une grille 2D de ulysses_degree * ring_degree dispositifs, et chaque GPU appartient à un groupe Ulysses et à un groupe Ring.

pipeline.transformer.enable_parallelism(config=ContextParallelConfig(ulysses_degree=2, ring_degree=2))

Le benchmark ci-dessous compare Ulysses, Ring et Unified Attention sur 4 GPU H100 à l'aide de ce script. Chaque image utilise 50 étapes d'inférence.

Backend CP Temps / Image (ms) Étapes / Sec Mémoire maximale (Go)
ulysses 6670.789 7.50 33.85
ring 13076.492 3.82 56.02
unified_balanced 11068.705 4.52 33.85

Ulysses offre le débit le plus élevé, mais il ne peut pas utiliser plus de GPU que le nombre de têtes d'attention du modèle. Unified Attention supprime cette limite.

Ulysses Anything Attention

L’attention Ulysses exige que la longueur de la séquence et le nombre de têtes d’attention soient divisibles par ulysses_degree. Ulysses Anything Attention supprime ces deux contraintes. Il ne peut pas être combiné avec Unified Attention, donc ring_degree doit être égal à 1.

Définissez ulysses_degree sur une valeur supérieure à 1 et ulysses_anything=True.

pipeline.transformer.enable_parallelism(config=ContextParallelConfig(ulysses_degree=2, ulysses_anything=True))

Ajoutez un backend gloo pour les tenseurs CPU à init_process_group. Cela évite les synchronisations CUDA forcées causées par les transferts hôte-vers-appareil et appareil-vers-hôte.

import torch.distributed as dist

dist.init_process_group(backend="cpu:gloo,cuda:nccl")

Le benchmark ci-dessous exécute FLUX.1-dev sur 4 GPU L20 avec ce script.

Backend CP Temps / Itération (ms) Étapes / Sec Mémoire maximale (Go) Forme (HxW)
ulysses 281.07 3.56 37.11 1024x1024
ring 351.34 2.85 37.01 1024x1024
unified_balanced 324.37 3.08 37.16 1024x1024
ulysses_anything 280.94 3.56 37.11 1024x1024
ulysses échoué échoué échoué 1008x1008
ring échoué échoué échoué 1008x1008
unified_balanced échoué échoué échoué 1008x1008
ulysses_anything 278.40 3.59 36.99 1008x1008

À 1008x1008, où la longueur de la séquence ne se divise pas exactement, seul Ulysses Anything Attention s'exécute. À 1024x1024, il offre les mêmes performances que l'Ulysses Attention standard.

Ring Anything Attention

Ring Attention exige que la longueur de la séquence soit divisible par ring_degree. Ring Anything Attention prend en charge toute longueur de séquence. Il remplit les K/V locaux de chaque GPU jusqu'à la longueur locale maximale, effectue un all-gather sur les K/V remplis, puis recoupe chaque bloc à sa longueur réelle avant de calculer l'attention.

Vous ne pouvez pas le combiner avec Unified Attention, donc ulysses_degree doit être 1. Définissez ring_degree sur une valeur supérieure à 1 et ring_anything=True.

pipeline.transformer.enable_parallelism(config=ContextParallelConfig(ring_degree=2, ring_anything=True))

Ring Anything Attention bénéficie également de l’arrière-plan gloo présenté dans Ulysses Anything Attention.

Ring Anything Attention prend en charge uniquement l'inférence, et attn_mask doit être None.

Le benchmark ci-dessous exécute FLUX.1-dev sur 4 GPU RTX 4090 (48 Go).

Backend CP Temps / Itération (ms) Étapes / Sec Mémoire maximale (Go) Forme (HxW)
ulysses 259.07 3.86 33.83 1024x1024
ring 338.98 2.95 33.83 1024x1024
unified_balanced 321.54 3.11 33.83 1024x1024
ulysses_anything 259.07 3.86 33.83 1024x1024
ring_anything 340.14 2.94 33.83 1024x1024
ulysses échoué échoué échoué 1008x1008
ring échoué échoué échoué 1008x1008
unified_balanced échoué échoué échoué 1008x1008
ulysses_anything 253.16 3.95 33.75 1008x1008
ring_anything 335.57 2.98 33.75 1008x1008

Ring Anything Attention s’exécute à 1008x1008 et offre des performances similaires à celles de Ring Attention standard. Pour la motivation et les compromis, consultez ce commentaire.

Activer le parallélisme de contexte lors du chargement

Au lieu d'appeler enable_parallelism après le chargement, passez le ContextParallelConfig à l'argument parallel_config de from_pretrained(). Initialisez d'abord le groupe de processus, car from_pretrained lève une erreur si torch.distributed n'est pas initialisé. Lancez le script avec torchrun, comme dans l'exemple Ring Attention.

import os
import torch
from torch import distributed as dist
from diffusers import AutoModel, ContextParallelConfig, DiffusionPipeline

CKPT_ID = "black-forest-labs/FLUX.1-dev"

dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"])
device = torch.device(f"cuda:{local_rank}")
torch.cuda.set_device(device)

cp_config = ContextParallelConfig(ring_degree=dist.get_world_size())
transformer = AutoModel.from_pretrained(
    CKPT_ID,
    subfolder="transformer",
    dtype=torch.bfloat16,
    parallel_config=cp_config
)

pipeline = DiffusionPipeline.from_pretrained(
    CKPT_ID, transformer=transformer, dtype=torch.bfloat16,
).to(device)

Xet Storage Details

Size:
13.1 kB
·
Xet hash:
8ceb2517f09c3dc07111fee98f9938340babd5c9a1c83450d0a0fdbbfcaa891e

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.