multimodalart's picture
multimodalart HF Staff
StatePlay SF3 world-model demo (ZeroGPU)
e84ba1f verified
Raw
History Blame Contribute Delete
7.04 kB
"""UMT5 text encoder used by Wan2.2 (custom diffsynth weight layout).
Architecture is a stripped UMT5: tied vocab, GELU-gated FFN, T5-style relative
position bias, RMS-style layer norm. Matches the keys in
`models_t5_umt5-xxl-enc-bf16.pth`.
"""
import html
import math
import re
import ftfy
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import AutoTokenizer
def _fp16_clamp(x):
if x.dtype == torch.float16 and torch.isinf(x).any():
c = torch.finfo(x.dtype).max - 1000
x = torch.clamp(x, min=-c, max=c)
return x
class _GELU(nn.Module):
def forward(self, x):
return 0.5 * x * (1.0 + torch.tanh(
math.sqrt(2.0 / math.pi) * (x + 0.044715 * x.pow(3))))
class _T5LayerNorm(nn.Module):
def __init__(self, dim, eps=1e-6):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x):
x = x * torch.rsqrt(x.float().pow(2).mean(dim=-1, keepdim=True) + self.eps)
if self.weight.dtype in (torch.float16, torch.bfloat16):
x = x.type_as(self.weight)
return self.weight * x
class _T5Attention(nn.Module):
def __init__(self, dim, dim_attn, num_heads):
super().__init__()
self.num_heads = num_heads
self.head_dim = dim_attn // num_heads
self.q = nn.Linear(dim, dim_attn, bias=False)
self.k = nn.Linear(dim, dim_attn, bias=False)
self.v = nn.Linear(dim, dim_attn, bias=False)
self.o = nn.Linear(dim_attn, dim, bias=False)
def forward(self, x, mask=None, pos_bias=None):
b, n, c = x.size(0), self.num_heads, self.head_dim
q = self.q(x).view(b, -1, n, c)
k = self.k(x).view(b, -1, n, c)
v = self.v(x).view(b, -1, n, c)
attn_bias = x.new_zeros(b, n, q.size(1), k.size(1))
if pos_bias is not None:
attn_bias = attn_bias + pos_bias
if mask is not None:
mask = mask.view(b, 1, 1, -1) if mask.ndim == 2 else mask.unsqueeze(1)
attn_bias.masked_fill_(mask == 0, torch.finfo(x.dtype).min)
attn = torch.einsum('binc,bjnc->bnij', q, k) + attn_bias
attn = F.softmax(attn.float(), dim=-1).type_as(attn)
out = torch.einsum('bnij,bjnc->binc', attn, v).reshape(b, -1, n * c)
return self.o(out)
class _T5FeedForward(nn.Module):
def __init__(self, dim, dim_ffn):
super().__init__()
self.gate = nn.Sequential(nn.Linear(dim, dim_ffn, bias=False), _GELU())
self.fc1 = nn.Linear(dim, dim_ffn, bias=False)
self.fc2 = nn.Linear(dim_ffn, dim, bias=False)
def forward(self, x):
return self.fc2(self.fc1(x) * self.gate(x))
class _T5SelfAttention(nn.Module):
def __init__(self, dim, dim_attn, dim_ffn, num_heads, num_buckets, shared_pos):
super().__init__()
self.norm1 = _T5LayerNorm(dim)
self.attn = _T5Attention(dim, dim_attn, num_heads)
self.norm2 = _T5LayerNorm(dim)
self.ffn = _T5FeedForward(dim, dim_ffn)
self.pos_embedding = None if shared_pos else _T5RelativeEmbedding(
num_buckets, num_heads, bidirectional=True)
def forward(self, x, mask=None, pos_bias=None):
e = pos_bias if self.pos_embedding is None else self.pos_embedding(x.size(1), x.size(1))
x = _fp16_clamp(x + self.attn(self.norm1(x), mask=mask, pos_bias=e))
x = _fp16_clamp(x + self.ffn(self.norm2(x)))
return x
class _T5RelativeEmbedding(nn.Module):
def __init__(self, num_buckets, num_heads, bidirectional, max_dist=128):
super().__init__()
self.num_buckets = num_buckets
self.bidirectional = bidirectional
self.max_dist = max_dist
self.embedding = nn.Embedding(num_buckets, num_heads)
def forward(self, lq, lk):
device = self.embedding.weight.device
rel = torch.arange(lk, device=device).unsqueeze(0) - torch.arange(lq, device=device).unsqueeze(1)
rel = self._bucket(rel)
return self.embedding(rel).permute(2, 0, 1).unsqueeze(0).contiguous()
def _bucket(self, rel):
if self.bidirectional:
n = self.num_buckets // 2
buckets = (rel > 0).long() * n
rel = rel.abs()
else:
n = self.num_buckets
buckets = 0
rel = -torch.min(rel, torch.zeros_like(rel))
max_exact = n // 2
large = max_exact + (torch.log(rel.float() / max_exact) /
math.log(self.max_dist / max_exact) * (n - max_exact)).long()
large = torch.min(large, torch.full_like(large, n - 1))
buckets += torch.where(rel < max_exact, rel, large)
return buckets
class WanTextEncoder(nn.Module):
"""UMT5 encoder used by Wan2.2-TI2V-5B; loads `models_t5_umt5-xxl-enc-bf16.pth`."""
def __init__(self, vocab=256384, dim=4096, dim_attn=4096, dim_ffn=10240,
num_heads=64, num_layers=24, num_buckets=32, shared_pos=False):
super().__init__()
self.token_embedding = nn.Embedding(vocab, dim)
self.pos_embedding = _T5RelativeEmbedding(
num_buckets, num_heads, bidirectional=True) if shared_pos else None
self.blocks = nn.ModuleList([
_T5SelfAttention(dim, dim_attn, dim_ffn, num_heads, num_buckets, shared_pos)
for _ in range(num_layers)
])
self.norm = _T5LayerNorm(dim)
self.shared_pos = shared_pos
def forward(self, ids, mask=None):
x = self.token_embedding(ids)
e = self.pos_embedding(x.size(1), x.size(1)) if self.shared_pos else None
for blk in self.blocks:
x = blk(x, mask=mask, pos_bias=e)
return self.norm(x)
def _whitespace_clean(text: str) -> str:
"""Bit-identical port of `diffsynth.models.wan_video_text_encoder.
whitespace_clean(basic_clean(text))`. Run on every prompt before tokenizing
— both the training-time pipeline and the cache precompute do this, so
skipping it makes the negative prompt's T5 embedding diverge (e.g. the
Chinese fullwidth comma `,` U+FF0C → ASCII `,` U+002C swap maps to a
completely different UMT5 token id). Don't drop this."""
text = ftfy.fix_text(text)
text = html.unescape(html.unescape(text))
text = text.strip()
text = re.sub(r"\s+", " ", text)
return text.strip()
class WanTokenizer:
"""Wraps HF AutoTokenizer with the (return_mask, max_length) interface used by Wan."""
def __init__(self, path: str, seq_len: int = 512):
self.tokenizer = AutoTokenizer.from_pretrained(path)
self.seq_len = seq_len
def __call__(self, text):
if isinstance(text, str):
text = [text]
text = [_whitespace_clean(t) for t in text]
out = self.tokenizer(text, return_tensors='pt', padding='max_length',
truncation=True, max_length=self.seq_len,
add_special_tokens=True)
return out.input_ids, out.attention_mask