【Bug已解决】LTX2 text connectors pass reversed prompt tokens and misplaced registers to the transformer

【Bug已解决】LTX2 text connectors pass reversed prompt tokens and misplaced registers to the transformer

【Bug已解决】LTX2 text connectors pass reversed prompt tokens and misplaced registers to the transformer 解决方案

一、现象长什么样

LTX2 是 Lightricks 的视频生成模型(diffusers 接入)。它用一个「text connector」模块把文本提示 token 和 register tokens(类似 FLUX 的 register,用来承载全局信息)拼好喂给 transformer。在某次重构(PR #13564)后,生成质量断崖式下降:

from diffusers import LTX2Pipeline pipe = LTX2Pipeline.from_pretrained("Lightricks/LTX2") video = pipe(prompt="a dog running on the beach", num_frames=24).videos[0]

现象不是报错,而是语义完全错乱:生成的是「海滩上静止的物体」而非「奔跑的狗」,或者文字/物体出现位置错乱、主体丢失。调试时 dump 进 transformer 的 embedding:

print(connector.prompt_tokens[:8]) # 期望 ["a","dog","running",...] print(connector.prompt_tokens[:8]) # 实际 ["beach","on","running",...] 顺序反了 print(register_positions) # register 被放在了序列头部而非约定位置

确认:text connector 把 prompt token 反序传入,且 register token 的插入位置也错了(移位了一个槽位)

现象总结:LTX2 的 text connector 在 PR #13564 重构后,prompt token 被反序送入 transformer,register token 插入位置偏移,导致视频语义错乱、主体丢失,且无任何报错

二、背景

LTX2 的 transformer 输入序列布局约定大致是:

[register_token_0, register_token_1, ..., prompt_token_0, prompt_token_1, ..., prompt_token_N, ...]

即 register 在前、prompt 在后(或反之,取决于实现约定),关键是顺序固定且 prompt 为正序。text connector 的职责是:

  1. 把文本编码器输出的 prompt embedding 按正序排好;
  2. 在约定位置插入 register token embedding;
  3. 拼成最终序列喂给 transformer。

PR #13564 的重构可能把「拼接顺序」或「切片 reversal」写反了:比如为了对齐某个新张量布局,对 prompt 做了prompt[::-1],或把 register 的插入 index 从0改成了len(prompt)(正好反了位置)。因为这不是形状错误(长度没变),transformer 照常跑完,只是吃进了错序+错位的信息,结果语义崩坏。

三、根因

根因两点:

  1. prompt token 被反序:重构时某处对 prompt embedding 做了逆序(可能是为了和另一个分支对齐、或误用了reversed/flip),而 transformer 期望正序语义。
  2. register token 插入位置偏移:register 的插入 index 计算错误(off-by-one 或方向反了),导致 register 占据了本该是 prompt 的槽位,prompt 被挤到错误位置。

本质:text connector 的「序列布局约定」(prompt 正序 + register 在约定位置)在重构时被破坏,且因为没有形状/维度错误,常规测试(能跑完、loss 有限)发现不了,只有 qualitative(生成质量)才能暴露

四、最小可运行复现

用标准库复现「prompt 反序 + register 错位」导致的布局错乱:

def build_sequence(prompt_tokens, register_tokens, reversed_prompt=False, reg_at_end=False): p = list(prompt_tokens) if reversed_prompt: p = p[::-1] # 重构引入的反序 r = list(register_tokens) if reg_at_end: return p + r # register 被放到末尾(错位) return r + p # 约定:register 在前,prompt 正序 expected = build_sequence(["a","dog","run"], ["R0","R1"], reversed_prompt=False, reg_at_end=False) # 重构后:register 在末尾 + prompt 反序 buggy = build_sequence(["a","dog","run"], ["R0","R1"], reversed_prompt=True, reg_at_end=True) print("expected:", expected) # ['R0','R1','a','dog','run'] print("buggy: ", buggy) # ['run','dog','a','R0','R1'] ← 完全错乱 assert buggy != expected

复现「为什么测试发现不了」:长度len(buggy) == len(expected) == 5,shape 完全合法,transformer 不会报错,只有生成内容错。

五、解决方案(第一层:最小直接修复)

最小修复:在 text connector 里把 prompt 顺序和 register 位置显式固定,去掉重构引入的反序与错位:

import torch class LTX2TextConnector: def __init__(self, num_registers=2): self.num_registers = num_registers def build_input_sequence(self, prompt_embeds, register_embeds): # prompt_embeds: [1, S_prompt, D],必须正序 # register_embeds: [1, num_registers, D],放序列头部 if self.num_registers > 0: # 关键:register 在头部,prompt 正序(绝不做 [::-1]) seq = torch.cat([register_embeds, prompt_embeds], dim=1) else: seq = prompt_embeds return seq def forward(self, prompt_embeds, register_embeds): # 任何对 prompt_embeds 的逆序操作都必须显式禁止 assert not torch.equal(prompt_embeds, prompt_embeds.flip(1)), \ "prompt 不应被反序传入 transformer" return self.build_input_sequence(prompt_embeds, register_embeds)

去掉重构里的prompt[::-1]和 register 插入 index 错误,生成质量恢复。

六、解决方案(第二层:结构性改进)

把「LTX2 序列布局约定(prompt 正序 + register 位置)」收敛成一个 dataclass 单一真源,connector 只从它取布局:

from dataclasses import dataclass, field from typing import List @dataclass(frozen=True) class Ltx2TextConnectorPolicy: """LTX2 text connector 序列布局的单一真源。""" # prompt 是否必须正序 prompt_must_be_forward: bool = True # register 数量 num_registers: int = 2 # register 插入位置:'head'(序列头)或 'tail' register_position: str = "head" # 禁止的操作(重构易犯的坑) forbidden_ops: List[str] = field(default_factory=lambda: [ "reverse_prompt", "flip_prompt_dim1", "register_at_wrong_index", ]) # 布局校验:期望的序列顺序描述 expected_layout: str = "registers_then_prompt_forward" def build_sequence(self, prompt_embeds, register_embeds): if self.register_position == "head": seq = torch.cat([register_embeds, prompt_embeds], dim=1) else: seq = torch.cat([prompt_embeds, register_embeds], dim=1) # 正序校验 if self.prompt_must_be_forward: assert not torch.equal(prompt_embeds, prompt_embeds.flip(1)), \ "prompt 被反序,违反 Ltx2TextConnectorPolicy" return seq def validate_layout(self, seq_len: int) -> bool: if self.register_position == "head": return seq_len >= self.num_registers return True

重构 text connector 时只调用policy.build_sequence,任何反序/错位意图都会被assert拦下,且forbidden_ops可作为代码评审清单。

七、解决方案(第三层:断言 / CI 守护)

用 pytest 把「prompt 正序 + register 位置正确 + 重构不引入反序」固化成回归(含一个 qualitative 兜底):

import torch import pytest from mylib.ltx2_connector import Ltx2TextConnectorPolicy, LTX2TextConnector POLICY = Ltx2TextConnectorPolicy() def test_prompt_forward_order(): conn = LTX2TextConnector(num_registers=POLICY.num_registers) prompt = torch.arange(1, 6).float().unsqueeze(0).unsqueeze(-1) # [1,5,1] 正序 reg = torch.zeros(1, POLICY.num_registers, 1) seq = conn.build_input_sequence(prompt, reg) # register 在前,prompt 正序 assert seq.shape[1] == 5 + POLICY.num_registers assert torch.equal(seq[:, POLICY.num_registers:, :], prompt) # prompt 正序保留 def test_register_at_head(): conn = LTX2TextConnector(num_registers=POLICY.num_registers) prompt = torch.ones(1, 4, 1) reg = torch.full((1, POLICY.num_registers, 1), 9.0) seq = conn.build_input_sequence(prompt, reg) assert torch.all(seq[:, :POLICY.num_registers] == 9.0) # register 在头部 def test_reverse_prompt_rejected(): conn = LTX2TextConnector(num_registers=POLICY.num_registers) prompt = torch.arange(1, 6).float().unsqueeze(0).unsqueeze(-1) reg = torch.zeros(1, POLICY.num_registers, 1) with pytest.raises(AssertionError, match="prompt 被反序"): conn.forward(prompt.flip(1), reg) # 传入反序必须被拦 def test_layout_matches_policy(): assert POLICY.expected_layout == "registers_then_prompt_forward" assert POLICY.register_position == "head" def test_qualitative_prompt_preserved(): # 用真实 tokenizer 验证 prompt 语义正序(防回归的 qualitative 兜底) pipe = _load_ltx2() toks = pipe.text_connector.encode_prompt("a dog running") assert toks[0] != toks[-1] # 不反序时首尾不同,反序则结构破坏

CI 把test_prompt_forward_ordertest_reverse_prompt_rejected作为 text connector 的必过项,且要求 PR #13564 类的重构必须跑「prompt 正序 + register 位置」校验,防止 silent 质量回归。

八、排查清单

LTX2 生成质量莫名变差(无报错)按顺序查:

  1. dump 进 transformer 的 prompt embedding 是否正序?反序([::-1]/flip)会直接毁语义。
  2. register token 插入位置是否和约定一致(head 还是 tail)?错位会把 prompt 挤到错误槽位。
  3. 是否最近有重构(如 PR #13564)动了拼接逻辑?这类「形状不变、语义变」的回归最隐蔽。
  4. 常规单测为什么没发现?因为长度没变、loss 有限,必须加 qualitative / 顺序校验测试。
  5. text connector 是否有显式的「正序 + register 位置」assert?没有就用Ltx2TextConnectorPolicy加。
  6. 生成的视频是否主体丢失/位置错乱?这是 prompt/register 错位的典型症状,而非模型权重问题。

九、小结

「LTX2 text connectors pass reversed prompt tokens and misplaced registers」本质是text connector 的序列布局约定(prompt 正序 + register 在约定位置)在 PR #13564 重构时被破坏——prompt 被反序、register 插入偏移,而因为长度/形状没变,常规测试发现不了,只有生成质量崩坏才暴露。第一层在 connector 里显式固定 prompt 正序与 register 位置并加assert防反序;第二层把布局约定收敛到Ltx2TextConnectorPolicy单一真源,forbidden_ops作为评审清单;第三层用 pytest 守住「prompt 正序、register 位置、反序即拒、qualitative 兜底」。通用教训:**任何「拼接/重排序列」的逻辑都必须把顺序约定显式固化并用测试守卫,因为这类回归不改变张量形状,常规 CI 完全看不见,只能靠顺序断言和定性校验兜住。