【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 的职责是:
- 把文本编码器输出的 prompt embedding 按正序排好;
- 在约定位置插入 register token embedding;
- 拼成最终序列喂给 transformer。
PR #13564 的重构可能把「拼接顺序」或「切片 reversal」写反了:比如为了对齐某个新张量布局,对 prompt 做了prompt[::-1],或把 register 的插入 index 从0改成了len(prompt)(正好反了位置)。因为这不是形状错误(长度没变),transformer 照常跑完,只是吃进了错序+错位的信息,结果语义崩坏。
三、根因
根因两点:
- prompt token 被反序:重构时某处对 prompt embedding 做了逆序(可能是为了和另一个分支对齐、或误用了
reversed/flip),而 transformer 期望正序语义。 - 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_order与test_reverse_prompt_rejected作为 text connector 的必过项,且要求 PR #13564 类的重构必须跑「prompt 正序 + register 位置」校验,防止 silent 质量回归。
八、排查清单
LTX2 生成质量莫名变差(无报错)按顺序查:
- dump 进 transformer 的 prompt embedding 是否正序?反序(
[::-1]/flip)会直接毁语义。 - register token 插入位置是否和约定一致(head 还是 tail)?错位会把 prompt 挤到错误槽位。
- 是否最近有重构(如 PR #13564)动了拼接逻辑?这类「形状不变、语义变」的回归最隐蔽。
- 常规单测为什么没发现?因为长度没变、loss 有限,必须加 qualitative / 顺序校验测试。
- text connector 是否有显式的「正序 + register 位置」assert?没有就用
Ltx2TextConnectorPolicy加。 - 生成的视频是否主体丢失/位置错乱?这是 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 完全看不见,只能靠顺序断言和定性校验兜住。
