执行摘要
- 一句话:修复Ernie-Image动态批处理变长标题精度错误
- 推荐动作:值得精读。PR展示了如何系统性地修复动态批处理中变长输入的精度问题,包括正确的mask传递模式(从postprocess到cond kwargs再到attention layer)。同时,作者通过逐步提交和revert管理风险的做法值得借鉴。重点关注
_prepare_encoder_hidden_states_mask的设计(均匀长度返回None)以及build_varlen_mask_meta在DiT中的使用。
功能与动机
PR #29742修复了Z-Image中类似bug,review询问该bug是否Z-Image特有。分析发现Ernie-Image和Wan T2V存在相同漏洞:动态批处理合并不同长度请求后,DiT未屏蔽填充token导致生成质量下降。本PR修复Ernie-Image,并计划修复Wan(后因回归恢复)。
实现拆解
- 修改tokenizer padding配置:在
ernie_image.py的text_encoder_extra_args中将padding从False改为"longest",使不同长度的请求可以合并为单个张量。
- 重写postprocess_text:
ernie_image_postprocess_text现在通过_text_inputs.attention_mask提取每个请求的真实(未填充)token跨度,并调用pad_text_embeddings_with_mask返回TextConditioningOutput,其中包含真实的prompt_seq_lens和prompt_embeds_mask。
- 新增掩码构建方法:在
ErnieImagePipelineConfig中新增_prepare_encoder_hidden_states_mask,当批处理中请求长度不一致时生成[B, padded_len]的bool掩码,长度一致时返回None(零开销)。
- 修改_prepare_cond_kwargs:使用
require_text_seq_lens获取真实长度,并调用_prepare_encoder_hidden_states_mask将掩码放入cond_kwargs。
- 修改DiT模型:在
ernie_image.py的ErnieImageSelfAttention.forward、ErnieImageBlock.forward和ErnieImageDiT.forward中添加attn_mask和attn_mask_meta参数。DiT forward中构建联合[image, text]掩码并调用build_varlen_mask_meta,然后沿调用链传递到USPAttention。
- 新增单元测试:
test_ernie_image_pipeline_config.py包含两个测试类,分别验证postprocess_text正确提取真实长度和_prepare_cond_kwargs构建正确掩码(均匀长度返回None,变长返回边界掩码)。
关键文件:
python/sglang/multimodal_gen/configs/pipeline_configs/ernie_image.py(模块 管道配置;类别 source;类型 core-logic;符号 ernie_image_postprocess_text, _prepare_cond_kwargs, _prepare_encoder_hidden_states_mask, ErnieImagePipelineConfig): 核心配置修改:tokenizer padding、postprocess_text 重构、新增掩码构建逻辑、修改 cond_kwargs 准备。
python/sglang/multimodal_gen/test/unit/test_ernie_image_pipeline_config.py(模块 配置测试;类别 test;类型 test-coverage;符号 TestErnieImagePostprocessText, test_single_request_returns_full_length_conditioning, test_ragged_batch_preserves_real_lengths, TestErnieImagePrepareCondKwargs): 新增单元测试,覆盖 postprocess 的变长批处理和 cond_kwargs 的掩码逻辑。
python/sglang/multimodal_gen/runtime/models/dits/ernie_image.py(模块 模型定义;类别 source;类型 data-contract;符号 ErnieImageSelfAttention.forward, ErnieImageBlock.forward, ErnieImageDiT.forward): DiT 模型文件接受并传递注意力掩码到 self-attention 层,是数据契约变更的主要位置。
关键符号: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
核心配置修改: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)
评论区精华
代码审查机器人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部分保持正确。
-
WanI2VCrossAttention context_lens 形状不匹配 (correctness): 作者随后恢复 Wan 部分变更,因此该问题无需修复(Wan 不再改动)。
- t5_postprocess_text 设备不匹配风险 (correctness): 此问题随 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部分恢复, 测试覆盖有限
关联脉络
- PR #29742 Fix Z-Image ragged-caption dynamic-batching accuracy bug: 本PR是#29742的后续,修复了同类漏洞在Ernie-Image中的表现,并使用了相同的修复模式(TextConditioningOutput、require_text_seq_lens)。
参与讨论