Skip to content

Commit 1399181

Browse files
committed
perf(anima): tier-2 optimizations — flash_attn adapter, native repeat removal
Optimizations ported from https://github.com/sorryhyun/anima_lora: LLMAdapter flash attention: - LLMAdapterAttention now dispatches to flash_attn_func (no mask), flash_attn_varlen_func (with mask packing), or SDPA fallback - Mask handling changed from 4D [B,1,1,L] to 2D [B,L] bool tensors - flash_attn_varlen packs valid tokens only, eliminating wasted FLOPs on padding positions - Q/K/V states computed in [B,L,H,D] layout (native for flash_attn) - LLMAdapterTransformerBlock passes separate q_mask/kv_mask to self-attention and cross-attention - LLMAdapter.forward keeps masks as 2D, squeezes 4D for backward compat Remaining einops removal: - Replace einops repeat() with unsqueeze+expand in VideoRopePosition3DEmb.generate_embeddings and LearnablePosEmbAxis.generate_embeddings (zero-copy views) - Remove unused rearrange and repeat imports from einops - Remove numpy import, replace np.sqrt with math.sqrt - Remove redundant .float() in Timesteps.forward Other: - Pass x_flat as explicit self-attention context in Block._forward instead of None to skip the context=x branch in compute_qkv Add 11 new tests covering flash_attn dispatch, 2D mask handling, varlen packing correctness, and backward-compatible 4D mask support.
1 parent 6046def commit 1399181

2 files changed

Lines changed: 531 additions & 107 deletions

File tree

library/anima_models.py

Lines changed: 110 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import math
55
from typing import Any, Optional, Tuple
66

7-
import numpy as np
87
import torch
9-
from einops import rearrange, repeat
108
from einops.layers.torch import Rearrange
119
from torch import nn
1210
import torch.nn.functional as F
@@ -461,9 +459,9 @@ def generate_embeddings(
461459

462460
em_T_H_W_D = torch.cat(
463461
[
464-
repeat(half_emb_t, "t d -> t h w d", h=H, w=W),
465-
repeat(half_emb_h, "h d -> t h w d", t=T, w=W),
466-
repeat(half_emb_w, "w d -> t h w d", t=T, h=H),
462+
half_emb_t[:, None, None, :].expand(-1, H, W, -1),
463+
half_emb_h[None, :, None, :].expand(T, -1, W, -1),
464+
half_emb_w[None, None, :, :].expand(T, H, -1, -1),
467465
]
468466
* 2,
469467
dim=-1,
@@ -527,16 +525,16 @@ def generate_embeddings(self, B_T_H_W_C: torch.Size, fps: Optional[torch.Tensor]
527525
emb_w_W = self.pos_emb_w[:W]
528526
emb_t_T = self.pos_emb_t[:T]
529527
emb = (
530-
repeat(emb_t_T, "t d-> b t h w d", b=B, h=H, w=W)
531-
+ repeat(emb_h_H, "h d-> b t h w d", b=B, t=T, w=W)
532-
+ repeat(emb_w_W, "w d-> b t h w d", b=B, t=T, h=H)
528+
emb_t_T[None, :, None, None, :].expand(B, -1, H, W, -1)
529+
+ emb_h_H[None, None, :, None, :].expand(B, T, -1, W, -1)
530+
+ emb_w_W[None, None, None, :, :].expand(B, T, H, -1, -1)
533531
)
534532
assert list(emb.shape)[:4] == [B, T, H, W], f"bad shape: {list(emb.shape)[:4]} != {B, T, H, W}"
535533
else:
536534
raise ValueError(f"Unknown interpolation method {self.interpolation}")
537535

538536
norm = torch.linalg.vector_norm(emb, dim=-1, keepdim=True, dtype=torch.float32)
539-
norm = torch.add(1e-6, norm, alpha=np.sqrt(norm.numel() / emb.numel()))
537+
norm = torch.add(1e-6, norm, alpha=math.sqrt(norm.numel() / emb.numel()))
540538
return emb / norm.to(emb.dtype)
541539

542540

@@ -557,7 +555,7 @@ def forward(self, timesteps_B_T: torch.Tensor) -> torch.Tensor:
557555
exponent = exponent / (half_dim - 0.0)
558556

559557
emb = torch.exp(exponent)
560-
emb = timesteps[:, None].float() * emb[None, :]
558+
emb = timesteps[:, None] * emb[None, :]
561559

562560
sin_emb = torch.sin(emb)
563561
cos_emb = torch.cos(emb)
@@ -603,33 +601,6 @@ def forward(self, sample: torch.Tensor) -> Tuple[torch.Tensor, Optional[torch.Te
603601

604602
return emb_B_T_D, adaln_lora_B_T_3D
605603

606-
607-
# Commented out Fourier Features (not used in Anima). Kept for reference.
608-
# class FourierFeatures(nn.Module):
609-
# """Fourier feature transform: [B] -> [B, D]."""
610-
611-
# def __init__(self, num_channels: int, bandwidth: int = 1, normalize: bool = False):
612-
# super().__init__()
613-
# self.register_buffer("freqs", 2 * np.pi * bandwidth * torch.randn(num_channels), persistent=True)
614-
# self.register_buffer("phases", 2 * np.pi * torch.rand(num_channels), persistent=True)
615-
# self.gain = np.sqrt(2) if normalize else 1
616-
# self.bandwidth = bandwidth
617-
# self.num_channels = num_channels
618-
# self.reset_parameters()
619-
620-
# def reset_parameters(self) -> None:
621-
# generator = torch.Generator()
622-
# generator.manual_seed(0)
623-
# self.freqs = 2 * np.pi * self.bandwidth * torch.randn(self.num_channels, generator=generator).to(self.freqs.device)
624-
# self.phases = 2 * np.pi * torch.rand(self.num_channels, generator=generator).to(self.freqs.device)
625-
626-
# def forward(self, x: torch.Tensor, gain: float = 1.0) -> torch.Tensor:
627-
# in_dtype = x.dtype
628-
# x = x.to(torch.float32).ger(self.freqs.to(torch.float32)).add(self.phases.to(torch.float32))
629-
# x = x.cos().mul(self.gain * gain).to(in_dtype)
630-
# return x
631-
632-
633604
# Patch Embedding
634605
class PatchEmbed(nn.Module):
635606
"""Patch embedding: (B, C, T, H, W) -> (B, T', H', W', D)"""
@@ -904,7 +875,7 @@ def _adaln_fn(_x, _norm_layer, _scale, _shift):
904875
result = self.self_attn(
905876
x_flat,
906877
attn_params,
907-
None,
878+
x_flat,
908879
rope_cos_sin=rope_cos_sin,
909880
).unflatten(1, (T, H, W))
910881
x_B_T_H_W_D = x_B_T_H_W_D + gate_self_attn_B_T_1_1_D * result
@@ -1457,27 +1428,110 @@ def __init__(self, query_dim, context_dim, n_heads, head_dim):
14571428

14581429
self.o_proj = nn.Linear(inner_dim, query_dim, bias=False)
14591430

1460-
def forward(self, x, mask=None, context=None, position_embeddings=None, position_embeddings_context=None):
1431+
def forward(
1432+
self,
1433+
x,
1434+
q_mask=None,
1435+
kv_mask=None,
1436+
context=None,
1437+
position_embeddings=None,
1438+
position_embeddings_context=None,
1439+
):
1440+
"""
1441+
Args:
1442+
x: Query input [B, L_q, D].
1443+
q_mask: Optional 2-D bool mask [B, L_q] — True = valid token.
1444+
kv_mask: Optional 2-D bool mask [B, L_kv] — True = valid token.
1445+
context: Key/Value input [B, L_kv, D]. Defaults to x (self-attention).
1446+
position_embeddings: (cos, sin) for query RoPE.
1447+
position_embeddings_context: (cos, sin) for key RoPE.
1448+
"""
14611449
context = x if context is None else context
14621450
input_shape = x.shape[:-1]
14631451
q_shape = (*input_shape, self.n_heads, self.head_dim)
14641452
context_shape = context.shape[:-1]
14651453
kv_shape = (*context_shape, self.n_heads, self.head_dim)
14661454

1467-
query_states = self.q_norm(self.q_proj(x).view(q_shape)).transpose(1, 2)
1468-
key_states = self.k_norm(self.k_proj(context).view(kv_shape)).transpose(1, 2)
1469-
value_states = self.v_proj(context).view(kv_shape).transpose(1, 2)
1455+
# [B, L, H, D] layout — native for flash_attn
1456+
query_states = self.q_norm(self.q_proj(x).view(q_shape))
1457+
key_states = self.k_norm(self.k_proj(context).view(kv_shape))
1458+
value_states = self.v_proj(context).view(kv_shape)
14701459

14711460
if position_embeddings is not None:
14721461
assert position_embeddings_context is not None
1462+
# RoPE expects [B, H, L, D] layout
14731463
cos, sin = position_embeddings
1474-
query_states = _adapter_apply_rotary_pos_emb(query_states, cos, sin)
1464+
query_states = _adapter_apply_rotary_pos_emb(
1465+
query_states.transpose(1, 2), cos, sin
1466+
).transpose(1, 2)
14751467
cos, sin = position_embeddings_context
1476-
key_states = _adapter_apply_rotary_pos_emb(key_states, cos, sin)
1468+
key_states = _adapter_apply_rotary_pos_emb(
1469+
key_states.transpose(1, 2), cos, sin
1470+
).transpose(1, 2)
1471+
1472+
can_use_flash = (
1473+
attention.flash_attn_varlen_func is not None
1474+
and query_states.dtype in (torch.float16, torch.bfloat16)
1475+
)
1476+
1477+
if can_use_flash and q_mask is None and kv_mask is None:
1478+
# No masking — simple flash attention, [B, L, H, D] layout
1479+
attn_output = attention.flash_attn_func(
1480+
query_states, key_states, value_states
1481+
)
1482+
elif can_use_flash:
1483+
# Varlen flash attention: pack valid tokens, attend, unpack
1484+
B, L_q = query_states.shape[:2]
1485+
L_kv = key_states.shape[1]
1486+
1487+
eff_q_mask = (
1488+
q_mask
1489+
if q_mask is not None
1490+
else query_states.new_ones(B, L_q, dtype=torch.bool)
1491+
)
1492+
eff_kv_mask = (
1493+
kv_mask
1494+
if kv_mask is not None
1495+
else key_states.new_ones(B, L_kv, dtype=torch.bool)
1496+
)
14771497

1478-
attn_output = F.scaled_dot_product_attention(query_states, key_states, value_states, attn_mask=mask)
1498+
q_seqlens = eff_q_mask.sum(dim=1, dtype=torch.int32)
1499+
kv_seqlens = eff_kv_mask.sum(dim=1, dtype=torch.int32)
1500+
1501+
cu_seqlens_q = F.pad(q_seqlens.cumsum(0, dtype=torch.int32), (1, 0))
1502+
cu_seqlens_kv = F.pad(kv_seqlens.cumsum(0, dtype=torch.int32), (1, 0))
1503+
1504+
# Pack by removing padding: [B, L, H, D] -> [total_valid, H, D]
1505+
q_packed = query_states[eff_q_mask]
1506+
k_packed = key_states[eff_kv_mask]
1507+
v_packed = value_states[eff_kv_mask]
1508+
1509+
out_packed = attention.flash_attn_varlen_func(
1510+
q_packed,
1511+
k_packed,
1512+
v_packed,
1513+
cu_seqlens_q,
1514+
cu_seqlens_kv,
1515+
L_q,
1516+
L_kv,
1517+
)
14791518

1480-
attn_output = attn_output.transpose(1, 2).reshape(*input_shape, -1).contiguous()
1519+
# Unpack: [total_valid_q, H, D] -> [B, L_q, H, D]
1520+
attn_output = query_states.new_zeros(B, L_q, self.n_heads, self.head_dim)
1521+
attn_output[eff_q_mask] = out_packed
1522+
else:
1523+
# Fallback to PyTorch SDPA: needs [B, H, L, D] layout
1524+
# Expand kv_mask to 4D for SDPA broadcasting: [B, L] -> [B, 1, 1, L]
1525+
sdpa_mask = kv_mask[:, None, None, :] if kv_mask is not None else None
1526+
attn_output = F.scaled_dot_product_attention(
1527+
query_states.transpose(1, 2),
1528+
key_states.transpose(1, 2),
1529+
value_states.transpose(1, 2),
1530+
attn_mask=sdpa_mask,
1531+
)
1532+
attn_output = attn_output.transpose(1, 2)
1533+
1534+
attn_output = attn_output.reshape(*input_shape, -1).contiguous()
14811535
attn_output = self.o_proj(attn_output)
14821536
return attn_output
14831537

@@ -1525,7 +1579,8 @@ def forward(
15251579
normed = self.norm_self_attn(x)
15261580
attn_out = self.self_attn(
15271581
normed,
1528-
mask=target_attention_mask,
1582+
q_mask=target_attention_mask,
1583+
kv_mask=target_attention_mask,
15291584
position_embeddings=position_embeddings,
15301585
position_embeddings_context=position_embeddings,
15311586
)
@@ -1534,7 +1589,8 @@ def forward(
15341589
normed = self.norm_cross_attn(x)
15351590
attn_out = self.cross_attn(
15361591
normed,
1537-
mask=source_attention_mask,
1592+
q_mask=target_attention_mask,
1593+
kv_mask=source_attention_mask,
15381594
context=context,
15391595
position_embeddings=position_embeddings,
15401596
position_embeddings_context=position_embeddings_context,
@@ -1577,15 +1633,17 @@ def __init__(
15771633
self.norm = LLMAdapterRMSNorm(target_dim)
15781634

15791635
def forward(self, source_hidden_states, target_input_ids, target_attention_mask=None, source_attention_mask=None):
1636+
# Keep masks as 2D [B, L] bool tensors — the attention layer handles
1637+
# expansion to 4D for SDPA or packing for flash_attn_varlen_func.
15801638
if target_attention_mask is not None:
15811639
target_attention_mask = target_attention_mask.to(torch.bool)
1582-
if target_attention_mask.ndim == 2:
1583-
target_attention_mask = target_attention_mask.unsqueeze(1).unsqueeze(1)
1640+
if target_attention_mask.ndim == 4:
1641+
target_attention_mask = target_attention_mask.squeeze(1).squeeze(1)
15841642

15851643
if source_attention_mask is not None:
15861644
source_attention_mask = source_attention_mask.to(torch.bool)
1587-
if source_attention_mask.ndim == 2:
1588-
source_attention_mask = source_attention_mask.unsqueeze(1).unsqueeze(1)
1645+
if source_attention_mask.ndim == 4:
1646+
source_attention_mask = source_attention_mask.squeeze(1).squeeze(1)
15891647

15901648
x = self.in_proj(self.embed(target_input_ids))
15911649
context = source_hidden_states
@@ -1604,57 +1662,3 @@ def forward(self, source_hidden_states, target_input_ids, target_attention_mask=
16041662
)
16051663
return self.norm(self.out_proj(x))
16061664

1607-
1608-
# Not used currently, but kept for reference
1609-
1610-
# def get_dit_config(state_dict, key_prefix=""):
1611-
# """Derive DiT configuration from state_dict weight shapes."""
1612-
# dit_config = {}
1613-
# dit_config["max_img_h"] = 512
1614-
# dit_config["max_img_w"] = 512
1615-
# dit_config["max_frames"] = 128
1616-
# concat_padding_mask = True
1617-
# dit_config["in_channels"] = (state_dict["{}x_embedder.proj.1.weight".format(key_prefix)].shape[1] // 4) - int(
1618-
# concat_padding_mask
1619-
# )
1620-
# dit_config["out_channels"] = 16
1621-
# dit_config["patch_spatial"] = 2
1622-
# dit_config["patch_temporal"] = 1
1623-
# dit_config["model_channels"] = state_dict["{}x_embedder.proj.1.weight".format(key_prefix)].shape[0]
1624-
# dit_config["concat_padding_mask"] = concat_padding_mask
1625-
# dit_config["crossattn_emb_channels"] = 1024
1626-
# dit_config["pos_emb_cls"] = "rope3d"
1627-
# dit_config["pos_emb_learnable"] = True
1628-
# dit_config["pos_emb_interpolation"] = "crop"
1629-
# dit_config["min_fps"] = 1
1630-
# dit_config["max_fps"] = 30
1631-
1632-
# dit_config["use_adaln_lora"] = True
1633-
# dit_config["adaln_lora_dim"] = 256
1634-
# if dit_config["model_channels"] == 2048:
1635-
# dit_config["num_blocks"] = 28
1636-
# dit_config["num_heads"] = 16
1637-
# elif dit_config["model_channels"] == 5120:
1638-
# dit_config["num_blocks"] = 36
1639-
# dit_config["num_heads"] = 40
1640-
# elif dit_config["model_channels"] == 1280:
1641-
# dit_config["num_blocks"] = 20
1642-
# dit_config["num_heads"] = 20
1643-
1644-
# if dit_config["in_channels"] == 16:
1645-
# dit_config["extra_per_block_abs_pos_emb"] = False
1646-
# dit_config["rope_h_extrapolation_ratio"] = 4.0
1647-
# dit_config["rope_w_extrapolation_ratio"] = 4.0
1648-
# dit_config["rope_t_extrapolation_ratio"] = 1.0
1649-
# elif dit_config["in_channels"] == 17:
1650-
# dit_config["extra_per_block_abs_pos_emb"] = False
1651-
# dit_config["rope_h_extrapolation_ratio"] = 3.0
1652-
# dit_config["rope_w_extrapolation_ratio"] = 3.0
1653-
# dit_config["rope_t_extrapolation_ratio"] = 1.0
1654-
1655-
# dit_config["extra_h_extrapolation_ratio"] = 1.0
1656-
# dit_config["extra_w_extrapolation_ratio"] = 1.0
1657-
# dit_config["extra_t_extrapolation_ratio"] = 1.0
1658-
# dit_config["rope_enable_fps_modulation"] = False
1659-
1660-
# return dit_config

0 commit comments

Comments
 (0)