Files

399 lines
14 KiB
Python

import math
from dataclasses import dataclass
from typing import Any
import torch
ANIMA_COEFFICIENTS = (
16.440769544691957,
-21.14123745515793,
13.006701290921894,
-1.6731977662609427,
0.08632681059623949,
)
_STEP_INFO_KEY = "anima_teacache_step_info"
_WRAPPER_KEY = "anima_teacache"
def poly1d(coefficients, value):
result = 0.0
for coefficient in coefficients:
result = result * value + coefficient
return result
@dataclass
class CacheEntry:
previous_modulated_input: torch.Tensor | None = None
previous_residual: torch.Tensor | None = None
accumulated_distance: float = 0.0
signature: tuple[Any, ...] | None = None
@dataclass
class CacheDecision:
should_calc: bool
cacheable: bool
keys: list[Any]
slices: list[slice]
class AnimaTeaCacheRuntime:
def __init__(self, rel_l1_thresh, start_percent, end_percent, cache_device):
self.rel_l1_thresh = rel_l1_thresh
self.start_percent = start_percent
self.end_percent = end_percent
self.cache_device = cache_device
self.entries: dict[Any, CacheEntry] = {}
self.schedule_key = None
self.last_step_index = None
def reset(self):
self.entries.clear()
self.schedule_key = None
self.last_step_index = None
def diffusion_model_wrapper(self, executor, *args, **kwargs):
diffusion_model = executor.class_obj
transformer_options = kwargs.get("transformer_options", {})
step_info = self._build_step_info(
transformer_options.get("sigmas"),
transformer_options.get("sample_sigmas"),
)
if step_info is not None:
transformer_options[_STEP_INFO_KEY] = step_info
executor.original = lambda *inner_args, **inner_kwargs: self.forward(
diffusion_model, *inner_args, **inner_kwargs
)
return executor(*args, **kwargs)
def _build_step_info(self, current_sigmas, sample_sigmas):
if (
not torch.is_tensor(current_sigmas)
or not torch.is_tensor(sample_sigmas)
):
return None
sigmas = sample_sigmas.detach().flatten()
total_steps = sigmas.numel() - 1
if total_steps <= 0:
return None
current_sigma = current_sigmas.detach().flatten()[0].to(
device=sigmas.device, dtype=sigmas.dtype
)
step_index = int((sigmas[:-1] - current_sigma).abs().argmin().item())
percent = step_index / total_steps
return {
"index": step_index,
"total": total_steps,
"percent": percent,
"schedule_key": tuple(sigmas.float().cpu().tolist()),
}
def _split_layout(self, tensor, transformer_options):
labels = transformer_options.get("cond_or_uncond")
uuids = transformer_options.get("uuids")
if not labels:
return [0], [slice(0, tensor.shape[0])]
if tensor.shape[0] % len(labels) != 0:
return None
chunk_size = tensor.shape[0] // len(labels)
occurrences = {}
keys = []
slices = []
for index, label in enumerate(labels):
uuid = uuids[index] if uuids and len(uuids) == len(labels) else None
identity = (label, uuid)
occurrence = occurrences.get(identity, 0)
occurrences[identity] = occurrence + 1
keys.append((label, uuid, occurrence))
slices.append(slice(index * chunk_size, (index + 1) * chunk_size))
return keys, slices
def decide(self, modulated_input, transformer_options):
step_info = transformer_options.get(_STEP_INFO_KEY)
layout = self._split_layout(modulated_input, transformer_options)
if step_info is None or layout is None:
self.reset()
return CacheDecision(True, False, [], [])
step_index = step_info["index"]
schedule_key = step_info["schedule_key"]
if (
self.schedule_key != schedule_key
or (step_index == 0 and self.last_step_index != 0)
or (self.last_step_index is not None and step_index < self.last_step_index)
):
self.reset()
self.schedule_key = schedule_key
self.last_step_index = step_index
keys, slices = layout
in_range = self.start_percent <= step_info["percent"] <= self.end_percent
boundary = step_index == 0 or step_index == step_info["total"] - 1
force_full = not in_range or boundary
current_inputs = []
for key, section in zip(keys, slices):
current = modulated_input[section].detach().to(self.cache_device)
current_inputs.append(current)
entry = self.entries.setdefault(key, CacheEntry())
signature = (tuple(current.shape), current.dtype, tuple(modulated_input[section].stride()))
if entry.signature != signature:
entry.previous_modulated_input = None
entry.previous_residual = None
entry.accumulated_distance = 0.0
entry.signature = signature
force_full = True
previous = entry.previous_modulated_input
if previous is None or entry.previous_residual is None:
force_full = True
elif not force_full:
denominator = previous.abs().mean()
if not torch.isfinite(denominator) or denominator.item() <= 0.0:
force_full = True
else:
relative_l1 = ((current - previous).abs().mean() / denominator).float().item()
scaled = poly1d(ANIMA_COEFFICIENTS, relative_l1)
if not math.isfinite(scaled):
force_full = True
else:
entry.accumulated_distance += scaled
if entry.accumulated_distance >= self.rel_l1_thresh:
force_full = True
for key, current in zip(keys, current_inputs):
self.entries[key].previous_modulated_input = current
if force_full:
for key in keys:
self.entries[key].accumulated_distance = 0.0
return CacheDecision(force_full, True, keys, slices)
def commit_residual(self, decision, residual):
if not decision.cacheable:
return
for key, section in zip(decision.keys, decision.slices):
self.entries[key].previous_residual = residual[section].detach().to(self.cache_device)
def reuse_residual(self, decision, hidden_states):
residuals = []
for key in decision.keys:
residual = self.entries[key].previous_residual
if residual is None:
raise RuntimeError("Anima TeaCache residual is not initialized")
residuals.append(residual.to(device=hidden_states.device, dtype=hidden_states.dtype))
return hidden_states + torch.cat(residuals, dim=0)
def _first_block_modulated_input(
self,
diffusion_model,
hidden_states,
timestep_embedding,
adaln_lora,
extra_pos_emb,
):
block = diffusion_model.blocks[0]
block_input = hidden_states
if extra_pos_emb is not None:
block_input = block_input + extra_pos_emb
modulation = block.adaln_modulation_self_attn(timestep_embedding)
if block.use_adaln_lora:
modulation = modulation + adaln_lora
shift, scale, _ = modulation.chunk(3, dim=-1)
shift = shift[:, :, None, None, :]
scale = scale[:, :, None, None, :]
return block.layer_norm_self_attn(block_input) * (1 + scale) + shift
def forward(
self,
diffusion_model,
x,
timesteps,
context,
fps=None,
padding_mask=None,
**kwargs,
):
import comfy.ldm.common_dit
original_shape = list(x.shape)
x = comfy.ldm.common_dit.pad_to_patch_size(
x,
(
diffusion_model.patch_temporal,
diffusion_model.patch_spatial,
diffusion_model.patch_spatial,
),
)
hidden_states, rope_embedding, extra_pos_emb = diffusion_model.prepare_embedded_sequence(
x, fps=fps, padding_mask=padding_mask
)
if timesteps.ndim == 1:
timesteps = timesteps.unsqueeze(1)
sinusoidal_timestep = diffusion_model.t_embedder[0](timesteps).to(hidden_states.dtype)
timestep_embedding, adaln_lora = diffusion_model.t_embedder[1](sinusoidal_timestep)
timestep_embedding = diffusion_model.t_embedding_norm(timestep_embedding)
diffusion_model.affline_scale_log_info = {
"t_embedding_B_T_D": timestep_embedding.detach()
}
diffusion_model.affline_emb = timestep_embedding
diffusion_model.crossattn_emb = context
if extra_pos_emb is not None:
assert hidden_states.shape == extra_pos_emb.shape
# Keep ComfyUI model patches active when TeaCache takes over the
# diffusion-model forward. In particular, Anima LLLite prepares its
# conditioning in a post-input patch and applies the conditioning to
# attention/MLP inputs through the transformer patch collections.
transformer_options = kwargs.get("transformer_options", {})
patches = transformer_options.get("patches", {})
if "post_input" in patches:
transformer_options = transformer_options.copy()
transformer_options["model_patch_data"] = {}
for patch in patches["post_input"]:
out = patch({
"img": hidden_states,
"x": x,
"transformer_options": transformer_options,
})
hidden_states = out["img"]
block_kwargs = {
"rope_emb_L_1_1_D": rope_embedding.unsqueeze(1).unsqueeze(0),
"adaln_lora_B_T_3D": adaln_lora,
"extra_per_block_pos_emb": extra_pos_emb,
"transformer_options": transformer_options,
}
if hidden_states.dtype == torch.float16:
hidden_states = hidden_states.float()
modulated_input = self._first_block_modulated_input(
diffusion_model,
hidden_states,
timestep_embedding,
adaln_lora,
extra_pos_emb,
)
transformer_options = block_kwargs["transformer_options"]
decision = self.decide(modulated_input, transformer_options)
if decision.should_calc:
original_hidden_states = hidden_states
for block_index, block in enumerate(diffusion_model.blocks):
transformer_options["block_index"] = block_index
hidden_states = block(
hidden_states,
timestep_embedding,
context,
**block_kwargs,
)
self.commit_residual(decision, hidden_states - original_hidden_states)
else:
hidden_states = self.reuse_residual(decision, hidden_states)
output = diffusion_model.final_layer(
hidden_states.to(context.dtype),
timestep_embedding,
adaln_lora_B_T_3D=adaln_lora,
)
output = diffusion_model.unpatchify(output)
return output[
:,
:,
: original_shape[-3],
: original_shape[-2],
: original_shape[-1],
]
class AnimaTeaCache:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL", {"tooltip": "The native ComfyUI Anima model."}),
"rel_l1_thresh": (
"FLOAT",
{"default": 0.05, "min": 0.0, "max": 10.0, "step": 0.001},
),
"start_percent": (
"FLOAT",
{"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01},
),
"end_percent": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01},
),
"cache_device": (["cuda", "cpu"], {"default": "cuda"}),
}
}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "apply_teacache"
CATEGORY = "TeaCache"
TITLE = "Anima TeaCache"
def apply_teacache(
self,
model,
rel_l1_thresh,
start_percent,
end_percent,
cache_device,
):
if rel_l1_thresh == 0:
return (model,)
if start_percent > end_percent:
raise ValueError("start_percent must not be greater than end_percent")
from comfy.ldm.anima.model import Anima
diffusion_model = model.get_model_object("diffusion_model")
if not isinstance(diffusion_model, Anima):
raise TypeError("Anima TeaCache only supports ComfyUI's native Anima model")
import comfy.model_management as model_management
import comfy.patcher_extension as patcher_extension
WrappersMP = patcher_extension.WrappersMP
device = (
model_management.get_torch_device()
if cache_device == "cuda"
else torch.device("cpu")
)
runtime = AnimaTeaCacheRuntime(
rel_l1_thresh=rel_l1_thresh,
start_percent=start_percent,
end_percent=end_percent,
cache_device=device,
)
patched_model = model.clone()
transformer_options = patched_model.model_options.setdefault("transformer_options", {})
wrappers = transformer_options.setdefault("wrappers", {})
wrappers.setdefault(WrappersMP.DIFFUSION_MODEL, {}).pop(_WRAPPER_KEY, None)
patcher_extension.add_wrapper_with_key(
WrappersMP.DIFFUSION_MODEL,
_WRAPPER_KEY,
runtime.diffusion_model_wrapper,
patched_model.model_options,
is_model_options=True,
)
return (patched_model,)
NODE_CLASS_MAPPINGS = {"AnimaTeaCache": AnimaTeaCache}
NODE_DISPLAY_NAME_MAPPINGS = {"AnimaTeaCache": AnimaTeaCache.TITLE}