Buckets:

mishig's picture
|
download
raw
18.2 kB

ParaAttention

Les grands modèles de génération d’images et de vidéos, tels que FLUX.1-dev et HunyuanVideo, peuvent poser un défi d’inférence pour les applications en temps réel et le déploiement en raison de leur taille.

ParaAttention est une bibliothèque qui implémente le context parallelism et le first block cache, et peut être combinée avec d'autres techniques (torch.compile, quantification dynamique fp8) pour accélérer l'inférence.

Ce guide vous montre comment appliquer ParaAttention à FLUX.1-dev et HunyuanVideo sur des GPU NVIDIA L20. Aucune optimisation n’est appliquée pour notre benchmark de référence, à l’exception de HunyuanVideo afin d’éviter les erreurs de mémoire insuffisante.

Notre benchmark de référence montre que FLUX.1-dev est capable de générer une image en résolution 1024x1024 en 28 étapes en 26,36 secondes, et que HunyuanVideo est capable de générer 129 images en résolution 720p en 30 étapes en 3675,71 secondes.

Pour une inférence encore plus rapide avec le parallélisme de contexte, essayez d’utiliser des GPU NVIDIA A100 ou H100 (si disponibles) avec prise en charge de NVLink, en particulier lorsqu’il y a un grand nombre de GPU.

Cache du premier bloc

Mettre en cache la sortie des blocs de transformers du modèle et la réutiliser aux étapes d'inférence suivantes réduit le coût de calcul et accélère l'inférence.

Cependant, il est difficile de déterminer quand réutiliser le cache afin de garantir la qualité des images ou vidéos générées. ParaAttention utilise directement la différence résiduelle de la sortie du premier bloc transformer pour approximer l'écart entre les sorties du modèle. Lorsque cet écart est suffisamment faible, la différence résiduelle des étapes d'inférence précédentes est réutilisée. Autrement dit, l'étape de débruitage est ignorée.

Cela permet d'obtenir un gain de vitesse de 2x lors de l'inférence de FLUX.1-dev et HunyuanVideo, avec une très bonne qualité.

Fonctionnement d'AdaCache, First Block Cache en est une variante

Pour appliquer le cache du premier bloc sur FLUX.1-dev, appelez apply_cache_on_pipe comme indiqué ci-dessous. 0,08 est la valeur par défaut de la différence résiduelle pour les modèles FLUX.

import time
import torch
from diffusers import FluxPipeline

pipe = FluxPipeline.from_pretrained(
    "black-forest-labs/FLUX.1-dev",
    dtype=torch.bfloat16,
).to("cuda")  # or "mps", "xpu", "cpu"

from para_attn.first_block_cache.diffusers_adapters import apply_cache_on_pipe

apply_cache_on_pipe(pipe, residual_diff_threshold=0.08)

# Enable memory savings
# pipe.enable_model_cpu_offload()
# pipe.enable_sequential_cpu_offload()

begin = time.time()
image = pipe(
    "A cat holding a sign that says hello world",
    num_inference_steps=28,
).images[0]
end = time.time()
print(f"Time: {end - begin:.2f}s")

print("Saving image to flux.png")
image.save("flux.png")
Optimisations Original FBCache rdt=0.06 FBCache rdt=0.08 FBCache rdt=0.10 FBCache rdt=0.12
Aperçu Original FBCache rdt=0.06 FBCache rdt=0.08 FBCache rdt=0.10 FBCache rdt=0.12
Temps réel (s) 26.36 21.83 17.01 16.00 13.78

First Block Cache a réduit la vitesse d’inférence à 17,01 secondes par rapport à la référence, soit 1,55 fois plus rapide, tout en maintenant une perte de qualité quasi nulle.

Pour appliquer First Block Cache sur HunyuanVideo, utilisez apply_cache_on_pipe comme indiqué ci-dessous. 0,06 est la valeur par défaut de la différence résiduelle pour les modèles HunyuanVideo.

import time
import torch
from diffusers import HunyuanVideoPipeline, HunyuanVideoTransformer3DModel
from diffusers.utils import export_to_video

model_id = "tencent/HunyuanVideo"
transformer = HunyuanVideoTransformer3DModel.from_pretrained(
    model_id,
    subfolder="transformer",
    dtype=torch.bfloat16,
    revision="refs/pr/18",
)
pipe = HunyuanVideoPipeline.from_pretrained(
    model_id,
    transformer=transformer,
    dtype=torch.float16,
    revision="refs/pr/18",
).to("cuda")  # or "mps", "xpu", "cpu"

from para_attn.first_block_cache.diffusers_adapters import apply_cache_on_pipe

apply_cache_on_pipe(pipe, residual_diff_threshold=0.6)

pipe.vae.enable_tiling()

begin = time.time()
output = pipe(
    prompt="A cat walks on the grass, realistic",
    height=720,
    width=1280,
    num_frames=129,
    num_inference_steps=30,
).frames[0]
end = time.time()
print(f"Time: {end - begin:.2f}s")

print("Saving video to hunyuan_video.mp4")
export_to_video(output, "hunyuan_video.mp4", fps=15)

Votre navigateur ne prend pas en charge la balise vidéo.

HunyuanVideo sans FBCache

Votre navigateur ne prend pas en charge la balise vidéo.

HunyuanVideo avec FBCache

First Block Cache a réduit la vitesse d’inférence à 2271,06 secondes par rapport à la référence, soit 1,62 fois plus rapide, tout en maintenant une perte de qualité quasi nulle.

quantification fp8

fp8 avec quantification dynamique accélère davantage l’inférence et réduit l’utilisation de la mémoire. Les activations et les poids doivent tous deux être quantifiés afin d’utiliser les NVIDIA Tensor Cores 8 bits.

Utilisez float8_weight_only et float8_dynamic_activation_float8_weight pour quantifier le codeur de texte et le modèle transformer.

La méthode de quantification par défaut est la quantification par tenseur, mais si votre GPU prend en charge la quantification par ligne, vous pouvez également l'essayer pour une meilleure précision.

Installez torchao avec la commande ci-dessous.

pip3 install -U torch torchao

torch.compile avec mode="max-autotune-no-cudagraphs" ou mode="max-autotune" sélectionne le meilleur noyau pour les performances. La compilation peut prendre beaucoup de temps lors du premier appel du modèle, mais cela en vaut la peine une fois le modèle compilé.

Cet exemple ne quantise que le modèle transformer, mais vous pouvez également quantiser l’encodeur de texte pour réduire encore davantage l’utilisation de la mémoire.

La quantification dynamique peut modifier considérablement la distribution de la sortie du modèle, il est donc nécessaire d’augmenter la valeur de residual_diff_threshold pour que cela prenne effet.

import time
import torch
from diffusers import FluxPipeline

pipe = FluxPipeline.from_pretrained(
    "black-forest-labs/FLUX.1-dev",
    dtype=torch.bfloat16,
).to("cuda")  # or "mps", "xpu", "cpu"

from para_attn.first_block_cache.diffusers_adapters import apply_cache_on_pipe

apply_cache_on_pipe(
    pipe,
    residual_diff_threshold=0.12,  # Use a larger value to make the cache take effect
)

from torchao.quantization import quantize_, float8_dynamic_activation_float8_weight, float8_weight_only

quantize_(pipe.text_encoder, float8_weight_only())
quantize_(pipe.transformer, float8_dynamic_activation_float8_weight())
pipe.transformer = torch.compile(
   pipe.transformer, mode="max-autotune-no-cudagraphs",
)

# Enable memory savings
# pipe.enable_model_cpu_offload()
# pipe.enable_sequential_cpu_offload()

for i in range(2):
    begin = time.time()
    image = pipe(
        "A cat holding a sign that says hello world",
        num_inference_steps=28,
    ).images[0]
    end = time.time()
    if i == 0:
        print(f"Warm up time: {end - begin:.2f}s")
    else:
        print(f"Time: {end - begin:.2f}s")

print("Saving image to flux.png")
image.save("flux.png")

La quantification dynamique fp8 et torch.compile ont réduit le temps d’inférence à 7,56 secondes par rapport à la référence, soit 3,48 fois plus rapide.

import time
import torch
from diffusers import HunyuanVideoPipeline, HunyuanVideoTransformer3DModel
from diffusers.utils import export_to_video

model_id = "tencent/HunyuanVideo"
transformer = HunyuanVideoTransformer3DModel.from_pretrained(
    model_id,
    subfolder="transformer",
    dtype=torch.bfloat16,
    revision="refs/pr/18",
)
pipe = HunyuanVideoPipeline.from_pretrained(
    model_id,
    transformer=transformer,
    dtype=torch.float16,
    revision="refs/pr/18",
).to("cuda")  # or "mps", "xpu", "cpu"

from para_attn.first_block_cache.diffusers_adapters import apply_cache_on_pipe

apply_cache_on_pipe(pipe)

from torchao.quantization import quantize_, float8_dynamic_activation_float8_weight, float8_weight_only

quantize_(pipe.text_encoder, float8_weight_only())
quantize_(pipe.transformer, float8_dynamic_activation_float8_weight())
pipe.transformer = torch.compile(
   pipe.transformer, mode="max-autotune-no-cudagraphs",
)

# Enable memory savings
pipe.vae.enable_tiling()
# pipe.enable_model_cpu_offload()
# pipe.enable_sequential_cpu_offload()

for i in range(2):
    begin = time.time()
    output = pipe(
        prompt="A cat walks on the grass, realistic",
        height=720,
        width=1280,
        num_frames=129,
        num_inference_steps=1 if i == 0 else 30,
    ).frames[0]
    end = time.time()
    if i == 0:
        print(f"Warm up time: {end - begin:.2f}s")
    else:
        print(f"Time: {end - begin:.2f}s")

print("Saving video to hunyuan_video.mp4")
export_to_video(output, "hunyuan_video.mp4", fps=15)

Un GPU NVIDIA L20 ne dispose que de 48 Go de mémoire et peut rencontrer des erreurs de mémoire insuffisante (OOM) après la compilation si enable_model_cpu_offload n'est pas appelé, car HunyuanVideo génère des tenseurs d'activation très volumineux lors de l'exécution en haute résolution et avec un grand nombre d'images. Pour les GPU disposant de moins de 80 Go de mémoire, vous pouvez essayer de réduire la résolution et le nombre d'images pour éviter les erreurs OOM.

Les grands modèles de génération vidéo sont généralement limités par les calculs d'attention plutôt que par les couches entièrement connectées. Ces modèles ne bénéficient pas significativement de la quantification et de torch.compile.

Parallélisme de contexte

Le parallélisme de contexte (Context Parallelism) parallélise l'inférence et s'échelonne sur plusieurs GPU. La conception compositionnelle de ParaAttention vous permet de combiner le parallélisme de contexte avec le cache du premier bloc et la quantification dynamique.

Reportez-vous au dépôt ParaAttention pour des instructions détaillées et des exemples sur la manière de mettre à l’échelle l’inférence avec plusieurs GPU.

Si le processus d’inférence doit être persistant et accessible en tant que service, il est recommandé d’utiliser torch.multiprocessing pour écrire votre propre processeur d’inférence. Cela permet d’éliminer le surcoût lié au lancement du processus ainsi qu’au chargement et à la recompilation du modèle.

L'extrait de code ci-dessous combine First Block Cache, quantification dynamique fp8, torch.compile et Context Parallelism pour la vitesse d'inférence la plus rapide.

import time
import torch
import torch.distributed as dist
from diffusers import FluxPipeline

dist.init_process_group()

torch.cuda.set_device(dist.get_rank())

pipe = FluxPipeline.from_pretrained(
    "black-forest-labs/FLUX.1-dev",
    dtype=torch.bfloat16,
).to("cuda")  # or "mps", "xpu", "cpu"

from para_attn.context_parallel import init_context_parallel_mesh
from para_attn.context_parallel.diffusers_adapters import parallelize_pipe
from para_attn.parallel_vae.diffusers_adapters import parallelize_vae

mesh = init_context_parallel_mesh(
    pipe.device.type,
    max_ring_dim_size=2,
)
parallelize_pipe(
    pipe,
    mesh=mesh,
)
parallelize_vae(pipe.vae, mesh=mesh._flatten())

from para_attn.first_block_cache.diffusers_adapters import apply_cache_on_pipe

apply_cache_on_pipe(
    pipe,
    residual_diff_threshold=0.12,  # Use a larger value to make the cache take effect
)

from torchao.quantization import quantize_, float8_dynamic_activation_float8_weight, float8_weight_only

quantize_(pipe.text_encoder, float8_weight_only())
quantize_(pipe.transformer, float8_dynamic_activation_float8_weight())
torch._inductor.config.reorder_for_compute_comm_overlap = True
pipe.transformer = torch.compile(
   pipe.transformer, mode="max-autotune-no-cudagraphs",
)

# Enable memory savings
# pipe.enable_model_cpu_offload(gpu_id=dist.get_rank())
# pipe.enable_sequential_cpu_offload(gpu_id=dist.get_rank())

for i in range(2):
    begin = time.time()
    image = pipe(
        "A cat holding a sign that says hello world",
        num_inference_steps=28,
        output_type="pil" if dist.get_rank() == 0 else "pt",
    ).images[0]
    end = time.time()
    if dist.get_rank() == 0:
        if i == 0:
            print(f"Warm up time: {end - begin:.2f}s")
        else:
            print(f"Time: {end - begin:.2f}s")

if dist.get_rank() == 0:
    print("Saving image to flux.png")
    image.save("flux.png")

dist.destroy_process_group()

Enregistrez dans run_flux.py et lancez-le avec torchrun.

# Use --nproc_per_node to specify the number of GPUs
torchrun --nproc_per_node=2 run_flux.py

La vitesse d’inférence est réduite à 8,20 secondes par rapport à la référence, soit 3,21 fois plus rapide, avec 2 GPU NVIDIA L20. Sur 4 L20, la vitesse d’inférence est de 3,90 secondes, soit 6,75 fois plus rapide.

L'extrait de code ci-dessous combine First Block Cache et Context Parallelism pour obtenir la vitesse d'inférence la plus rapide.

import time
import torch
import torch.distributed as dist
from diffusers import HunyuanVideoPipeline, HunyuanVideoTransformer3DModel
from diffusers.utils import export_to_video

dist.init_process_group()

torch.cuda.set_device(dist.get_rank())

model_id = "tencent/HunyuanVideo"
transformer = HunyuanVideoTransformer3DModel.from_pretrained(
    model_id,
    subfolder="transformer",
    dtype=torch.bfloat16,
    revision="refs/pr/18",
)
pipe = HunyuanVideoPipeline.from_pretrained(
    model_id,
    transformer=transformer,
    dtype=torch.float16,
    revision="refs/pr/18",
).to("cuda")  # or "mps", "xpu", "cpu"

from para_attn.context_parallel import init_context_parallel_mesh
from para_attn.context_parallel.diffusers_adapters import parallelize_pipe
from para_attn.parallel_vae.diffusers_adapters import parallelize_vae

mesh = init_context_parallel_mesh(
    pipe.device.type,
)
parallelize_pipe(
    pipe,
    mesh=mesh,
)
parallelize_vae(pipe.vae, mesh=mesh._flatten())

from para_attn.first_block_cache.diffusers_adapters import apply_cache_on_pipe

apply_cache_on_pipe(pipe)

# from torchao.quantization import quantize_, float8_dynamic_activation_float8_weight, float8_weight_only
#
# torch._inductor.config.reorder_for_compute_comm_overlap = True
#
# quantize_(pipe.text_encoder, float8_weight_only())
# quantize_(pipe.transformer, float8_dynamic_activation_float8_weight())
# pipe.transformer = torch.compile(
#    pipe.transformer, mode="max-autotune-no-cudagraphs",
# )

# Enable memory savings
pipe.vae.enable_tiling()
# pipe.enable_model_cpu_offload(gpu_id=dist.get_rank())
# pipe.enable_sequential_cpu_offload(gpu_id=dist.get_rank())

for i in range(2):
    begin = time.time()
    output = pipe(
        prompt="A cat walks on the grass, realistic",
        height=720,
        width=1280,
        num_frames=129,
        num_inference_steps=1 if i == 0 else 30,
        output_type="pil" if dist.get_rank() == 0 else "pt",
    ).frames[0]
    end = time.time()
    if dist.get_rank() == 0:
        if i == 0:
            print(f"Warm up time: {end - begin:.2f}s")
        else:
            print(f"Time: {end - begin:.2f}s")

if dist.get_rank() == 0:
    print("Saving video to hunyuan_video.mp4")
    export_to_video(output, "hunyuan_video.mp4", fps=15)

dist.destroy_process_group()

Enregistrez dans run_hunyuan_video.py et lancez-le avec torchrun.

# Use --nproc_per_node to specify the number of GPUs
torchrun --nproc_per_node=8 run_hunyuan_video.py

La vitesse d'inférence est réduite à 649,23 secondes par rapport à la référence, soit 5,66 fois plus rapide, avec 8 GPU NVIDIA L20.

Benchmarks

Type de GPU Nombre de GPU Optimisations Temps réel (s) Accélération
NVIDIA L20 1 Ligne de base 26.36 1.00x
NVIDIA L20 1 FBCache (rdt=0.08) 17.01 1.55x
NVIDIA L20 1 FP8 DQ 13.40 1.96x
NVIDIA L20 1 FBCache (rdt=0.12) + FP8 DQ 7.56 3.48x
NVIDIA L20 2 FBCache (rdt=0.12) + FP8 DQ + CP 4.92 5.35x
NVIDIA L20 4 FBCache (rdt=0.12) + FP8 DQ + CP 3.90 6.75x
Type de GPU Nombre de GPU Optimisations Temps réel (s) Accélération
NVIDIA L20 1 Ligne de base 3675.71 1.00x
NVIDIA L20 1 FBCache 2271.06 1.62x
NVIDIA L20 2 FBCache + CP 1132.90 3.24x
NVIDIA L20 4 FBCache + CP 718.15 5.12x
NVIDIA L20 8 FBCache + CP 649.23 5.66x

Xet Storage Details

Size:
18.2 kB
·
Xet hash:
1a62a1735bfc28f95350c8c3fdce69301440cf9ec14a8864460c84f6141888a8

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