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

解决方案1、Cache-DiT 推理加速

Cache-DiT 技术简介

Cache-DiT 是 vLLM-Omni 针对 DiT(Diffusion Transformer)模型的多步推理加速技术。DiT 模型在图像生成时需要迭代执行多步(如 50 步),每一步都跑一遍完整的 transformer forward。Cache-DiT 的核心思路是:相邻步之间的中间层输出变化很小,可以缓存上一步的结果,跳过部分层的计算,直接复用缓存的残差。

框架代码修改

1、修改ErnieImageTransformer.py

# 旧
class ErnieImageTransformer2DModel(nn.Module):

# 新
class ErnieImageTransformer2DModel(CachedTransformer):

通过修改ErnieImageTransformer,使用cache-dit特性来进行模型的性能验证。

继承 CachedTransformer的效果
  1. Cache-DiT 兼容性标记 — CachedTransformer 是 vLLM-Omni 对 DiT 模型的"标记基类"。cache_backend.enable(pipeline) 为 ERNIE-Image 调用专属 enable_cache_for_ernie_image,使用 Pattern_3 对 transformer.layers 做缓存适配。

  2. CFG(Classifier-Free Guidance) 分离支持 — do_true_cfg 属性和 enable_separate_cfg 类属性用于 CFG 并行推理时区分正/负条件分支的缓存状态,避免残差互相污染。

修改后的预估计算量差异如下:

无 Cache-DiT(50 步全部计算):
  Step 0: 全部 N 层计算 → hidden_0
  Step 1: 全部 N 层计算 → hidden_1
  ...
  Step 49: 全部 N 层计算 → hidden_49
  总计算量: 50 × N 层

有 Cache-DiT(缓存部分步的部分层):
  Step 0-3: 全部 N 层计算               ← warmup,始终全算
  Step 4-6: 残差差值 < threshold → 缓存 ← 只算前几层,其余复用
  Step 7:   连续缓存达上限,强制全算    ← max_continuous_cached_steps
  ...
  总计算量: 大幅减少(取决于缓存比例)

影响cache-dit的核心参数 参数解释如下

参数默认值作用
max_warmup_steps4前 4 步始终全算,不缓存(建立缓存基线)
residual_diff_threshold0.24残差差值低于此阈值 → 缓存;高于 → 全算
max_continuous_cached_steps3最大连续缓存步数,防止精度累积退化
max_cached_steps30最大总缓存步数(-1 = 不限)
Fn_compute_blocks1每个 cache cycle 的前向计算块数
NPU 单卡性能对比

测试配置:

参数值
模型ERNIE-Image
图像尺寸1328 × 928
推理步数50
SP size1(单卡)
平台华为昇腾 NPU(单卡)
guidance_scale4.0

时延对比:

配置总推理时延 (s)加速比Cache-DiT 参数备注
Cache-DiT OFF (--cache-backend none)69.98——全量计算基线
Cache-DiT ON (原始配置, threshold=0.05)69.171.1%F1B1_W5_M20_MC10_R0.05threshold 过严,几乎无缓存
Cache-DiT ON (配置 A, threshold=0.24)47.032.8%F1B0_W4_M30_MC10_R0.24threshold 放宽 + Bn=0,效果显著

配置 的关键调整:

参数原始值配置值影响
residual_diff_threshold0.050.24最关键:0.05 太严(仅 5% 差值以下才缓存),0.24 放宽到代码默认值,大幅提高缓存命中率
Bn_compute_blocks10移除 backward compute block,减少缓存周期开销
max_warmup_steps54多一步可缓存
max_cached_steps2030放宽总缓存上限

解决方案2、NPU SP 多卡并行适配

vLLM‑Omni 配置了sp_plan,这个是一套规划器 + 配置接口,用来自动 / 手动配置 “序列并行怎么切、怎么调度、怎么通信” 。但是由于NPU和GPU的通信方式等差异,不能直接复用 GPU 的 _sp_plan hook 方案。以下按数据流阶段解析每处修改。

层级原因详细说明
工程层_sp_plan hook 不可用SequenceParallelSplitHook + auto_pad 依赖 NCCL 通信行为。HCCL 上 auto_pad 与 all_to_all_single 有兼容性问题,hook 系统不可用
Padding 层Padding 时机矛盾GPU 在 freqs 输出 上补零(RoPE 计算完之后);NPU 必须在 grid_yx 输入坐标 上补零(RoPE 计算之前),因为 gather 后 freqs shape 必须与全局序列对齐。
健壮性层HCCL 排序不确定NCCL 保证 gather/scatter 排序确定性;HCCL 排序不确定,local RoPE 路径依赖排序对称性,global RoPE 消除排序依赖
初始化阶段:禁用 GPU hook + 安装 NPU processor

代码变化:

self.sp_size = _get_sequence_parallel_world_size_or_one()

if current_omni_platform.is_npu():
    self._sp_plan = {}                          # 清空自动分片计划
    if self.sp_size > 1:
        for layer in self.layers:
            attn = layer.self_attention
            attn.attn.skip_sequence_parallel = True   # 跳过 vLLM 内置 SP
            ulysses_processor = ErnieImageUlyssesAttnProcessorV2()
            attn.set_processor(ulysses_processor)     # 安装自定义处理器

三个关键操作:

  1. _sp_plan = {} — 禁用 _sp_plan hook 系统(三层原因见概述),转为手动控制分片时机

  2. skip_sequence_parallel = True — 阻止 vLLM Attention 内部做 AllToAll。Ulysses processor 已在外部做 AllToAll,如果 Attention 内部再做一次,就重复通信了

  3. 安装 Ulysses Processor — 每个 attention 层安装 ErnieImageUlyssesAttnProcessorV2,统一处理 AllToAll → RoPE → Attention → AllToAll 的完整流程

Pipeline 初始化也需要调整:

if current_omni_platform.is_npu():
    # 1. 直接读 config.json(需要传入 parallel_config + od_config)
    transformer_config_path = Path(model) / "transformer" / "config.json"
    tf_config_dict = json.load(transformer_config_path)
    self.transformer = ErnieImageTransformer2DModel(
        parallel_config=self.parallel_config,
        od_config=od_config,
        **tf_config_dict,
    ).to(self._execution_device)

    # 2. __init__ 中直接加载权重(DO NOT remove unless verified with benchmark)
    #    实测:__init__ 加载 ~27s,延迟到 load_weights() ~34s(慢 21%)
    transformer_path = Path(model) / "transformer"
    for weight_file in transformer_path.glob("*.safetensors"):
        ...
    self.transformer.load_weights(weights_dict.items())

NPU 需要传入 parallel_config(初始化 sp_size)和 od_config(完整并行配置),GPU 的 get_transformer_config_kwargs 不支持这两个参数。权重提前加载是因为 NPU 上 load_weights() 有 21% 的额外开销。

输入准备阶段:_prepare_npu vs unified_prepare

原版 forward() 是一个统一的流程(unified_prepare),现在拆为两条路径:

if current_omni_platform.is_npu():
    x, S_local, ... = self._prepare_npu(hidden_states, text_bth, text_lens)
else:
    x, S_local, ... = self._prepare_gpu(hidden_states, text_bth, text_lens, N_img)

核心差异:padding 的处理时机

场景GPU (_sp_plan)NPU (_prepare_npu)
运行范围在 全序列 上计算(不分片)在 本地 shard 上计算(手动分片后)
Padding 时机auto_pad 在 RoPE 输出上补零手动在 grid_yx 输入坐标上补零
Padding 后 RoPE先算 RoPE 再 shard(split_output=True)先 padding grid_yx → 再算 RoPE → 再 shard
Mask2D 本地 [B, seq_local]4D 全局 [B, 1, S_global, S_global]

为什么 NPU 必须先 padding 再算 RoPE:

当 N_img % sp_size != 0 时,需要在 image grid 上补零行。这些零行对应的 RoPE 位置坐标(grid_yx)也必须补零,否则 RoPE 给 padding token 分配真实位置编号,导致注意力出错。

# GPU: pad freqs output (unified_prepare)
freqs_cos = torch.cat([freqs_cos, torch.zeros(...)], dim=...)  # ← RoPE 之后补零

# NPU: pad grid_yx input (_prepare_npu)
pad_grid = torch.zeros((pad_size, 2))  # ← RoPE 之前补零坐标 (0,0)
grid_yx_padded = torch.cat([grid_yx, pad_grid], dim=0)

GPU 的 _sp_plan 可以在 RoPE 输出上补零(因为 hook 在 shard 前就完成了 RoPE,padding 零值不影响位置编码)。NPU 没有 hook,必须在输入坐标上 padding,因为 gather 后 freqs shape 必须与全局序列对齐——在 freqs 输出上补零会导致 gather 后全局 freqs 的零行与 padding token 实际位置不匹配。

注意力计算阶段:Ulysses Processor

NPU 的注意力计算通过自定义 ErnieImageUlyssesAttnProcessorV2 完成,是 SP 适配的核心。

Processor 机制

# ErnieImageAttention 新增
self.processor = None

def set_processor(self, processor):
    self.processor = processor

def forward(self, ...):
    if self.processor is not None:
        return self.processor(self, hidden_states, attention_mask, image_rotary_emb, **kwargs)
    # Default GPU attention logic (原有代码不变)
    ...

借鉴 diffusers 的 AttentionProcessor 设计模式——通过外置处理器替换注意力逻辑,模型本体保持不变。运行时只需一行判断,零开销。

Ulysses Processor 完整流程

输入: hidden_states [B, S_local, C](本地序列片段)

Step 1: to_q/k/v 投影
  → query/key/value: [B, S_local, H, D]

Step 2: AllToAll gather (scatter heads, gather sequence)
  SeqAllToAll4D(scatter_idx=2, gather_idx=1)
  → [B, S_global, H/P, D](全局序列,部分 heads)

Step 3: 全局 RoPE
  freqs 如是本地片段 → sp_gather 拼成全局
  → 正确的全局位置编码

Step 4: 全局 Attention
  mask: 从 4D mask 中提取全局掩码
  → attn(query, key, value, metadata)

Step 5: AllToAll scatter (gather heads, scatter sequence)
  SeqAllToAll4D(scatter_idx=1, gather_idx=2)
  → [B, S_local, H, D](回到本地序列,全部 heads)

Step 6: to_out 输出投影
  → [B, S_local, C]

_apply_rotary_emb 格式适配

# 旧:只支持 [B, S, H, D]
cos_ = freqs_cos.unsqueeze(2).to(x.dtype)

# 新:同时支持 [S, B, H, D](NPU AllToAll 后可能出现的格式)
if x.dim() == 4 and x.shape[0] != freqs_cos.shape[0]:
    cos_ = freqs_cos.transpose(0, 1).unsqueeze(2).to(x.dtype)  # freqs [B,S,D] → [S,B,1,D]
else:
    cos_ = freqs_cos.unsqueeze(2).to(x.dtype)                    # freqs [B,S,D] → [B,S,1,D]

NPU 的 AllToAll 操作后,Q/K/V tensor 可能变为 [S, B, H, D](序列维度在最前面),而 freqs 始终是 [B, S, D](batch-first)。不做维度对齐会导致 freqs 的 batch 维与 x 的 sequence 维对乘——位置编码错乱。

三种场景使得 [S, B, H, D] 格式出现:

场景说明
HCCL 数据排序差异HCCL 的 all_to_all_single 内部数据打包策略可能使 [S_global, B, H/P, D] 成为更自然的输出格式,强行 transpose 回 batch-first 可能因 NPU 内存布局产生性能开销
NPU 注意力后端格式要求MindIE-SD 的 attention_forward 可能期望 sequence-first 格式,避免多余 transpose
freqs 维度对齐x 是 [S, B, H, D] 时,freqs 必须 transpose(0,1) 变为 [S, B, D] 再 unsqueeze(2) 才能与 x 的 dim 排列一致
为什么 NPU 必须先 AllToAll 再 RoPE(不能像 GPU 先 RoPE 再 AllToAll)

GPU 的"本地 RoPE"实际是"全局计算后 _sp_plan hook 自动分片"——freqs 在分片前已包含绝对位置,分片后自然正确。NPU 不能复用,原因有三层:

第一层:_sp_plan hook 不可用 — auto_pad 与 HCCL 不兼容,没有"全局计算后自动分片"机制。

第二层:Padding 时机矛盾 — GPU pad freqs 输出,NPU 必须 pad grid_yx 输入。没有 hook 就不能"先算 RoPE 再 shard",必须先 shard 再 gather 再算 RoPE。

第三层:HCCL 排序不确定 — NCCL 保证 gather/scatter 排序对称性。HCCL 不保证,local RoPE 路径依赖排序对称性(scatter 期望按 rank 顺序拆分回各 rank)。global RoPE 通过 sp_gather freqs 与实际 token 一一对应,消除排序依赖。

输出收集阶段:_gather_image_npu + VAE rank 0 独占解码

def _vae_decode(self, latents, device, output_type="pil"):
    if current_omni_platform.is_npu():
        sp_size = getattr(self.parallel_config, "sequence_parallel_size", None) or 1
        if sp_size > 1:
            sp_rank = get_sequence_parallel_rank()
            dist.broadcast(latents, src=0, group=get_sp_group().device_group)
            if sp_rank != 0:
                return DiffusionOutput(output=[], peak_memory_mb=0.0)
    # 原有的 BN 校正 + unpatchify + VAE decode
    ...

为什么 NPU 需要 rank 0 独占解码:

  • GPU 的 _sp_plan 自动 gather 完整输出,所有 rank 的 latents 一致,冗余解码不影响正确性
  • NPU 的 _gather_image_npu() gather 后,各 rank latents 可能因 padding 截断时机不同有微小差异
  • NPU 的 VAE 解码是显存密集型操作,多卡同时解码浪费 NPU HBM

策略:dist.broadcast 确保 rank 0 的 latents 广播给所有 rank → 只有 rank 0 做 VAE 解码。

GPU vs NPU AllToAll Gather 结构差异

维度GPU (NCCL)NPU (HCCL)
通信库NCCLHCCL
硬件互联NVLink / PCIeHCCS / PCIe
调用时机vLLM Attention 内部自动Ulysses processor 手动
RoPE 时机AllToAll 之前(全局计算后 shard)AllToAll 之后(先 gather 再算)
数据流全局 RoPE → shard → AllToAll → Attention → AllToAll → gatherpad+shard → AllToAll → 全局 RoPE → Attention → AllToAll → 截断
Paddingfreqs 输出上补零grid_yx 输入坐标上补零
Mask2D [B, seq_local]4D [B, 1, S_global, S_global]
Token 排序NCCL 确定性 rank 0→1→2→...HCCL 不确定,global RoPE 消除依赖
内存布局CUDA 行优先Ascend 5ND + 对齐约束
VAE所有 rank 冗余解码rank 0 独占
权重加载延迟到 load_weights()__init__ 中提前加载(+21%)

SP 性能对比

测试配置:

参数值
模型ERNIE-Image
图像尺寸1328 × 928
推理步数50
guidance_scale4.0
平台华为昇腾 NPU

时延对比:

SP size总推理时延 (s)加速比备注
SP=1(单卡)69.981.00x基线
SP=236.321.93x近线性加速
SP=427.322.56x超线性衰减,通信开销占比增大

解决效果/价值

通过使能cache-dit,增加到4卡进行并发推理,实现性能2.56倍提升,满足客户需求。

附录

详细修改见:vllm-omni:pr3415