Diffusers documentation

ParaAttention

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

ParaAttention

Modelos grandes de geração de imagem e vídeo, como FLUX.1-dev e HunyuanVideo, podem representar um desafio de inferência para aplicações em tempo real e implantação devido ao seu tamanho.

ParaAttention é uma biblioteca que implementa paralelismo de contexto e cache do primeiro bloco e pode ser combinada com outras técnicas (torch.compile, quantização dinâmica fp8) para acelerar a inferência.

Este guia mostrará como aplicar o ParaAttention ao FLUX.1-dev e ao HunyuanVideo em GPUs NVIDIA L20. Nenhuma otimização é aplicada ao nosso benchmark de referência, exceto para o HunyuanVideo, a fim de evitar erros de memória insuficiente.

Nosso benchmark de referência mostra que o FLUX.1-dev é capaz de gerar uma imagem com resolução de 1024x1024 em 28 passos em 26,36 segundos, e o HunyuanVideo é capaz de gerar 129 quadros em resolução 720p em 30 passos em 3675,71 segundos.

Para inferência ainda mais rápida com paralelismo de contexto, tente usar GPUs NVIDIA A100 ou H100 (se disponíveis) com suporte a NVLink, especialmente quando há um grande número de GPUs.

Cache do Primeiro Bloco

O cache da saída dos blocos do transformers no modelo e a reutilização deles nas etapas de inferência subsequentes reduzem o custo de computação e tornam a inferência mais rápida.

No entanto, é difícil decidir quando reutilizar o cache para garantir a qualidade das imagens ou vídeos gerados. O ParaAttention usa diretamente a diferença residual da saída do primeiro bloco do transformer para aproximar a diferença entre as saídas do modelo. Quando a diferença é pequena o suficiente, a diferença residual das etapas de inferência anteriores é reutilizada. Em outras palavras, a etapa de desruido é ignorada.

Isso alcança uma aceleração de 2x na inferência do FLUX.1-dev e do HunyuanVideo, com muito boa qualidade.

Cache in Diffusion Transformer
Como o AdaCache funciona, o First Block Cache é uma variante dele
FLUX-1.dev
HunyuanVideo

Para aplicar o cache do primeiro bloco no FLUX.1-dev, chame apply_cache_on_pipe conforme mostrado abaixo. 0,08 é o valor padrão de diferença residual para os modelos 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")
OtimizaçõesOriginalFBCache rdt=0.06FBCache rdt=0.08FBCache rdt=0.10FBCache rdt=0.12
Pré-visualizaçãoOriginalFBCache rdt=0.06FBCache rdt=0.08FBCache rdt=0.10FBCache rdt=0.12
Tempo Real (s)26.3621.8317.0116.0013.78

O First Block Cache reduziu a velocidade de inferência para 17,01 segundos em comparação com a linha de base, ou seja, 1,55x mais rápido, mantendo uma perda de qualidade quase nula.

quantização fp8

O fp8 com quantização dinâmica acelera ainda mais a inferência e reduz o uso de memória. Tanto as ativações quanto os pesos devem ser quantizados para usar os NVIDIA Tensor Cores de 8 bits.

Use float8_weight_only e float8_dynamic_activation_float8_weight para quantizar o codificador de texto e o modelo transformer.

O método de quantização padrão é a quantização por tensor, mas se sua GPU suportar quantização por linha, você também pode experimentá-la para obter melhor precisão.

Instale o torchao com o comando abaixo.

pip3 install -U torch torchao

torch.compile com mode="max-autotune-no-cudagraphs" ou mode="max-autotune" seleciona o melhor kernel para desempenho. A compilação pode levar muito tempo se for a primeira vez que o modelo é chamado, mas vale a pena uma vez que o modelo tenha sido compilado.

Este exemplo apenas quantiza o modelo transformer, mas você também pode quantizar o codificador de texto para reduzir ainda mais o uso de memória.

A quantização dinâmica pode alterar significativamente a distribuição da saída do modelo, portanto, você precisa alterar o residual_diff_threshold para um valor maior para que ela tenha efeito.

FLUX-1.dev
HunyuanVideo
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")

A quantização dinâmica fp8 e o torch.compile reduziram o tempo de inferência para 7,56 segundos em comparação com a linha de base, ou seja, 3,48x mais rápido.

Paralelismo de Contexto

O Context Parallelism paraleliza a inferência e escala com múltiplas GPUs. O design composicional do ParaAttention permite combinar o Context Parallelism com o First Block Cache e a quantização dinâmica.

Consulte o repositório ParaAttention para obter instruções detalhadas e exemplos de como escalar a inferência com múltiplas GPUs.

Se o processo de inferência precisar ser persistente e acionável, é sugerido usar torch.multiprocessing para escrever seu próprio processador de inferência. Isso pode eliminar o overhead de iniciar o processo e de carregar e recompilar o modelo.

FLUX-1.dev
HunyuanVideo

O exemplo de código abaixo combina First Block Cache, quantização dinâmica fp8, torch.compile e Context Parallelism para a velocidade de inferência mais rápida.

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()

Salve em run_flux.py e execute com torchrun.

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

A velocidade de inferência é reduzida para 8,20 segundos em comparação com a linha de base, ou seja, 3,21x mais rápida, com 2 GPUs NVIDIA L20. Em 4 L20s, a velocidade de inferência é de 3,90 segundos, ou seja, 6,75x mais rápida.

Benchmarks

FLUX-1.dev
HunyuanVideo
Tipo de GPUNúmero de GPUsOtimizaçõesTempo Real (s)Aceleração
NVIDIA L201Linha de base26.361.00x
NVIDIA L201FBCache (rdt=0.08)17.011.55x
NVIDIA L201FP8 DQ13.401.96x
NVIDIA L201FBCache (rdt=0.12) + FP8 DQ7.563.48x
NVIDIA L202FBCache (rdt=0.12) + FP8 DQ + CP4.925.35x
NVIDIA L204FBCache (rdt=0.12) + FP8 DQ + CP3.906.75x
Update on GitHub