Prhub

#30241 [diffusion] Fix ragged-caption dynamic-batching accuracy bug in Ernie-Image

原始 PR 作者 mickqian 合并时间 2026-07-07 08:41 文件变更 3 提交数 3 评论 3 代码增减 +174 / -9

执行摘要

修复 Ernie-Image 动态批处理变长标题精度错误

PR #29742修复了Z-Image中类似bug,review询问该bug是否Z-Image特有。分析发现Ernie-Image和Wan T2V存在相同漏洞:动态批处理合并不同长度请求后,DiT未屏蔽填充token导致生成质量下降。本PR修复Ernie-Image,并计划修复Wan(后因回归恢复)。

值得精读。PR展示了如何系统性地修复动态批处理中变长输入的精度问题,包括正确的mask传递模式(从postprocess到cond kwargs再到attention layer)。同时,作者通过逐步提交和revert管理风险的做法值得借鉴。重点关注_prepare_encoder_hidden_states_mask的设计(均匀长度返回None)以及build_varlen_mask_meta在DiT中的使用。

讨论亮点

代码审查机器人gemini-code-assist[bot]提出了两个关键问题:

  • WanI2VCrossAttention context_lens形状不匹配(严重):context_lens形状为[B, 512]代表文本部分,但代码中错误地切片[:, 257:]导致形状不匹配。建议直接使用整个掩码。
  • t5_postprocess_text设备不匹配风险(中等):seq_lens可能不在positions所在设备,建议显式.to(positions.device)
    作者后来恢复Wan部分变更,这两个问题随Wan代码撤回而消失,Ernie-Image部分保持正确。

实现拆解

  1. 修改tokenizer padding配置:在ernie_image.pytext_encoder_extra_args中将paddingFalse改为"longest",使不同长度的请求可以合并为单个张量。
  2. 重写postprocess_texternie_image_postprocess_text现在通过_text_inputs.attention_mask提取每个请求的真实(未填充)token跨度,并调用pad_text_embeddings_with_mask返回TextConditioningOutput,其中包含真实的prompt_seq_lensprompt_embeds_mask
  3. 新增掩码构建方法:在ErnieImagePipelineConfig中新增_prepare_encoder_hidden_states_mask,当批处理中请求长度不一致时生成[B, padded_len]的bool掩码,长度一致时返回None(零开销)。
  4. 修改_prepare_cond_kwargs:使用require_text_seq_lens获取真实长度,并调用_prepare_encoder_hidden_states_mask将掩码放入cond_kwargs
  5. 修改DiT模型:在ernie_image.pyErnieImageSelfAttention.forwardErnieImageBlock.forwardErnieImageDiT.forward中添加attn_maskattn_mask_meta参数。DiT forward中构建联合[image, text]掩码并调用build_varlen_mask_meta,然后沿调用链传递到USPAttention
  6. 新增单元测试test_ernie_image_pipeline_config.py包含两个测试类,分别验证postprocess_text正确提取真实长度和_prepare_cond_kwargs构建正确掩码(均匀长度返回None,变长返回边界掩码)。
文件 模块 状态 重要度
python/sglang/multimodal_gen/configs/pipeline_configs/ernie_image.py 管道配置 modified 7.74
python/sglang/multimodal_gen/test/unit/test_ernie_image_pipeline_config.py 配置测试 added 7.39
python/sglang/multimodal_gen/runtime/models/dits/ernie_image.py 模型定义 modified 6.96

关键符号

ernie_image_postprocess_text _prepare_encoder_hidden_states_mask _prepare_cond_kwargs ErnieImageSelfAttention.forward ErnieImageBlock.forward ErnieImageDiT.forward

关键源码片段

python/sglang/multimodal_gen/configs/pipeline_configs/ernie_image.py core-logic

核心配置修改:tokenizer padding、postprocess_text 重构、新增掩码构建逻辑、修改 cond_kwargs 准备。

def ernie_image_postprocess_text(outputs, _text_inputs, hidden_layer_index=-2):
    """Return Ernie-Image text embeddings, re-padded from real token spans.
    Batched requests can have different real caption lengths after
    tokenization; extract each request's real (unpadded) span via the
    tokenizer's attention mask and re-pad, so TextConditioningOutput carries
    the true per-request lengths instead of the tokenizer's padded length.
    """
    hidden_states = outputs.hidden_states[hidden_layer_index]
    prompt_mask = _text_inputs.attention_mask.to(hidden_states.device).bool()
    split_hidden_states = [
        hidden_states[idx][prompt_mask[idx]] for idx in range(hidden_states.shape[0])
    ]
    # pad_text_embeddings_with_mask 返回 TextConditioningOutput,包含
    # prompt_embeds ( 重新填充后的 [B, P, D])、prompt_seq_lens ( 真实长度列表 )
    # 和 prompt_embeds_mask (bool 掩码 )
    return pad_text_embeddings_with_mask(split_hidden_states)
​
​
def _prepare_encoder_hidden_states_mask(
    self,
    batch,
    txt_seq_lens: list[int],
    text_seq_len: int,
    device,
):
    """Return a `[batch, text_seq_len]` bool mask over real (non-padded) text tokens.
    Dynamic batches can merge requests whose captions have different real
    lengths after tokenization; the DiT still sees one padded
    `encoder_hidden_states` tensor of shape `[batch, text_seq_len, dim]`, so
    we need a mask to keep attention off the padding. Returns None when every
    request already fills the full padded length (no mask needed, zero overhead).
    """
    if all(seq_len == text_seq_len for seq_len in txt_seq_lens):
        # 均匀长度:掩码为 None,注意力层不执行任何遮盖
        return None
​
    positions = torch.arange(text_seq_len, device=device)
    seq_lens = torch.tensor(txt_seq_lens, device=device, dtype=torch.long)
    # positions: [0, 1, ..., text_seq_len-1]
    # 对每个位置 i,若 i < 该请求的 real seq_len,则标记为 True
    return positions.unsqueeze(0) < seq_lens.unsqueeze(1)

评论区精华

WanI2VCrossAttention context_lens 形状不匹配 正确性

gemini-code-assist[bot] 指出 context_lens 形状为 [B, 512](代表文本部分),但代码中错误地使用 context_lens[:, 257:] 切片,导致形状不匹配。建议直接使用 context_lens 而不切片。

结论:作者随后恢复 Wan 部分变更,因此该问题无需修复(Wan 不再改动)。 · 已解决

t5_postprocess_text 设备不匹配风险 正确性

gemini-code-assist[bot] 建议在 t5_postprocess_text 中将 seq_lens 显式移动到 positions 所在设备再比较,避免潜在设备不匹配。

结论:此问题随 Wan 恢复而消失,未在最终代码中体现。 · 已解决

风险与影响

  • 核心路径变更风险:DiT forward签名新增两个参数,需确认无其他直接调用ErnieImageDiT.forward()的地方(通过CPP或外部脚本)。
  • 缺少GPU端到端验证:作者明确标注未运行GPU测试,存在潜行回归风险。CI的pre-existing依赖冲突也阻止了单元测试在本环境中执行。
  • Wan部分恢复暴露风险:Wan的掩码导致严重视觉回归,表明类似逻辑可能引入意想不到的精度影响。虽然Ernie-Image已独立测试,但缺乏GPU验证仍需谨慎。
  • 单元测试覆盖有限:仅覆盖CPU侧的掩码构建逻辑,未测试DiT forward的实际注意力行为。
  • 用户影响:Ernie-Image模型动态批处理生成质量显著提升,消除了变长标题批处理时生成的静默错误。单请求或均匀长度批处理不受影响(掩码为None,零额外开销)。
  • 系统影响:对均匀长度请求无开销,对变长批处理引入少量掩码计算和build_varlen_mask_meta开销,但注意力计算本身节省了填充token的计算(通过sparse attention)。
  • 团队影响:展示了可复用的变长掩码模式(require_text_seq_lens / build_varlen_mask_meta),为后续修复SD3等模型提供了参考。
核心路径变更 缺少 GPU 端到端验证 Wan 部分恢复 测试覆盖有限

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论