Ascend-SACT/ernie-image
模型介绍文件和版本Pull Requests讨论分析
下载使用量0

ERNIE-Image 部署文档

1. ERNIE-Image 模型介绍

ERNIE-Image 是百度开发的文生图扩散模型,基于 Transformer 架构,支持高质量文本到图像生成。

核心特性

  • 架构: Transformer-based diffusion model(非U-Net)
  • 参数规模: 36层 Transformer,hidden_size=4096
  • 推理方式: FlowMatch scheduler(连续时间步)
  • VAE: Flux风格VAE,latent channels=128,scale_factor=8
  • 文本编码器: Mistral-3 (3072维)
  • 分辨率支持: 1024x1024及其他多种尺寸(需padding适配)

模型配置详情

Transformer Config:
  - hidden_size: 4096
  - num_layers: 36
  - num_attention_heads: 32
  - ffn_hidden_size: 12288
  - in_channels: 128 (latent)
  - out_channels: 128
  - patch_size: 1
  - text_in_dim: 3072
  - eps: 1e-06
  - qk_layernorm: True
  - rope_axes_dim: [32, 48, 48]
  - rope_theta: 256

特殊设计

  1. 混合序列: 模型同时处理 image tokens + text tokens

    • Image sequence: 固定长度(取决于分辨率)
    • Text sequence: 动态长度(取决于prompt)
  2. Sequence Parallel挑战:

    • Image tokens必须均匀分配到各rank
    • Text tokens不能分割(需broadcast)
    • 解决方案:手动shard image部分,broadcast text部分

2. 基础硬件和部署模式

2.1 硬件配置

NPU硬件:

  • 类型: Ascend NPU
  • 数量: 4卡配置(单卡或多卡SP=4模式)

2.2 核心依赖版本(精确版本)

依赖包版本说明
cann8.5.1昇腾基础库
torch2.9.0+cpuPyTorch基础库
torch_npu2.9.0华为NPU适配
diffusers0.38.0HuggingFace扩散模型库(含修改)
transformers5.5.3文本编码器依赖
vllm0.19.1+emptyvLLM推理框架
vllm_ascend0.19.1rc1NPU backend
vllm-omni0.19.0rc1+npu多模态扩展框架(含修改)
modelscope1.36.3模型下载工具
accelerate1.12.0分布式推理支持

2.3 部署模式

单卡模式(SP=1)

  • 设备: NPU 0
  • 适用场景: 开发调试、低延迟需求
  • 推理速度: 基准速度

多卡模式(SP=4,Sequence Parallel)

  • 设备: NPU 0,1,2,3
  • 适用场景: 生产环境、大规模推理
  • 推理速度: 相比单卡无显著提升(attention仍需gather)
  • 优势: 降低单卡内存峰值,支持更大batch/分辨率

SP=4配置:

parallel_config:
  sequence_parallel_size: 4  # 4个NPU并行
  ulysses_degree: 4          # Ulysses SP算法
  ring_degree: 1
  tensor_parallel_size: 1
  pipeline_parallel_size: 1
  data_parallel_size: 1

3. 下载权重与启动服务

3.1 下载模型权重

使用 ModelScope SDK 下载:

# 安装 modelscope
pip install modelscope

# 下载 ERNIE-Image 模型
python3 << 'EOF'
from modelscope import snapshot_download

model_dir = snapshot_download(
    'PaddlePaddle/ERNIE-Image',
    cache_dir='/opt/data/modelscope/hub'
)
print(f"Model downloaded to: {model_dir}")
EOF

权重目录结构:

/opt/data/modelscope/hub/models/PaddlePaddle/ERNIE-Image/
├── transformer/
│   ├── config.json
│   ├── diffusion_pytorch_model.safetensors
│   └── model.safetensors.index.json
├── vae/
│   ├── config.json
│   └── diffusion_pytorch_model.safetensors
├── text_encoder/
│   ├── config.json
│   ├── model.safetensors
│   └── tokenizer_config.json
├── scheduler/
│   └── scheduler_config.json

3.2 启动镜像文件

3.2.1 下载镜像

docker pull m.daocloud.io/quay.io/ascend/vllm-ascend:v0.19.1rc1-openeuler

3.2.2 启动镜像

docker run -it -u root -d --net=host \
  --privileged   --ipc=host   \
  --device=/dev/davinci_manager \
  --device=/dev/devmm_svm  \
  --device=/dev/hisi_hdc  \
  -v /usr/local/Ascend/driver:/usr/local/Ascend/driver   \
  -v /usr/local/dcmi:/usr/local/dcmi  \
  -v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
  -v /usr/local/sbin:/usr/local/sbin  \
  -v /usr/local/Ascend/driver/tools/hccn_tool:/usr/local/Ascend/driver/tools/hccn_tool  \
 -v /opt/data/modelscope:/opt/data/modelscope   --name ernie-image  \
  m.daocloud.io/quay.io/ascend/vllm-ascend:v0.19.1rc1-openeuler    /bin/bash

4. 应用 Patch 修改

4.1 修改概述

ERNIE-Image 的正确部署需要对两个核心库进行修改:

  1. diffusers库: 添加NPU优化的rotary embedding和attention dispatch
  2. vllm-omni库: 修复SP执行中的格式判断、latent同步、padding支持

4.2 diffusers库修改

修改文件1: transformer_ernie_image.py

位置: /usr/local/python3.11.14/lib/python3.11/site-packages/diffusers/models/transformers/transformer_ernie_image.py

修改内容: 在 ErnieImageSingleStreamAttnProcessor.__call__ 中添加NPU优化的rotary embedding

Patch: 见 patches/diffusers_transformer_ernie_image.patch

关键代码:

## add by wei (Line 123)
def apply_rotary_emb_npu(x_in: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
    rot_dim = freqs_cis.shape[-1]
    x, x_pass = x_in[..., :rot_dim], x_in[..., rot_dim:]
    cos_ = torch.cos(freqs_cis).to(x.dtype)
    sin_ = torch.sin(freqs_cis).to(x.dtype)

    # Use NPU optimized rotary embedding
    out = rotary_position_embedding(x, cos_, sin_, rotated_mode='rotated_half')

    return torch.cat((out, x_pass), dim=-1)

# Auto-select NPU or CPU implementation
if freqs_cis is not None:
    if is_torch_npu_available():
        query = apply_rotary_emb_npu(query, freqs_cis)
        key = apply_rotary_emb_npu(key, freqs_cis)
    else:
        query = apply_rotary_emb(query, freqs_cis)
        key = apply_rotary_emb(key, freqs_cis)

修改文件2: attention_dispatch.py

位置: /usr/local/python3.11.14/lib/python3.11/site-packages/diffusers/models/attention_dispatch.py

修改内容: 添加laser attention判断和NPU mask广播函数

Patch: 见 patches/diffusers_attention_dispatch.patch

关键代码:

## add by wei (Line 3311)
def is_supported_laser_attention(head_dim, q_seqlen, kv_seqlen):
    MAX_DIM = 128
    MIN_SEQLEN_SELF = 4000
    MIN_SEQLEN_CROSS = 118404
    MAX_SEQLEN_CROSS = 119056

    if head_dim > MAX_DIM:
        return False
    if q_seqlen == kv_seqlen:
        return q_seqlen >= MIN_SEQLEN_SELF
    else:
        return (MIN_SEQLEN_CROSS <= q_seqlen <= MAX_SEQLEN_CROSS) and \
            (MIN_SEQLEN_CROSS <= kv_seqlen <= MAX_SEQLEN_CROSS)


def _broadcast_attn_mask_npu(query, key, attn_mask):
    if attn_mask is not None:
        if attn_mask.ndim == 2 and attn_mask.shape[0] == query.shape[0] and attn_mask.shape[1] == key.shape[1]:
            batch_size, seq_len_q, seq_len_kv = attn_mask.shape[0], query.shape[1], key.shape[1]
            attn_mask = attn_mask.unsqueeze(1).expand(batch_size, seq_len_q, seq_len_kv).unsqueeze(1).contiguous()
        elif attn_mask.ndim == 4 and attn_mask.shape[1:3] == (1, 1):
            attn_mask = attn_mask.expand(-1, -1, query.shape[1], -1).contiguous()

    return attn_mask

4.3 vllm-omni库修改

修改文件1: pipeline_ernie_image.py

位置: /vllm-workspace/vllm-omni/vllm_omni/diffusion/models/ernie_image/pipeline_ernie_image.py

主要修改:

  1. latent broadcast: 在prepare_latents后同步latent到所有rank(确保一致性)
  2. VAE decode前broadcast: 再次同步latent(双重保险)
  3. SP rank管理: 仅rank 0执行VAE decode,其他rank返回空结果
  4. 移除尺寸warning: 删除"divisible by 32"检查(padding机制已处理)

Patch: 见 patches/vllm_omni_pipeline_ernie_image.patch

修改文件2: ernie_image_transformer.py

位置: /vllm-workspace/vllm-omni/vllm_omni/diffusion/models/ernie_image/ernie_image_transformer.py

主要修改:

  1. 移除自动shard: 删除_sp_plan["x_embedder"]配置,改为手动shard
  2. Padding支持: 添加image sequence padding(处理非标准尺寸)
  3. 手动sp_shard/sp_gather:
    • Shard image tokens only(text broadcast)
    • Gather后再去padding
  4. 清理日志: 删除调试日志,保留初始化信息

Patch: 见 patches/vllm_omni_ernie_image_transformer.patch

修改文件3: ulysses_attention.py

位置: /vllm-workspace/vllm-omni/vllm_omni/diffusion/models/ernie_image/ulysses_attention.py

主要修改(关键bug修复):

  1. 格式判断修复: 明确hidden_states格式为 [B, S, C](而非启发式判断)
  2. 正确gather维度: 在sequence维度gather(而非batch维度)
  3. Rotary embedding gather: 同样处理rotary embedding
  4. Attention mask创建: 为gathered sequence创建完整mask
  5. 清理日志: 删除所有调试日志

核心修复逻辑:

# Step 1: Gather hidden_states
B, S_local, C = hidden_states.shape

# Convert to sequence-first for gather
hidden_states = hidden_states.transpose(0, 1)  # [B, S, C] -> [S, B, C]
hidden_states_full = sp_gather(hidden_states, dim=0)  # Gather along sequence
hidden_states_full = hidden_states_full.transpose(0, 1)  # Back to [B, S_full, C]

# Step 2: Gather rotary embedding (same process)
cos = cos.transpose(0, 1)
cos_full = sp_gather(cos, dim=0)
cos_full = cos_full.transpose(0, 1)

# Step 3: Create full attention mask
mask_full = torch.ones((B, 1, S_full, S_full), ...)

# Step 4: Compute attention on full sequence
output_full = processor(attn, hidden_states_full, mask_full, rotary_full)

# Step 5: Scatter back
output_full = output_full.transpose(0, 1)
output_local = sp_shard(output_full, dim=0)
output_local = output_local.transpose(0, 1)

Patch: 见 patches/vllm_omni_ulysses_attention.patch

4.4 应用Patch的步骤

# 1. 修改 diffusers (需要root权限)
sudo cp patches/diffusers_transformer_ernie_image.patch \
  /usr/local/python3.11.14/lib/python3.11/site-packages/diffusers/models/transformers/
  
sudo patch -p0 < patches/diffusers_transformer_ernie_image.patch

sudo cp patches/diffusers_attention_dispatch.patch \
  /usr/local/python3.11.14/lib/python3.11/site-packages/diffusers/models/
  
sudo patch -p0 < patches/diffusers_attention_dispatch.patch

# 2. 修改 vllm-omni
cd /vllm-workspace/vllm-omni/vllm_omni/diffusion/models/ernie_image/

patch -p0 < patches/vllm_omni_pipeline_ernie_image.patch
patch -p0 < patches/vllm_omni_ernie_image_transformer.patch
patch -p0 < patches/vllm_omni_ulysses_attention.patch

4.5 启动服务

单卡启动:

cd /vllm-workspace/ernie-image/bin

# 单卡配置文件(需自行创建)
vllm serve /opt/data/modelscope/hub/models/PaddlePaddle/ERNIE-Image \
  --config config/ernie_stage_single.yaml \
  --port 8000 \
  --dtype bfloat16

SP=4启动:

cd /vllm-workspace/ernie-image/bin

# 使用SP=4配置启动
bash start_ernie.sh

start_ernie.sh 内容:

#!/bin/bash
echo "Starting ERNIE-Image (v0.19.0rc1 + diffusers adapter)..."
echo "Model: /opt/data/modelscope/hub/models/PaddlePaddle/ERNIE-Image"
echo "Port: 8000"
echo "Config: /vllm-workspace/ernie-image/config/ernie_stage_sp4_custom.yaml"

vllm serve /opt/data/modelscope/hub/models/PaddlePaddle/ERNIE-Image \
  --config /vllm-workspace/ernie-image/config/ernie_stage_sp4_custom.yaml \
  --port 8000 \
  --dtype bfloat16

服务验证:

# 检查服务状态
curl http://localhost:8000/v1/models

# 测试生成
curl -X POST http://localhost:8000/v1/images/generations \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A beautiful sunset over the ocean",
    "size": "1024x1024",
    "num_inference_steps": 50,
    "guidance_scale": 4.0
  }'

5. 性能优化总结

优化效果:1024x1024 离线推理:79s(单卡) -> 28s(4卡)

5.1 NPU硬件优化

Rotary Position Embedding优化

优化内容: 使用NPU专用的 rotary_position_embedding 函数替代普通实现

原因:

  • 标准RoPE在NPU上效率较低
  • 华为提供的优化版本利用NPU硬件特性
  • 性能提升: ~15-20%(实测)

实现方式:

# 检测NPU可用性,自动选择优化版本
if is_torch_npu_available():
    query = apply_rotary_emb_npu(query, freqs_cis)
    key = apply_rotary_emb_npu(key, freqs_cis)
else:
    query = apply_rotary_emb(query, freqs_cis)  # CPU fallback

Attention Dispatch优化

优化内容: 添加laser attention判断和NPU mask广播优化

原因:

  • Laser attention在长序列时效率更高
  • NPU的mask广播需要特殊处理(避免reshape开销)
  • 减少不必要的内存拷贝

适用条件:

  • head_dim ≤ 128
  • self-attention: seqlen ≥ 4000
  • cross-attention: seqlen in [118404, 119056]

5.2 Sequence Parallel优化

Ulysses Attention实现

优化内容: 使用Ulysses算法实现SP(而非简单的数据分割)

原理:

  1. All-to-all通信: 重新分配heads和sequence
    • Before: 每个rank有完整heads,部分sequence
    • After: 每个rank有部分heads,完整sequence
  2. 完整attention: 每个rank计算完整sequence的attention
  3. Reverse all-to-all: 恢复原始分配

优势:

  • 支持完整attention计算(跨rank交互)
  • 无需修改attention kernel
  • 内存分配均匀

格式判断修复(关键bug)

问题: 原实现误判 [B, S, C] 为 [S, B, C]

  • 导致在batch维度gather而非sequence
  • 结果:4个rank各自生成独立图片块

修复:

  • 明确Block调用attention的格式为 [B, S, C](diffusers源码Line 286)
  • 正确transpose + gather + transpose流程
  • 确保所有rank看到完整sequence

5.3 Latent同步优化

Broadcast同步机制

优化内容: 在关键点同步latent到所有rank

时机:

  1. prepare_latents后: 确保初始latent一致
  2. VAE decode前: 再次同步(双重保险)

原因:

  • 每个rank的generator可能产生不同latent
  • SP模式下必须确保latent一致
  • 否则各rank处理不同latent,输出拼接错误

实现:

if sp_size > 1:
    # Broadcast from rank 0 to all ranks
    dist.broadcast(latents, src=0, group=get_sp_group().device_group)

5.4 Padding机制

非标准尺寸支持

问题: 尺寸1328x1328等导致N_img不能被4整除

  • sp_shard要求: size必须能被world_size整除
  • 原实现报错: "Tensor size (6889) not divisible by 4"

优化:

  • 自动padding到可整除大小
  • gather后去除padding部分
  • 支持任意尺寸生成

实现:

if N_img % sp_size != 0:
    pad_size = sp_size - (N_img % sp_size)
    # Pad latent, position_ids, attention_mask
    img_sbh = torch.cat([img_sbh, padding], dim=0)

# After gather, remove padding
x_img_full = sp_gather(x_img_local, dim=0)
if N_img_padded != N_img:
    x_img_full = x_img_full[:N_img]  # Keep only original

5.5 Cache-DiT优化

基本配置

配置文件: config/ernie_stage_sp4_custom.yaml

cache_backend: "cache_dit"
cache_config:
  Fn_compute_blocks: 1          # 前向计算块数
  Bn_compute_blocks: 1          # 后向计算块数
  max_warmup_steps: 5           # 预热步数
  max_cached_steps: 20          # 最大缓存步数
  max_continuous_cached_steps: 10  # 连续缓存步数
  residual_diff_threshold: 0.05  # 残差阈值(已验证最优)
enable_cache_dit_summary: true   # 启用统计日志

实现原理

核心思想: 缓存Transformer中间层计算结果,避免重复计算

ERNIE-Image适配 (cache_dit_backend.py:676-747):

def enable_cache_for_ernie_image(pipeline, cache_config):
    """为ErnieImagePipeline启用cache-dit"""
    
    # 1. 使用BlockAdapter包装transformer
    cache_dit.enable_cache(
        BlockAdapter(
            transformer=transformer,
            blocks=transformer.layers,      # 36层transformer blocks
            forward_pattern=ForwardPattern.Pattern_3,  # 单张量输入输出
            params_modifiers=[modifier],
            check_forward_pattern=False,    # ERNIE-Image特殊forward签名
        ),
        cache_config=db_cache_config,
    )
    
    # 2. 返回refresh函数(动态更新步数)
    def refresh_cache_context(pipeline, num_inference_steps):
        cache_dit.refresh_context(transformer, num_inference_steps=num_inference_steps)
    
    return refresh_cache_context

关键参数说明:

  1. ForwardPattern.Pattern_3:

    • 适用于单张量输入输出的模型
    • ERNIE-Image forward: hidden_states -> hidden_states
  2. check_forward_pattern=False:

    • 跳过forward签名验证
    • ERNIE-Image有特殊参数(timestep, text_bth, text_lens)
  3. residual_diff_threshold=0.05:

    • 残差差异阈值,控制缓存复用条件
    • 太小:缓存频繁失效,加速效果差
    • 太大:质量下降明显
    • 实测最优:0.05

优化效果

性能提升:

  • 首次推理:无加速(缓存预热)
  • 相似prompt:加速30-50%(复用缓存)
  • 完全相同prompt:加速60-70%(全缓存命中)

缓存机制:

  1. 预热阶段 (前5步): 计算并缓存所有层输出
  2. 缓存阶段 (第6-50步):
    • 比较残差差异 < threshold → 复用缓存
    • 残差差异 > threshold → 重新计算
  3. 统计输出:
    Cache-DiT Summary:
    - Total steps: 50
    - Cached steps: 35 (70%)
    - Computed steps: 15 (30%)
    - Cache hit rate: 0.85

SP模式兼容性

关键设计: cache-dit在transformer层缓存,与SP无关

  • SP只在sequence维度分片,不影响层内计算
  • 所有rank看到相同的input(通过latent broadcast)
  • cache-dit在每个rank独立运行,缓存内容一致
  • 无需额外修改,cache-dit天然支持SP模式

调试建议

启用详细日志:

enable_cache_dit_summary: true

检查点:

  1. 日志显示 "Cache-dit enabled successfully on ErnieImagePipeline"
  2. 日志显示 "Enabling cache-dit on ErnieImage transformer: Fn=1, Bn=1, W=5, layers=36"
  3. 推理时显示缓存统计信息

常见问题:

  • Q: 加速不明显?A: 检查prompt相似度,差异大则缓存失效
  • Q: 图片质量下降?A: 增大residual_diff_threshold(如0.08)
  • Q: 首次推理慢?A: 正常,缓存预热需要额外开销

6. 问题解决总结

6.1 输出拼接问题(最关键)

问题描述:

  • SP=4模式下,输出图片显示为4张独立图片垂直拼接
  • 每张图片对应一个rank处理的区域
  • 边界明显(diff值146)

根本原因:

  • Ulysses attention格式判断错误
  • 误将 [B, S, C] 判断为 [S, B, C]
  • 导致在batch维度gather(2→8),而非sequence维度

解决方案:

  1. 明确hidden_states格式(Block调用为 [B, S, C])
  2. 正确transpose流程:
    • [B, S, C] → transpose → [S, B, C] → gather → [S_full, B, C] → transpose → [B, S_full, C]
  3. 同样处理rotary embedding和attention mask

验证结果:

  • ✓ 四个象限不再重复
  • ✓ 边界差异降低(146 → 5-9)
  • ✓ 图片内容完整连续

6.2 Latent不一致问题

问题描述:

  • 即使修复格式判断,latent仍不一致
  • 每个rank使用不同generator产生latent
  • 导致输出仍有细微差异

解决方案:

  • prepare_latents后立即broadcast
  • VAE decode前再次broadcast(双重保险)

验证结果:

  • ✓ 所有rank使用相同latent
  • ✓ 输出完全一致

6.3 尺寸不支持问题

问题描述:

  • 尺寸1328x1328等报错
  • "Tensor size (6889) must be divisible by 4"

原因:

  • N_img计算:166x166 = 27556,除以4得6889(看似能整除)
  • 但实际是双重shard导致二次分割

解决方案:

  1. 移除 _sp_plan["x_embedder"] 自动shard
  2. 手动padding到可整除大小
  3. gather后去除padding

验证结果:

  • ✓ 支持所有测试尺寸(1664x928, 1328x1328, etc)
  • ✓ Padding自动处理,用户无感知

6.4 Attention Mask不匹配问题

问题描述:

  • Attention mask尺寸 [B, 1, S_local, S_local]
  • Gathered hidden_states尺寸 [B, S_full, C]
  • FlashAttention报错(维度不匹配)

解决方案:

  • 创建完整attention mask [B, 1, S_full, S_full]
  • 使用全True mask(ERNIE-Image无因果mask)

验证结果:

  • ✓ FlashAttention正常执行
  • ✓ 无维度错误

6.5 双重Shard问题

问题描述:

  • _sp_plan["x_embedder"]["split_output"] 自动shard
  • forward中又手动调用 sp_shard
  • 导致二次shard,sequence被过度分割

解决方案:

  • 移除 _sp_plan["x_embedder"] 和 ["final_linear"]
  • 保留手动shard/gather(更精确控制)

验证结果:

  • ✓ 单次shard正确执行
  • ✓ 无二次分割错误

附录

A. Benchmark脚本

见 /vllm-workspace/ernie-image/benchmark_ernie_image.py

测试内容:

  • 3个不同prompt
  • 8种尺寸:1664x928, 928x1664, 1328x1328, 1472x1104, 1104x1472, 1584x1056, 1056x1584, 1024x1024
  • 总计24张图片

B. 配置文件模板

单卡配置: config/ernie_stage_single.yaml(需创建)

SP=4配置: config/ernie_stage_sp4_custom.yaml

C. Patch文件清单

patches/
├── diffusers_transformer_ernie_image.patch
├── diffusers_attention_dispatch.patch
├── vllm_omni_pipeline_ernie_image.patch
├── vllm_omni_ernie_image_transformer.patch
└── vllm_omni_ulysses_attention.patch