mirror of
https://github.com/vladmandic/automatic
synced 2026-09-04 12:00:46 +02:00
fa8bf0b5cd
Only the modular path had a per-forward kwargs hook, so a classic pipeline had no layout and the selector sparsified its whole sequence, prompt conditioning included. A denoiser pre-hook installed per generation now reads whichever convention the model uses: the *_indices tensors, or the txt_ids and img_ids a joint transformer is given. Packing order is not derivable from the call and differs between architectures, so only classes verified to pack text first publish; anything else falls back as before rather than pinning the wrong half of the sequence dense.
173 lines
8.1 KiB
Python
173 lines
8.1 KiB
Python
"""What each token in a packed sequence is, so the selector knows what it may sparsify."""
|
|
from dataclasses import dataclass
|
|
import torch
|
|
|
|
|
|
# only the bulk modalities are sparsifiable; everything else is pinned dense, and an unrecognized kind pins too
|
|
SPARSIFIABLE = frozenset({'video', 'image'})
|
|
DROPPED = frozenset({'pad'})
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Span:
|
|
kind: str
|
|
start: int
|
|
end: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TokenLayout:
|
|
"""Ordered spans covering one packed sequence."""
|
|
spans: tuple[Span, ...]
|
|
length: int
|
|
source: str = 'unknown' # how the layout was obtained, for the log
|
|
|
|
def key(self) -> tuple:
|
|
return (self.length, self.source, tuple((s.kind, s.start, s.end) for s in self.spans))
|
|
|
|
def kinds(self) -> tuple[str, ...]:
|
|
return tuple(dict.fromkeys(s.kind for s in self.spans))
|
|
|
|
def sparsifiable_tokens(self) -> int:
|
|
return sum(s.end - s.start for s in self.spans if s.kind in SPARSIFIABLE)
|
|
|
|
def token_flags(self, device) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Per token: may this be sparsified, and is it padding."""
|
|
sparse = torch.zeros(self.length, dtype=torch.bool, device=device)
|
|
pad = torch.zeros(self.length, dtype=torch.bool, device=device)
|
|
for span in self.spans:
|
|
if span.kind in SPARSIFIABLE:
|
|
sparse[span.start:span.end] = True
|
|
elif span.kind in DROPPED:
|
|
pad[span.start:span.end] = True
|
|
return sparse, pad
|
|
|
|
|
|
def runs(indices: torch.Tensor) -> list[tuple[int, int]]:
|
|
"""Contiguous [start, end) runs in a sorted 1d index tensor."""
|
|
if indices.numel() == 0:
|
|
return []
|
|
values = indices.detach().to('cpu', torch.int64).sort().values
|
|
breaks = (values[1:] - values[:-1] != 1).nonzero().flatten().tolist()
|
|
bounds = [0, *[b + 1 for b in breaks], values.numel()]
|
|
return [(int(values[bounds[i]].item()), int(values[bounds[i + 1] - 1].item()) + 1) for i in range(len(bounds) - 1)]
|
|
|
|
|
|
def layout_from_index_kwargs(kwargs: dict, length: int | None = None) -> TokenLayout | None:
|
|
"""Read a layout off the *_indices tensors a pipeline passes its transformer by name."""
|
|
spans: list[Span] = []
|
|
for name, value in kwargs.items():
|
|
if not name.endswith('_indices') or not torch.is_tensor(value) or value.dim() != 1 or value.is_floating_point():
|
|
continue
|
|
kind = name[:-len('_indices')].lower()
|
|
found = runs(value)
|
|
for position, (start, end) in enumerate(found):
|
|
# a video run that is not the last one is keyframe conditioning, which stays dense
|
|
resolved = 'cond' if (kind == 'video' and position < len(found) - 1) else kind
|
|
spans.append(Span(kind=resolved, start=start, end=end))
|
|
if not spans:
|
|
return None
|
|
spans.sort(key=lambda s: s.start)
|
|
return TokenLayout(spans=tuple(spans), length=length if length is not None else spans[-1].end, source='indices')
|
|
|
|
|
|
# how an architecture orders its joint sequence, which the call itself does not reveal. Verified against the
|
|
# diffusers transformers that take txt_ids and img_ids; HiDream packs image first and is deliberately absent, so
|
|
# it falls back rather than being pinned backwards. An unlisted class publishes nothing.
|
|
JOINT_TEXT_FIRST = frozenset({
|
|
'FluxTransformer2DModel', 'Flux2Transformer2DModel', 'ChromaTransformer2DModel', 'BriaTransformer2DModel',
|
|
'BriaFiboTransformer2DModel', 'LongCatImageTransformer2DModel', 'OvisImageTransformer2DModel',
|
|
})
|
|
|
|
|
|
def layout_from_stream_ids(kwargs: dict, cls_name: str | None) -> TokenLayout | None:
|
|
"""Read the stream lengths off the rotary id tensors a joint transformer is given by name."""
|
|
if cls_name not in JOINT_TEXT_FIRST:
|
|
return None
|
|
text, image = kwargs.get('txt_ids'), kwargs.get('img_ids')
|
|
if not torch.is_tensor(text) or not torch.is_tensor(image) or text.dim() < 2 or image.dim() < 2:
|
|
return None
|
|
return layout_from_segments((('text', text.shape[-2]), ('image', image.shape[-2])), source='stream-ids')
|
|
|
|
|
|
def layout_from_kwargs(kwargs: dict, cls_name: str | None = None) -> TokenLayout | None:
|
|
"""Whatever the denoiser says about its own packing, by whichever convention it uses."""
|
|
return layout_from_index_kwargs(kwargs or {}) or layout_from_stream_ids(kwargs or {}, cls_name)
|
|
|
|
|
|
def layout_from_segments(segments, length: int | None = None, source: str = 'segments') -> TokenLayout:
|
|
"""Build a layout from ordered (kind, count) pairs, the form a transformer knows at its packing site."""
|
|
spans: list[Span] = []
|
|
cursor = 0
|
|
for kind, count in segments:
|
|
if count <= 0:
|
|
continue
|
|
spans.append(Span(kind=kind, start=cursor, end=cursor + count))
|
|
cursor += count
|
|
return TokenLayout(spans=tuple(spans), length=length if length is not None else cursor, source=source)
|
|
|
|
|
|
def segments_from_live(live: torch.Tensor, kind: str, pad_kind: str = 'pad') -> list[tuple[str, int]]:
|
|
"""Run length encode a boolean live mask into ordered (kind, count) pairs, the dead runs labelled as padding."""
|
|
values = live.detach().to('cpu').bool()
|
|
if values.numel() == 0:
|
|
return []
|
|
changes = (values[1:] != values[:-1]).nonzero().flatten().tolist()
|
|
bounds = [0, *[c + 1 for c in changes], values.numel()]
|
|
return [(kind if bool(values[bounds[i]]) else pad_kind, bounds[i + 1] - bounds[i]) for i in range(len(bounds) - 1)]
|
|
|
|
|
|
def publish_segments(segments, length: int | None = None, source: str = 'segments') -> None:
|
|
"""Publish a layout from the site that packs the sequence, which is the only place the segment lengths are all known."""
|
|
from modules.attention import context
|
|
context.set_layout(layout_from_segments(segments, length=length, source=source))
|
|
|
|
|
|
def layout_from_prefix(length: int, prefix: int) -> TokenLayout:
|
|
"""Fallback when nothing published a layout: treat a leading run as conditioning and sparsify the rest."""
|
|
return layout_from_segments([('text', prefix), ('image', length - prefix)], length=length, source='prefix')
|
|
|
|
|
|
def block_flags(flags: torch.Tensor, block: int) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Per block: do all tokens carry the flag, does any token carry it."""
|
|
seq = flags.shape[0]
|
|
whole = (seq // block) * block
|
|
parts_all, parts_any = [], []
|
|
if whole:
|
|
view = flags[:whole].view(whole // block, block)
|
|
parts_all.append(view.all(dim=-1))
|
|
parts_any.append(view.any(dim=-1))
|
|
if whole < seq:
|
|
parts_all.append(flags[whole:].all(dim=-1, keepdim=True))
|
|
parts_any.append(flags[whole:].any(dim=-1, keepdim=True))
|
|
def join(parts):
|
|
return parts[0] if len(parts) == 1 else torch.cat(parts, dim=0)
|
|
return join(parts_all), join(parts_any)
|
|
|
|
|
|
pin_cache: dict = {}
|
|
|
|
|
|
def block_pins(layout: TokenLayout, seq_q: int, seq_kv: int, block_q: int, block_kv: int, device) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Tiles that must stay dense and tiles that can be skipped outright, as (1, 1, NQ, NK) masks."""
|
|
cache_key = (layout.key(), seq_q, seq_kv, block_q, block_kv, str(device))
|
|
hit = pin_cache.get(cache_key)
|
|
if hit is not None:
|
|
return hit
|
|
sparse_tokens, pad_tokens = layout.token_flags(device)
|
|
q_sparse = sparse_tokens[:seq_q] if layout.length >= seq_q else torch.nn.functional.pad(sparse_tokens, (0, seq_q - layout.length))
|
|
kv_sparse = sparse_tokens[:seq_kv] if layout.length >= seq_kv else torch.nn.functional.pad(sparse_tokens, (0, seq_kv - layout.length))
|
|
kv_pad = pad_tokens[:seq_kv] if layout.length >= seq_kv else torch.nn.functional.pad(pad_tokens, (0, seq_kv - layout.length))
|
|
q_all_sparse, _ = block_flags(q_sparse, block_q)
|
|
kv_all_sparse, _ = block_flags(kv_sparse, block_kv)
|
|
kv_all_pad, _ = block_flags(kv_pad, block_kv)
|
|
# a tile is pinned when its query tile or its key tile carries anything that is not sparsifiable, boundary tiles included
|
|
pins = (~q_all_sparse).unsqueeze(-1) | (~kv_all_sparse).unsqueeze(0)
|
|
drops = kv_all_pad.unsqueeze(0).expand_as(pins)
|
|
pins = (pins & ~drops).unsqueeze(0).unsqueeze(0).contiguous()
|
|
drops = drops.unsqueeze(0).unsqueeze(0).contiguous()
|
|
if len(pin_cache) > 32:
|
|
pin_cache.clear()
|
|
pin_cache[cache_key] = (pins, drops)
|
|
return pins, drops
|