mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-09-14 12:56:37 +08:00
61d06654cb
Preserves in-progress work so it is not lost: a new _cloud_media module plus the SDK, public-surface, and model-transform changes that reference it. All four files compile; committed as a recovery point rather than a validated release.
4594 lines
172 KiB
Python
4594 lines
172 KiB
Python
"""Core-owned model transforms available through ``ModelRef.patch``.
|
|
|
|
The host accepts only names in ``TRANSFORMS`` and validates every parameter.
|
|
Transform implementations run in the trusted process and cannot be registered
|
|
by a guest. Each transform returns a cloned patcher so requests compose without
|
|
mutating their input model.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Callable, Iterable
|
|
|
|
_MISSING = object()
|
|
|
|
|
|
class TransformError(Exception):
|
|
"""A transform request the host refuses. Always says what was wrong."""
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Parameter specs
|
|
#
|
|
# Declarative on purpose. Validation, the guest-facing error message, and the
|
|
# generated documentation all read the SAME object, so a parameter cannot be
|
|
# accepted with a range the docs do not state.
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
class Param:
|
|
type_name = "param"
|
|
|
|
def __init__(self, *, doc: str = "", default: Any = _MISSING) -> None:
|
|
self.doc = doc
|
|
self.default = default
|
|
|
|
@property
|
|
def required(self) -> bool:
|
|
return self.default is _MISSING
|
|
|
|
def check(self, name: str, value: Any) -> Any:
|
|
raise NotImplementedError
|
|
|
|
def describe(self) -> dict:
|
|
d: dict[str, Any] = {"type": self.type_name, "doc": self.doc}
|
|
if not self.required:
|
|
d["default"] = self.default
|
|
return d
|
|
|
|
|
|
class OneOf(Param):
|
|
type_name = "enum"
|
|
|
|
def __init__(self, choices: Iterable[str], **kw: Any) -> None:
|
|
super().__init__(**kw)
|
|
self.choices = tuple(choices)
|
|
|
|
def check(self, name: str, value: Any) -> Any:
|
|
if value not in self.choices:
|
|
raise TransformError(
|
|
f"{name}={value!r} is not one of {list(self.choices)}")
|
|
return value
|
|
|
|
def describe(self) -> dict:
|
|
return {**super().describe(), "choices": list(self.choices)}
|
|
|
|
|
|
class Bool(Param):
|
|
type_name = "bool"
|
|
|
|
def check(self, name: str, value: Any) -> Any:
|
|
if not isinstance(value, bool):
|
|
raise TransformError(f"{name} must be a bool, got {type(value).__name__}")
|
|
return value
|
|
|
|
|
|
class NullableBool(Bool):
|
|
type_name = "bool-or-null"
|
|
|
|
def check(self, name: str, value: Any) -> Any:
|
|
if value is None:
|
|
return None
|
|
return super().check(name, value)
|
|
|
|
|
|
class _Numeric(Param):
|
|
py_type: type = float
|
|
|
|
def __init__(self, minimum: float, maximum: float, **kw: Any) -> None:
|
|
super().__init__(**kw)
|
|
self.minimum = minimum
|
|
self.maximum = maximum
|
|
|
|
def check(self, name: str, value: Any) -> Any:
|
|
# bool is an int in Python and would silently pass an Int check as 0/1.
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
raise TransformError(
|
|
f"{name} must be a {self.type_name}, got {type(value).__name__}")
|
|
if self.py_type is int and not isinstance(value, int):
|
|
raise TransformError(f"{name} must be a whole number, got {value!r}")
|
|
if not (self.minimum <= value <= self.maximum):
|
|
raise TransformError(
|
|
f"{name}={value} is outside [{self.minimum}, {self.maximum}]")
|
|
return self.py_type(value)
|
|
|
|
def describe(self) -> dict:
|
|
return {**super().describe(), "min": self.minimum, "max": self.maximum}
|
|
|
|
|
|
class Int(_Numeric):
|
|
type_name = "int"
|
|
py_type = int
|
|
|
|
|
|
class Float(_Numeric):
|
|
type_name = "float"
|
|
py_type = float
|
|
|
|
|
|
class IntList(Param):
|
|
type_name = "int-list"
|
|
|
|
def __init__(self, max_items: int, **kw: Any) -> None:
|
|
super().__init__(**kw)
|
|
self.max_items = max_items
|
|
|
|
def check(self, name: str, value: Any) -> list[int]:
|
|
if not isinstance(value, (list, tuple)):
|
|
raise TransformError(
|
|
f"{name} must be a list of integers, got "
|
|
f"{type(value).__name__}")
|
|
if len(value) > self.max_items:
|
|
raise TransformError(
|
|
f"{name} has {len(value)} entries; maximum is {self.max_items}")
|
|
if any(isinstance(item, bool) or not isinstance(item, int)
|
|
for item in value):
|
|
raise TransformError(f"{name} must contain only integers")
|
|
return list(value)
|
|
|
|
def describe(self) -> dict:
|
|
return {**super().describe(), "max_items": self.max_items}
|
|
|
|
|
|
class TokenWeights(Param):
|
|
type_name = "token-weight-list"
|
|
|
|
def __init__(self, max_items: int, **kw: Any) -> None:
|
|
super().__init__(**kw)
|
|
self.max_items = max_items
|
|
|
|
def check(self, name: str, value: Any) -> list[tuple[int, float, float]]:
|
|
import math
|
|
|
|
if not isinstance(value, (list, tuple)):
|
|
raise TransformError(f"{name} must be a list of token weights")
|
|
if len(value) > self.max_items:
|
|
raise TransformError(
|
|
f"{name} has {len(value)} entries; maximum is {self.max_items}")
|
|
checked = []
|
|
for index, item in enumerate(value):
|
|
if not isinstance(item, (list, tuple)) or len(item) != 3:
|
|
raise TransformError(
|
|
f"{name}[{index}] must be (position, value factor, key bias)")
|
|
position, value_factor, key_bias = item
|
|
if (
|
|
isinstance(position, bool)
|
|
or not isinstance(position, int)
|
|
or not 0 <= position <= 1000000
|
|
):
|
|
raise TransformError(
|
|
f"{name}[{index}] has an invalid token position")
|
|
if any(
|
|
isinstance(number, bool)
|
|
or not isinstance(number, (int, float))
|
|
or not math.isfinite(float(number))
|
|
or abs(float(number)) > 1000000
|
|
for number in (value_factor, key_bias)
|
|
):
|
|
raise TransformError(
|
|
f"{name}[{index}] has a non-finite or unbounded weight")
|
|
checked.append((position, float(value_factor), float(key_bias)))
|
|
return checked
|
|
|
|
def describe(self) -> dict:
|
|
return {**super().describe(), "max_items": self.max_items}
|
|
|
|
|
|
class FloatList(Param):
|
|
type_name = "float-list"
|
|
|
|
def __init__(self, minimum: float, maximum: float, max_items: int,
|
|
**kw: Any) -> None:
|
|
super().__init__(**kw)
|
|
self.minimum = minimum
|
|
self.maximum = maximum
|
|
self.max_items = max_items
|
|
|
|
def check(self, name: str, value: Any) -> list[float]:
|
|
import math
|
|
|
|
if not isinstance(value, (list, tuple)):
|
|
raise TransformError(f"{name} must be a list of numbers")
|
|
if not value or len(value) > self.max_items:
|
|
raise TransformError(
|
|
f"{name} must contain 1 to {self.max_items} entries")
|
|
out = []
|
|
for index, item in enumerate(value):
|
|
if (
|
|
isinstance(item, bool)
|
|
or not isinstance(item, (int, float))
|
|
or not math.isfinite(float(item))
|
|
or not self.minimum <= float(item) <= self.maximum
|
|
):
|
|
raise TransformError(
|
|
f"{name}[{index}] must be within "
|
|
f"[{self.minimum}, {self.maximum}]")
|
|
out.append(float(item))
|
|
return out
|
|
|
|
def describe(self) -> dict:
|
|
return {
|
|
**super().describe(),
|
|
"min": self.minimum,
|
|
"max": self.maximum,
|
|
"max_items": self.max_items,
|
|
}
|
|
|
|
|
|
class RefOf(Param):
|
|
"""A parameter that is another ref — a mask, a conditioning.
|
|
|
|
Several transforms are parameterised by a tensor rather than a scalar
|
|
(a NAG conditioning, an attention mask). A ref token cannot be smuggled
|
|
inside a value payload, because values go out-of-band over the buffer
|
|
channel while tokens go over the JSON wire. So the option payload accepts
|
|
tokens natively and the host resolves them, checking the kind, before the
|
|
implementation ever sees an object.
|
|
"""
|
|
type_name = "ref"
|
|
|
|
def __init__(self, kind: str, **kw: Any) -> None:
|
|
super().__init__(**kw)
|
|
self.kind = kind
|
|
|
|
def check(self, name: str, value: Any) -> Any:
|
|
from ._sdk import Ref
|
|
|
|
if value is None and not self.required:
|
|
return None
|
|
if not isinstance(value, Ref) or value.kind != self.kind:
|
|
raise TransformError(
|
|
f"{name} must be a {self.kind} ref, got {type(value).__name__}")
|
|
return value
|
|
|
|
def describe(self) -> dict:
|
|
return {**super().describe(), "ref_kind": self.kind}
|
|
|
|
|
|
class ListOfRefs(Param):
|
|
"""A bounded homogeneous list of typed refs.
|
|
|
|
Retained regional-attention closures are the first consumer: they declare
|
|
a finite list of CONDITIONING and MASK inputs while the registering node's
|
|
ref table is still live. The host resolves every member before retaining
|
|
the closure; no ref token survives into sampling.
|
|
"""
|
|
|
|
type_name = "ref-list"
|
|
|
|
def __init__(
|
|
self, kind: str, *, min_items: int = 0, max_items: int = 32,
|
|
**kw: Any,
|
|
) -> None:
|
|
super().__init__(**kw)
|
|
if not (
|
|
isinstance(min_items, int)
|
|
and isinstance(max_items, int)
|
|
and 0 <= min_items <= max_items
|
|
):
|
|
raise ValueError("ref-list bounds must satisfy 0 <= min <= max")
|
|
self.kind = kind
|
|
self.min_items = min_items
|
|
self.max_items = max_items
|
|
|
|
def check(self, name: str, value: Any) -> list[Any]:
|
|
from ._sdk import Ref
|
|
|
|
if not isinstance(value, (list, tuple)):
|
|
raise TransformError(f"{name} must be a list of {self.kind} refs")
|
|
if not self.min_items <= len(value) <= self.max_items:
|
|
raise TransformError(
|
|
f"{name} must contain {self.min_items}..{self.max_items} refs")
|
|
checked = []
|
|
for index, item in enumerate(value):
|
|
if not isinstance(item, Ref) or item.kind != self.kind:
|
|
raise TransformError(
|
|
f"{name}[{index}] must be a {self.kind} ref, got "
|
|
f"{type(item).__name__}")
|
|
checked.append(item)
|
|
return checked
|
|
|
|
def describe(self) -> dict:
|
|
return {
|
|
**super().describe(),
|
|
"ref_kind": self.kind,
|
|
"min_items": self.min_items,
|
|
"max_items": self.max_items,
|
|
}
|
|
|
|
|
|
class SafeTensorName(Param):
|
|
"""A confined logical filename in a fixed host model catalogue."""
|
|
|
|
type_name = "safetensors-name"
|
|
|
|
def check(self, name: str, value: Any) -> str:
|
|
if not isinstance(value, str):
|
|
raise TransformError(f"{name} must be a string")
|
|
if (
|
|
not value
|
|
or len(value) > 1024
|
|
or "\\" in value
|
|
or "\x00" in value
|
|
or value.startswith("/")
|
|
or "://" in value
|
|
or any(part in ("", ".", "..") for part in value.split("/"))
|
|
or not value.lower().endswith((".safetensors", ".sft"))
|
|
):
|
|
raise TransformError(
|
|
f"{name} must be a confined SafeTensors catalogue name")
|
|
return value
|
|
|
|
|
|
class WeightArchiveName(Param):
|
|
"""A confined logical filename for a restricted tensor archive."""
|
|
|
|
type_name = "weight-archive-name"
|
|
|
|
def check(self, name: str, value: Any) -> str:
|
|
if not isinstance(value, str):
|
|
raise TransformError(f"{name} must be a string")
|
|
if (
|
|
not value
|
|
or len(value) > 1024
|
|
or "\\" in value
|
|
or "\x00" in value
|
|
or value.startswith("/")
|
|
or "://" in value
|
|
or any(part in ("", ".", "..") for part in value.split("/"))
|
|
or not value.lower().endswith(
|
|
(".bin", ".ckpt", ".patch", ".pt", ".pth")
|
|
)
|
|
):
|
|
raise TransformError(
|
|
f"{name} must be a confined PyTorch weight-archive name")
|
|
return value
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# The transform table
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
class Transform:
|
|
def __init__(self, name: str, summary: str, params: dict[str, Param],
|
|
apply: Callable[..., Any], *, experimental: bool = False) -> None:
|
|
self.name = name
|
|
self.summary = summary
|
|
self.params = params
|
|
self.apply = apply
|
|
self.experimental = experimental
|
|
|
|
def describe(self) -> dict:
|
|
return {
|
|
"name": self.name,
|
|
"summary": self.summary,
|
|
"experimental": self.experimental,
|
|
"params": {k: v.describe() for k, v in self.params.items()},
|
|
}
|
|
|
|
|
|
def validate(name: str, params: dict) -> dict:
|
|
"""Resolve a request against the table. Refuses everything unrecognised.
|
|
|
|
Unknown parameter names are an error rather than being ignored. A silently
|
|
dropped parameter is the worst outcome available here: the node appears to
|
|
work and the setting the user asked for is simply absent.
|
|
"""
|
|
t = TRANSFORMS.get(name)
|
|
if t is None:
|
|
raise TransformError(
|
|
f"unknown model transform {name!r}. Available: "
|
|
f"{sorted(TRANSFORMS)}")
|
|
|
|
unknown = set(params) - set(t.params)
|
|
if unknown:
|
|
raise TransformError(
|
|
f"{name}: unknown parameter(s) {sorted(unknown)}; "
|
|
f"accepts {sorted(t.params)}")
|
|
|
|
out = {}
|
|
for pname, spec in t.params.items():
|
|
if pname in params:
|
|
out[pname] = spec.check(f"{name}.{pname}", params[pname])
|
|
elif spec.required:
|
|
raise TransformError(f"{name}: missing required parameter {pname!r}")
|
|
else:
|
|
out[pname] = spec.default
|
|
return out
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Implementations. Core's own, calling core's own functions.
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
# Where each supported architecture keeps its transformer blocks and its
|
|
# optional embedding modules. This table is the whole reason block swapping is
|
|
# a generic transform rather than a vendor one: the POLICY ("keep N resident,
|
|
# offload the rest") is identical everywhere, and only these attribute names
|
|
# differ. Adding an architecture is a row here, not an API change.
|
|
_BLOCK_SWAP_ARCHITECTURES = {
|
|
"WAN21": {
|
|
"blocks": "blocks",
|
|
"txt_emb": "text_embedding",
|
|
"img_emb": "img_emb",
|
|
},
|
|
}
|
|
|
|
|
|
def _block_swap_layout(diffusion_model):
|
|
"""Find the block list for a model, or refuse by name."""
|
|
for type_name in (cls.__name__ for cls in type(diffusion_model).__mro__):
|
|
layout = _BLOCK_SWAP_ARCHITECTURES.get(type_name)
|
|
if layout is not None and hasattr(diffusion_model, layout["blocks"]):
|
|
return layout
|
|
raise TransformError(
|
|
f"block_swap does not support {type(diffusion_model).__name__} "
|
|
f"models; supported architectures are "
|
|
f"{sorted(_BLOCK_SWAP_ARCHITECTURES)}")
|
|
|
|
|
|
def _block_swap(patcher, blocks_to_swap: int, offload_img_emb: bool,
|
|
offload_txt_emb: bool, use_non_blocking: bool):
|
|
"""Keep the last N transformer blocks on the compute device, offload the rest.
|
|
|
|
Large video models do not fit in consumer VRAM. Offloading most blocks to
|
|
system RAM and moving them across on demand is how they are run at all, and
|
|
every video-model wrapper hand-rolls its own copy of it by reaching into
|
|
the model's module tree. The guest supplies only the four numbers a user
|
|
chose; core owns which modules those numbers refer to.
|
|
|
|
Placement happens in an ON_LOAD callback so it composes with ModelPatcher's
|
|
own offload and low-VRAM lifecycle rather than fighting it.
|
|
"""
|
|
from comfy import model_management
|
|
from comfy.patcher_extension import CallbacksMP
|
|
|
|
def swap_blocks(model, device_to, lowvram_model_memory,
|
|
force_patch_weights, full_load):
|
|
import gc
|
|
|
|
diffusion_model = model.model.diffusion_model
|
|
layout = _block_swap_layout(diffusion_model)
|
|
blocks = getattr(diffusion_model, layout["blocks"])
|
|
# Upstream hardcodes torch.device('cuda'), which raises outright on
|
|
# Apple Silicon and CPU-only hosts. Ask core for the active device;
|
|
# on CUDA this resolves to the same thing.
|
|
resident_device = model_management.get_torch_device()
|
|
offload_device = model.offload_device
|
|
|
|
# Clamped, so asking for 40 blocks of a 30-block model offloads 30
|
|
# rather than indexing past the end.
|
|
limit = min(int(blocks_to_swap), len(blocks) - 1)
|
|
for index, block in enumerate(blocks):
|
|
# `>` not `>=`, reproducing upstream's inclusive boundary: this
|
|
# offloads blocks 0..limit, one more than the label implies.
|
|
# Workflows are tuned against that, so it is preserved.
|
|
block.to(offload_device if index <= limit else resident_device)
|
|
|
|
for enabled, key in ((offload_txt_emb, "txt_emb"),
|
|
(offload_img_emb, "img_emb")):
|
|
attribute = layout.get(key)
|
|
module = (None if not enabled or attribute is None
|
|
else getattr(diffusion_model, attribute, None))
|
|
if module is not None:
|
|
module.to(offload_device, non_blocking=use_non_blocking)
|
|
|
|
model_management.soft_empty_cache()
|
|
gc.collect()
|
|
|
|
out = patcher.clone()
|
|
out.add_callback(CallbacksMP.ON_LOAD, swap_blocks)
|
|
return out
|
|
|
|
|
|
_ATTENTION_IMPLS = ("pytorch", "basic", "split", "sub_quad", "xformers",
|
|
"sage", "flash")
|
|
|
|
|
|
def _attention_impl(patcher, mode: str, allow_compile: bool):
|
|
"""Choose which of core's attention implementations this model uses.
|
|
|
|
Core already ships every one of these and already reads
|
|
``transformer_options["optimized_attention_override"]`` (attention.py:176).
|
|
What was missing was a way to pick one PER MODEL rather than per process —
|
|
which is exactly what the pack nodes were hand-rolling.
|
|
|
|
An implementation whose backing library is absent is refused HERE, by name,
|
|
rather than at the first attention call thirty seconds into a sample.
|
|
"""
|
|
from comfy.ldm.modules import attention as _attn
|
|
|
|
fn = getattr(_attn, f"attention_{mode}", None)
|
|
if fn is None: # pragma: no cover - guarded by the OneOf spec
|
|
raise TransformError(f"attention implementation {mode!r} is not built in")
|
|
if mode == "sage" and not getattr(_attn, "SAGE_ATTENTION_IS_AVAILABLE", False):
|
|
raise TransformError(
|
|
"sage attention is not installed on this host "
|
|
"(pip install sageattention)")
|
|
if mode == "flash" and not getattr(_attn, "FLASH_ATTENTION_IS_AVAILABLE", False):
|
|
raise TransformError(
|
|
"flash attention is not installed on this host "
|
|
"(pip install flash-attn)")
|
|
if mode == "xformers":
|
|
from comfy import model_management
|
|
|
|
if not model_management.xformers_enabled():
|
|
raise TransformError("xformers attention is not enabled on this host")
|
|
|
|
if not allow_compile:
|
|
import torch
|
|
|
|
fn = torch.compiler.disable()(fn)
|
|
|
|
def override(func, *args, **kwargs):
|
|
return fn(*args, **kwargs)
|
|
|
|
m = patcher.clone()
|
|
m.model_options.setdefault("transformer_options", {})
|
|
m.model_options["transformer_options"]["optimized_attention_override"] = override
|
|
return m
|
|
|
|
|
|
_SAGE_VARIANTS = (
|
|
"disabled",
|
|
"auto",
|
|
"sageattn_qk_int8_pv_fp16_cuda",
|
|
"sageattn_qk_int8_pv_fp16_triton",
|
|
"sageattn_qk_int8_pv_fp8_cuda",
|
|
"sageattn_qk_int8_pv_fp8_cuda++",
|
|
"sageattn3",
|
|
"sageattn3_per_block_mean",
|
|
)
|
|
|
|
|
|
def _sage_attention_variant(patcher, mode: str, allow_compile: bool):
|
|
if mode == "disabled":
|
|
return patcher
|
|
|
|
import torch
|
|
from comfy.ldm.modules.attention import attention_pytorch, wrap_attn
|
|
|
|
try:
|
|
if mode == "auto":
|
|
from sageattention import sageattn as implementation
|
|
|
|
def sage_func(q, k, v, is_causal=False, attn_mask=None,
|
|
tensor_layout="NHD"):
|
|
return implementation(
|
|
q, k, v, is_causal=is_causal, attn_mask=attn_mask,
|
|
tensor_layout=tensor_layout)
|
|
elif mode == "sageattn_qk_int8_pv_fp16_cuda":
|
|
from sageattention import (
|
|
sageattn_qk_int8_pv_fp16_cuda as implementation,
|
|
)
|
|
|
|
def sage_func(q, k, v, is_causal=False, attn_mask=None,
|
|
tensor_layout="NHD"):
|
|
return implementation(
|
|
q, k, v, is_causal=is_causal, attn_mask=attn_mask,
|
|
pv_accum_dtype="fp32", tensor_layout=tensor_layout)
|
|
elif mode == "sageattn_qk_int8_pv_fp16_triton":
|
|
from sageattention import (
|
|
sageattn_qk_int8_pv_fp16_triton as implementation,
|
|
)
|
|
|
|
def sage_func(q, k, v, is_causal=False, attn_mask=None,
|
|
tensor_layout="NHD"):
|
|
return implementation(
|
|
q, k, v, is_causal=is_causal, attn_mask=attn_mask,
|
|
tensor_layout=tensor_layout)
|
|
elif mode in (
|
|
"sageattn_qk_int8_pv_fp8_cuda",
|
|
"sageattn_qk_int8_pv_fp8_cuda++",
|
|
):
|
|
from sageattention import (
|
|
sageattn_qk_int8_pv_fp8_cuda as implementation,
|
|
)
|
|
accumulation = (
|
|
"fp32+fp16"
|
|
if mode.endswith("++")
|
|
else "fp32+fp32"
|
|
)
|
|
|
|
def sage_func(q, k, v, is_causal=False, attn_mask=None,
|
|
tensor_layout="NHD"):
|
|
return implementation(
|
|
q, k, v, is_causal=is_causal, attn_mask=attn_mask,
|
|
pv_accum_dtype=accumulation, tensor_layout=tensor_layout)
|
|
else:
|
|
from sageattn3 import sageattn3_blackwell as implementation
|
|
per_block_mean = mode == "sageattn3_per_block_mean"
|
|
|
|
def sage_func(q, k, v, is_causal=False, attn_mask=None,
|
|
tensor_layout="NHD"):
|
|
if tensor_layout == "NHD":
|
|
q, k, v = (value.transpose(1, 2)
|
|
for value in (q, k, v))
|
|
out = implementation(
|
|
q, k, v, is_causal=is_causal, attn_mask=attn_mask,
|
|
per_block_mean=per_block_mean)
|
|
return out.transpose(1, 2) if tensor_layout == "NHD" else out
|
|
except ImportError as error:
|
|
package = "sageattn3" if mode.startswith("sageattn3") else "sageattention"
|
|
raise TransformError(
|
|
f"{mode} requires the {package} package on this host") from error
|
|
|
|
if not allow_compile:
|
|
sage_func = torch.compiler.disable()(sage_func)
|
|
|
|
@wrap_attn
|
|
def attention_sage(q, k, v, heads, mask=None, attn_precision=None,
|
|
skip_reshape=False, skip_output_reshape=False, **kwargs):
|
|
if kwargs.get("low_precision_attention", True) is False:
|
|
return attention_pytorch(
|
|
q, k, v, heads, mask=mask, skip_reshape=skip_reshape,
|
|
skip_output_reshape=skip_output_reshape, **kwargs)
|
|
in_dtype = v.dtype
|
|
if any(value.dtype == torch.float32 for value in (q, k, v)):
|
|
q, k, v = (value.to(torch.float16) for value in (q, k, v))
|
|
if skip_reshape:
|
|
batch, _, _, dim_head = q.shape
|
|
tensor_layout = "HND"
|
|
else:
|
|
batch, _, dim_head = q.shape
|
|
dim_head //= heads
|
|
q, k, v = (
|
|
value.view(batch, -1, heads, dim_head)
|
|
for value in (q, k, v)
|
|
)
|
|
tensor_layout = "NHD"
|
|
if mask is not None:
|
|
if mask.ndim == 2:
|
|
mask = mask.unsqueeze(0)
|
|
if mask.ndim == 3:
|
|
mask = mask.unsqueeze(1)
|
|
sequence_dim = 2 if tensor_layout == "HND" else 1
|
|
if any(
|
|
(value.shape[sequence_dim] - 1) * value.stride(sequence_dim)
|
|
>= 2**31
|
|
for value in (q, k, v)
|
|
):
|
|
q, k, v = (value.contiguous() for value in (q, k, v))
|
|
out = sage_func(
|
|
q, k, v, attn_mask=mask, is_causal=False,
|
|
tensor_layout=tensor_layout).to(in_dtype)
|
|
if tensor_layout == "HND":
|
|
if not skip_output_reshape:
|
|
out = out.transpose(1, 2).reshape(
|
|
batch, -1, heads * dim_head)
|
|
elif skip_output_reshape:
|
|
out = out.transpose(1, 2)
|
|
else:
|
|
out = out.reshape(batch, -1, heads * dim_head)
|
|
return out
|
|
|
|
model = patcher.clone()
|
|
options = model.model_options.setdefault("transformer_options", {})
|
|
|
|
def override(func, *args, **kwargs):
|
|
return attention_sage.__wrapped__(*args, **kwargs)
|
|
|
|
options["optimized_attention_override"] = override
|
|
return model
|
|
|
|
|
|
def _strict_flash_attention(patcher, allow_compile: bool):
|
|
import torch
|
|
from comfy.ldm.modules.attention import wrap_attn
|
|
|
|
is_fa3 = False
|
|
try:
|
|
from flash_attn import flash_attn_func
|
|
except ImportError:
|
|
try:
|
|
from flash_attn_interface import flash_attn_func
|
|
is_fa3 = True
|
|
except ImportError as error:
|
|
raise TransformError(
|
|
"strict_flash_attention requires flash_attn (FA2) or "
|
|
"flash_attn_interface (FA3) on this host") from error
|
|
|
|
inference_dtype = (
|
|
patcher.model.get_dtype_inference()
|
|
if hasattr(patcher.model, "get_dtype_inference")
|
|
else torch.float16
|
|
)
|
|
cast_dtype = (
|
|
inference_dtype
|
|
if inference_dtype in (torch.float16, torch.bfloat16)
|
|
else torch.float16
|
|
)
|
|
|
|
def flash_func(q, k, v):
|
|
if is_fa3:
|
|
out = flash_attn_func(q, k, v, causal=False)
|
|
else:
|
|
out = flash_attn_func(
|
|
q, k, v, dropout_p=0.0, causal=False)
|
|
return out[0] if isinstance(out, tuple) else out
|
|
|
|
if not allow_compile:
|
|
flash_func = torch.compiler.disable()(flash_func)
|
|
if torch.cuda.is_available():
|
|
probe = torch.zeros(
|
|
1, 8, 2, 64, dtype=cast_dtype, device="cuda")
|
|
flash_func(probe, probe, probe)
|
|
|
|
@wrap_attn
|
|
def attention_flash(q, k, v, heads, mask=None, attn_precision=None,
|
|
skip_reshape=False, skip_output_reshape=False, **kwargs):
|
|
if mask is not None:
|
|
raise RuntimeError("Flash attention does not support attention masks")
|
|
in_dtype = v.dtype
|
|
if any(value.dtype == torch.float32 for value in (q, k, v)):
|
|
q, k, v = (value.to(cast_dtype) for value in (q, k, v))
|
|
if skip_reshape:
|
|
batch, _, _, dim_head = q.shape
|
|
q, k, v = (value.transpose(1, 2) for value in (q, k, v))
|
|
else:
|
|
batch, _, dim_head = q.shape
|
|
dim_head //= heads
|
|
q, k, v = (
|
|
value.view(batch, -1, heads, dim_head)
|
|
for value in (q, k, v)
|
|
)
|
|
out = flash_func(q, k, v).to(in_dtype)
|
|
if skip_output_reshape:
|
|
out = out.transpose(1, 2)
|
|
else:
|
|
out = out.reshape(batch, -1, heads * dim_head)
|
|
return out
|
|
|
|
model = patcher.clone()
|
|
options = model.model_options.setdefault("transformer_options", {})
|
|
|
|
def override(func, *args, **kwargs):
|
|
return attention_flash.__wrapped__(*args, **kwargs)
|
|
|
|
options["optimized_attention_override"] = override
|
|
return model
|
|
|
|
|
|
def _kohya_deep_shrink(
|
|
patcher, block_number: int, downscale_factor: float,
|
|
start_percent: float, end_percent: float,
|
|
downscale_after_skip: bool, downscale_method: str,
|
|
upscale_method: str,
|
|
):
|
|
"""Expose core's canonical PatchModelAddDownscale implementation."""
|
|
if start_percent > end_percent:
|
|
raise TransformError(
|
|
"kohya_deep_shrink.start_percent must not exceed end_percent")
|
|
from comfy_extras.nodes_model_downscale import PatchModelAddDownscale
|
|
|
|
output = PatchModelAddDownscale.execute(
|
|
patcher,
|
|
block_number,
|
|
downscale_factor,
|
|
start_percent,
|
|
end_percent,
|
|
downscale_after_skip,
|
|
downscale_method,
|
|
upscale_method,
|
|
)
|
|
return output.result[0]
|
|
|
|
|
|
def _nabla_sparse_attention(
|
|
patcher, latent, window_time: int, window_width: int,
|
|
window_height: int, sparsity: float, compile_attention: bool,
|
|
):
|
|
import math
|
|
import torch
|
|
from comfy import model_management
|
|
from comfy.ldm.modules.attention import optimized_attention
|
|
|
|
try:
|
|
from torch.nn.attention.flex_attention import BlockMask, flex_attention
|
|
except ImportError as error:
|
|
raise TransformError(
|
|
"nabla_sparse_attention requires torch flex_attention") from error
|
|
|
|
samples = latent.get("samples") if isinstance(latent, dict) else None
|
|
if not isinstance(samples, torch.Tensor) or samples.ndim != 5:
|
|
raise TransformError(
|
|
"nabla_sparse_attention.latent must contain a 5D samples tensor")
|
|
|
|
_, _, frames, height, width = samples.shape
|
|
frames, height, width = frames, height // 16, width // 16
|
|
max_axis = torch.tensor(
|
|
[frames, height, width], dtype=torch.float32).amax()
|
|
axis = torch.arange(
|
|
0, max_axis, 1, dtype=torch.int16,
|
|
device=model_management.get_torch_device())
|
|
distances = (axis.unsqueeze(1) - axis.unsqueeze(0)).abs()
|
|
temporal = distances[:frames, :frames].flatten() <= window_time // 2
|
|
vertical = distances[:height, :height].flatten() <= window_height // 2
|
|
horizontal = distances[:width, :width].flatten() <= window_width // 2
|
|
spatial = (
|
|
vertical.unsqueeze(1) * horizontal.unsqueeze(0)
|
|
).reshape(height, height, width, width).transpose(1, 2).flatten()
|
|
static_mask = (
|
|
temporal.unsqueeze(1) * spatial.unsqueeze(0)
|
|
).reshape(
|
|
frames, frames, height * width, height * width
|
|
).transpose(1, 2).reshape(
|
|
frames * height * width, frames * height * width
|
|
).unsqueeze_(0).unsqueeze_(0)
|
|
|
|
class NablaAttention:
|
|
def __call__(self, q, k, v, heads, **kwargs):
|
|
if q.shape[-2] < 3000 or k.shape[-2] < 3000:
|
|
return optimized_attention(q, k, v, heads, **kwargs)
|
|
block_mask = self._block_mask(q, k)
|
|
return flex_attention(
|
|
q, k, v, block_mask=block_mask,
|
|
).transpose(1, 2).contiguous().flatten(-2, -1)
|
|
|
|
@staticmethod
|
|
def _block_mask(q, k):
|
|
block_size = 64
|
|
batch, heads, sequence, dim = q.shape
|
|
blocks = sequence // block_size
|
|
q_average = q.reshape(
|
|
batch, heads, blocks, block_size, dim).mean(-2)
|
|
k_average = k.reshape(
|
|
batch, heads, blocks, block_size, dim,
|
|
).mean(-2).transpose(-2, -1)
|
|
attention_map = torch.softmax(
|
|
(q_average @ k_average) / math.sqrt(dim), dim=-1)
|
|
values, indices = attention_map.sort(-1)
|
|
cumulative = values.cumsum_(-1)
|
|
mask = (cumulative >= 1 - sparsity).int()
|
|
mask = mask.gather(-1, indices.argsort(-1))
|
|
mask = torch.logical_or(mask, static_mask)
|
|
kv_blocks = mask.sum(-1).to(torch.int32)
|
|
kv_indices = mask.argsort(
|
|
dim=-1, descending=True).to(torch.int32)
|
|
return BlockMask.from_kv_blocks(
|
|
torch.zeros_like(kv_blocks), kv_indices,
|
|
kv_blocks, kv_indices, BLOCK_SIZE=block_size,
|
|
mask_mod=None)
|
|
|
|
attention = NablaAttention()
|
|
|
|
def override(func, *args, **kwargs):
|
|
return attention(*args, **kwargs)
|
|
|
|
if compile_attention:
|
|
override = torch.compile(
|
|
override, mode="max-autotune-no-cudagraphs", dynamic=True)
|
|
|
|
model = patcher.clone()
|
|
options = model.model_options.setdefault("transformer_options", {})
|
|
options["optimized_attention_override"] = override
|
|
return model
|
|
|
|
|
|
def _feta_score(query, key, head_dim: int, num_frames: int, weight: float):
|
|
import torch
|
|
|
|
attention = (query * (head_dim ** -0.5)) @ key.transpose(-2, -1)
|
|
attention = attention.to(torch.float32).softmax(dim=-1)
|
|
attention = attention.reshape(-1, num_frames, num_frames)
|
|
diagonal = torch.eye(
|
|
num_frames, device=attention.device, dtype=torch.bool,
|
|
).unsqueeze(0).expand(attention.shape[0], -1, -1)
|
|
off_diagonal = attention.masked_fill(diagonal, 0)
|
|
count = num_frames * num_frames - num_frames
|
|
mean_scores = off_diagonal.sum(dim=(1, 2)) / count
|
|
return (mean_scores.mean() * (num_frames + weight)).clamp(min=1)
|
|
|
|
|
|
def _feta_scores(query, key, num_frames: int, weight: float,
|
|
num_heads: int = 12):
|
|
from einops import rearrange
|
|
|
|
if query.ndim == 4:
|
|
batch, sequence, num_heads, head_dim = query.shape
|
|
elif query.ndim == 3:
|
|
batch, sequence, hidden_dim = query.shape
|
|
head_dim = hidden_dim // num_heads
|
|
query = query.view(batch, sequence, num_heads, head_dim)
|
|
key = key.view(batch, sequence, num_heads, head_dim)
|
|
else:
|
|
raise TransformError(
|
|
f"enhance_a_video attention must be 3D or 4D, got {query.ndim}D")
|
|
spatial = sequence // num_frames
|
|
query_image = rearrange(
|
|
query, "B (T S) N C -> (B S) N T C",
|
|
T=num_frames, S=spatial, N=num_heads, C=head_dim)
|
|
key_image = rearrange(
|
|
key, "B (T S) N C -> (B S) N T C",
|
|
T=num_frames, S=spatial, N=num_heads, C=head_dim)
|
|
return _feta_score(query_image, key_image, head_dim, num_frames, weight)
|
|
|
|
|
|
def _wan_enhance_forward(module, x, freqs, num_frames: int, weight: float,
|
|
transformer_options=None):
|
|
from comfy.ldm.flux.math import apply_rope
|
|
from comfy.ldm.modules.attention import optimized_attention
|
|
|
|
transformer_options = transformer_options or {}
|
|
batch, sequence = x.shape[:2]
|
|
heads, head_dim = module.num_heads, module.head_dim
|
|
q = module.norm_q(module.q(x)).view(batch, sequence, heads, head_dim)
|
|
k = module.norm_k(module.k(x)).view(batch, sequence, heads, head_dim)
|
|
v = module.v(x).view(batch, sequence, heads * head_dim)
|
|
q, k = apply_rope(q, k, freqs)
|
|
score = _feta_scores(q, k, num_frames, weight)
|
|
out = optimized_attention(
|
|
q.view(batch, sequence, heads * head_dim),
|
|
k.view(batch, sequence, heads * head_dim),
|
|
v, heads=heads, transformer_options=transformer_options)
|
|
return module.o(out) * score
|
|
|
|
|
|
def _ltx_enhance_forward(
|
|
module, x, num_frames: int, weight: float, context=None, mask=None,
|
|
pe=None, k_pe=None, transformer_options=None,
|
|
):
|
|
from comfy.ldm.modules.attention import (
|
|
optimized_attention, optimized_attention_masked,
|
|
)
|
|
|
|
transformer_options = transformer_options or {}
|
|
q = module.to_q(x)
|
|
context = x if context is None else context
|
|
k = module.to_k(context)
|
|
v = module.to_v(context)
|
|
q = module.q_norm(q)
|
|
k = module.k_norm(k)
|
|
if pe is not None:
|
|
try:
|
|
from comfy.ldm.lightricks.model import apply_rotary_emb
|
|
except ImportError as error:
|
|
raise TransformError(
|
|
"LTX Enhance-A-Video needs core Lightricks rotary support") from error
|
|
q = apply_rotary_emb(q, pe)
|
|
k = apply_rotary_emb(k, pe if k_pe is None else k_pe)
|
|
score = _feta_scores(q, k, num_frames, weight, module.heads)
|
|
try:
|
|
from comfy.ldm.lightricks.model import (
|
|
GuideAttentionMask, _attention_with_guide_mask,
|
|
)
|
|
except ImportError:
|
|
GuideAttentionMask = None
|
|
_attention_with_guide_mask = None
|
|
if mask is None:
|
|
out = optimized_attention(
|
|
q, k, v, module.heads, attn_precision=module.attn_precision,
|
|
transformer_options=transformer_options)
|
|
elif GuideAttentionMask is not None and isinstance(mask, GuideAttentionMask):
|
|
out = _attention_with_guide_mask(
|
|
q, k, v, module.heads, mask,
|
|
attn_precision=module.attn_precision,
|
|
transformer_options=transformer_options)
|
|
else:
|
|
out = optimized_attention_masked(
|
|
q, k, v, module.heads, mask,
|
|
attn_precision=module.attn_precision,
|
|
transformer_options=transformer_options)
|
|
if module.to_gate_logits is not None:
|
|
import torch
|
|
|
|
gate_logits = module.to_gate_logits(x)
|
|
batch, sequence, _ = out.shape
|
|
out = out.view(batch, sequence, module.heads, module.dim_head)
|
|
gates = 2.0 * torch.sigmoid(gate_logits)
|
|
out = (out * gates.unsqueeze(-1)).view(
|
|
batch, sequence, module.heads * module.dim_head)
|
|
return module.to_out(out) * score
|
|
|
|
|
|
def _enhance_a_video(patcher, latent, architecture: str, weight: float):
|
|
import types
|
|
import torch
|
|
|
|
if weight == 0:
|
|
return patcher
|
|
samples = latent.get("samples") if isinstance(latent, dict) else None
|
|
if not isinstance(samples, torch.Tensor) or samples.ndim != 5:
|
|
raise TransformError(
|
|
"enhance_a_video.latent must contain a 5D samples tensor")
|
|
num_frames = samples.shape[2]
|
|
model = patcher.clone()
|
|
options = model.model_options.setdefault("transformer_options", {})
|
|
options["enhance_weight"] = weight
|
|
diffusion_model = model.get_model_object("diffusion_model")
|
|
|
|
if architecture == "wan":
|
|
blocks = getattr(diffusion_model, "blocks", None)
|
|
if blocks is None:
|
|
raise TransformError(
|
|
"Wan Enhance-A-Video needs diffusion_model.blocks")
|
|
compile_settings = getattr(patcher.model, "compile_settings", None)
|
|
for index, block in enumerate(blocks):
|
|
attention = block.self_attn
|
|
|
|
def forward(self_module, x, freqs, transformer_options=None,
|
|
_frames=num_frames, _weight=weight):
|
|
return _wan_enhance_forward(
|
|
self_module, x, freqs, _frames, _weight,
|
|
transformer_options)
|
|
|
|
patched = types.MethodType(forward, attention)
|
|
if compile_settings is not None:
|
|
patched = torch.compile(
|
|
patched, mode=compile_settings["mode"],
|
|
dynamic=compile_settings["dynamic"],
|
|
fullgraph=compile_settings["fullgraph"],
|
|
backend=compile_settings["backend"])
|
|
model.add_object_patch(
|
|
f"diffusion_model.blocks.{index}.self_attn.forward", patched)
|
|
return model
|
|
|
|
if architecture != "ltx":
|
|
raise TransformError(
|
|
f"unknown Enhance-A-Video architecture {architecture!r}")
|
|
blocks = getattr(diffusion_model, "transformer_blocks", None)
|
|
if blocks is None:
|
|
raise TransformError(
|
|
"LTX Enhance-A-Video needs diffusion_model.transformer_blocks")
|
|
for index, block in enumerate(blocks):
|
|
attention = block.attn1
|
|
|
|
def forward(self_module, x, context=None, mask=None, pe=None, k_pe=None,
|
|
transformer_options=None, _frames=num_frames,
|
|
_weight=weight):
|
|
return _ltx_enhance_forward(
|
|
self_module, x, _frames, _weight, context, mask, pe, k_pe,
|
|
transformer_options)
|
|
|
|
model.add_object_patch(
|
|
f"diffusion_model.transformer_blocks.{index}.attn1.forward",
|
|
types.MethodType(forward, attention))
|
|
return model
|
|
|
|
|
|
def _wan_nag_compute(module, query, context, transformer_options):
|
|
from comfy.ldm.modules.attention import optimized_attention
|
|
|
|
key = module.norm_k(module.k(context))
|
|
value = module.v(context)
|
|
return optimized_attention(
|
|
query, key, value, heads=module.num_heads,
|
|
transformer_options=transformer_options).flatten(2)
|
|
|
|
|
|
def _wan_nag_guidance(positive, negative, scale: float, alpha: float,
|
|
tau: float, inplace: bool):
|
|
import torch
|
|
|
|
if inplace:
|
|
guidance = negative.mul_(scale - 1).neg_().add_(positive, alpha=scale)
|
|
else:
|
|
guidance = negative * (scale - 1)
|
|
guidance = (positive * scale).sub_(guidance)
|
|
norm_positive = torch.norm(positive, p=1, dim=-1, keepdim=True)
|
|
norm_guidance = torch.norm(guidance, p=1, dim=-1, keepdim=True)
|
|
ratio = norm_guidance / norm_positive
|
|
torch.nan_to_num_(ratio, nan=10.0)
|
|
mask = ratio > tau
|
|
adjustment = (norm_positive * tau) / (norm_guidance + 1e-7)
|
|
guidance.mul_(torch.where(mask, adjustment, 1.0))
|
|
if inplace:
|
|
return guidance.sub_(positive).mul_(alpha).add_(positive)
|
|
guidance.mul_(alpha)
|
|
return guidance.add_(positive * (1 - alpha))
|
|
|
|
|
|
def _wan_nag_forward(
|
|
module, x, context, nag_context, nag_scale: float, nag_alpha: float,
|
|
nag_tau: float, input_type: str, inplace: bool,
|
|
transformer_options=None,
|
|
):
|
|
import torch
|
|
from comfy.ldm.modules.attention import optimized_attention
|
|
|
|
transformer_options = transformer_options or {}
|
|
if input_type == "default":
|
|
if context.shape[0] == 1:
|
|
x_positive, context_positive = x, context
|
|
x_negative = context_negative = None
|
|
else:
|
|
x_positive, x_negative = torch.chunk(x, 2, dim=0)
|
|
context_positive, context_negative = torch.chunk(context, 2, dim=0)
|
|
else:
|
|
x_positive, context_positive = x, context
|
|
x_negative = context_negative = None
|
|
query_positive = module.norm_q(module.q(x_positive))
|
|
if input_type == "batch":
|
|
nag_context = nag_context.repeat(x_positive.shape[0], 1, 1)
|
|
positive = _wan_nag_compute(
|
|
module, query_positive, context_positive, transformer_options)
|
|
negative = _wan_nag_compute(
|
|
module, query_positive, nag_context, transformer_options)
|
|
positive_output = _wan_nag_guidance(
|
|
positive, negative, nag_scale, nag_alpha, nag_tau, inplace)
|
|
if x_negative is not None and context_negative is not None:
|
|
query_negative = module.norm_q(module.q(x_negative))
|
|
key_negative = module.norm_k(module.k(context_negative))
|
|
value_negative = module.v(context_negative)
|
|
negative_output = optimized_attention(
|
|
query_negative, key_negative, value_negative,
|
|
heads=module.num_heads,
|
|
transformer_options=transformer_options)
|
|
out = torch.cat([positive_output, negative_output], dim=0)
|
|
else:
|
|
out = positive_output
|
|
return module.o(out)
|
|
|
|
|
|
def _wan_i2v_nag_forward(
|
|
module, x, context, context_img_len: int, nag_context,
|
|
nag_scale: float, nag_alpha: float, nag_tau: float, inplace: bool,
|
|
transformer_options=None,
|
|
):
|
|
import torch
|
|
from comfy.ldm.modules.attention import optimized_attention
|
|
|
|
transformer_options = transformer_options or {}
|
|
context_image = context[:, :context_img_len]
|
|
context = context[:, context_img_len:]
|
|
query_image = module.norm_q(module.q(x))
|
|
key_image = module.norm_k_img(module.k_img(context_image))
|
|
value_image = module.v_img(context_image)
|
|
image_output = optimized_attention(
|
|
query_image, key_image, value_image, heads=module.num_heads,
|
|
transformer_options=transformer_options)
|
|
if context.shape[0] == 2:
|
|
x, x_real_negative = torch.chunk(x, 2, dim=0)
|
|
context_positive, context_negative = torch.chunk(context, 2, dim=0)
|
|
else:
|
|
context_positive = context
|
|
context_negative = None
|
|
query = module.norm_q(module.q(x))
|
|
positive = _wan_nag_compute(
|
|
module, query, context_positive, transformer_options)
|
|
negative = _wan_nag_compute(
|
|
module, query, nag_context, transformer_options)
|
|
out = _wan_nag_guidance(
|
|
positive, negative, nag_scale, nag_alpha, nag_tau, inplace)
|
|
if context_negative is not None:
|
|
query_negative = module.norm_q(module.q(x_real_negative))
|
|
key_negative = module.norm_k(module.k(context_negative))
|
|
value_negative = module.v(context_negative)
|
|
x_real_negative = optimized_attention(
|
|
query_negative, key_negative, value_negative,
|
|
heads=module.num_heads,
|
|
transformer_options=transformer_options)
|
|
out = torch.cat([out, x_real_negative], dim=0)
|
|
return module.o(out + image_output)
|
|
|
|
|
|
def _wan_video_nag(
|
|
patcher, conditioning, nag_scale: float, nag_alpha: float,
|
|
nag_tau: float, input_type: str, inplace: bool,
|
|
):
|
|
import torch
|
|
import types
|
|
from comfy import model_management
|
|
|
|
if nag_scale == 0:
|
|
return patcher
|
|
if (
|
|
not isinstance(conditioning, (list, tuple))
|
|
or not conditioning
|
|
or not isinstance(conditioning[0], (list, tuple))
|
|
or not conditioning[0]
|
|
or not isinstance(conditioning[0][0], torch.Tensor)
|
|
):
|
|
raise TransformError(
|
|
"wan_video_nag.conditioning must contain an embedding tensor")
|
|
model = patcher.clone()
|
|
diffusion_model = model.get_model_object("diffusion_model")
|
|
text_embedding = getattr(diffusion_model, "text_embedding", None)
|
|
blocks = getattr(diffusion_model, "blocks", None)
|
|
if text_embedding is None or blocks is None:
|
|
raise TransformError(
|
|
"wan_video_nag needs Wan text_embedding and blocks")
|
|
device = model_management.get_torch_device()
|
|
dtype = model_management.unet_dtype()
|
|
text_embedding.to(device)
|
|
nag_context = text_embedding(conditioning[0][0].to(device, dtype))
|
|
for index, block in enumerate(blocks):
|
|
attention = block.cross_attn
|
|
is_i2v = hasattr(attention, "k_img")
|
|
if is_i2v:
|
|
def forward(
|
|
self_module, x, context, context_img_len,
|
|
transformer_options=None, _context=nag_context,
|
|
_scale=nag_scale, _alpha=nag_alpha, _tau=nag_tau,
|
|
_inplace=inplace,
|
|
):
|
|
return _wan_i2v_nag_forward(
|
|
self_module, x, context, context_img_len, _context,
|
|
_scale, _alpha, _tau, _inplace, transformer_options)
|
|
else:
|
|
def forward(
|
|
self_module, x, context, transformer_options=None,
|
|
_context=nag_context, _scale=nag_scale, _alpha=nag_alpha,
|
|
_tau=nag_tau, _input_type=input_type, _inplace=inplace,
|
|
):
|
|
return _wan_nag_forward(
|
|
self_module, x, context, _context, _scale, _alpha, _tau,
|
|
_input_type, _inplace, transformer_options)
|
|
model.add_object_patch(
|
|
f"diffusion_model.blocks.{index}.cross_attn.forward",
|
|
types.MethodType(forward, attention))
|
|
return model
|
|
|
|
|
|
def _krea2_attention_forward(
|
|
module, x, freqs=None, mask=None, transformer_options=None,
|
|
):
|
|
import torch
|
|
from einops import rearrange
|
|
from comfy.ldm.flux.math import apply_rope
|
|
from comfy.ldm.modules.attention import (
|
|
attention_pytorch, optimized_attention,
|
|
)
|
|
|
|
transformer_options = transformer_options or {}
|
|
weights = transformer_options.get("krea2_token_weights")
|
|
q, k, v, gate = module.wq(x), module.wk(x), module.wv(x), module.gate(x)
|
|
q = rearrange(q, "B L (H D) -> B H L D", H=module.heads)
|
|
k = rearrange(k, "B L (H D) -> B H L D", H=module.kvheads)
|
|
v = rearrange(v, "B L (H D) -> B H L D", H=module.kvheads)
|
|
if weights:
|
|
v = v.clone()
|
|
for position, value_factor, _ in weights:
|
|
if value_factor != 1.0 and position < v.shape[2]:
|
|
v[:, :, position] = v[:, :, position] * value_factor
|
|
q, k = module.qknorm(q, k)
|
|
if freqs is not None:
|
|
q, k = apply_rope(q, k, freqs)
|
|
if module.kvheads != module.heads:
|
|
repeat = module.heads // module.kvheads
|
|
k = k.repeat_interleave(repeat, dim=1)
|
|
v = v.repeat_interleave(repeat, dim=1)
|
|
bias = None
|
|
if weights and any(key_bias != 0.0 for _, _, key_bias in weights):
|
|
bias = q.new_zeros(1, k.shape[2])
|
|
for position, _, key_bias in weights:
|
|
if key_bias != 0.0 and position < bias.shape[1]:
|
|
bias[:, position] = key_bias
|
|
if bias is not None:
|
|
out = attention_pytorch(
|
|
q, k, v, module.heads, mask=bias, skip_reshape=True)
|
|
else:
|
|
out = optimized_attention(
|
|
q, k, v, module.heads, mask=mask, skip_reshape=True,
|
|
transformer_options=transformer_options)
|
|
return module.wo(out * torch.sigmoid(gate))
|
|
|
|
|
|
def _krea2_token_weights(patcher, weights):
|
|
import types
|
|
|
|
if not weights:
|
|
return patcher
|
|
model = patcher.clone()
|
|
diffusion_model = model.get_model_object("diffusion_model")
|
|
blocks = getattr(diffusion_model, "blocks", None)
|
|
if blocks is None or any(not hasattr(block, "attn") for block in blocks):
|
|
raise TransformError(
|
|
"krea2_token_weights needs Krea2 diffusion blocks with attention")
|
|
options = dict(model.model_options.get("transformer_options", {}))
|
|
options["krea2_token_weights"] = list(weights)
|
|
model.model_options["transformer_options"] = options
|
|
for index, block in enumerate(blocks):
|
|
attention = block.attn
|
|
|
|
def forward(
|
|
self_module, x, freqs=None, mask=None, transformer_options=None,
|
|
):
|
|
return _krea2_attention_forward(
|
|
self_module, x, freqs, mask, transformer_options)
|
|
|
|
model.add_object_patch(
|
|
f"diffusion_model.blocks.{index}.attn.forward",
|
|
types.MethodType(forward, attention))
|
|
return model
|
|
|
|
|
|
def _ltx2_audio_normalization(patcher, factors):
|
|
from comfy.patcher_extension import WrappersMP
|
|
|
|
def wrapper(
|
|
executor, noise, latent_image, sampler, sigmas, denoise_mask,
|
|
callback, disable_pbar, seed, latent_shapes,
|
|
):
|
|
import latent_preview
|
|
import comfy.utils
|
|
|
|
guider = executor.class_obj
|
|
ltxav = guider.model_patcher.model.diffusion_model
|
|
x0_output = {}
|
|
total_steps = sigmas.shape[-1] - 1
|
|
progress = comfy.utils.ProgressBar(total_steps)
|
|
completed = 0
|
|
previewer = latent_preview.get_previewer(
|
|
guider.model_patcher.load_device,
|
|
guider.model_patcher.model.latent_format)
|
|
|
|
def sampling_callback(step, x0, x, callback_total_steps):
|
|
nonlocal completed
|
|
x0_output["x0"] = x0
|
|
preview_bytes = None
|
|
if previewer:
|
|
preview_bytes = previewer.decode_latent_to_preview_image(
|
|
"JPEG", x0)
|
|
completed += 1
|
|
progress.update_absolute(
|
|
completed, total_steps, preview_bytes)
|
|
|
|
active_factors = list(factors)
|
|
if len(active_factors) < total_steps:
|
|
active_factors.extend(
|
|
[active_factors[-1]] * (total_steps - len(active_factors)))
|
|
split_indices = [
|
|
index + 1 for index, factor in enumerate(active_factors)
|
|
if factor != 1.0
|
|
]
|
|
if split_indices:
|
|
chunks = []
|
|
previous = 0
|
|
for index in sorted(set(split_indices)):
|
|
if previous < index:
|
|
chunks.append(sigmas[previous:index + 1])
|
|
previous = index
|
|
if previous < len(sigmas):
|
|
chunks.append(sigmas[previous:])
|
|
else:
|
|
chunks = [sigmas]
|
|
|
|
completed_steps = 0
|
|
for sigma_chunk in chunks:
|
|
completed_steps += len(sigma_chunk) - 1
|
|
latent_image = executor(
|
|
noise, latent_image, sampler, sigma_chunk, denoise_mask,
|
|
sampling_callback, disable_pbar, seed,
|
|
latent_shapes=latent_shapes)
|
|
if "x0" in x0_output:
|
|
latent_image = guider.model_patcher.model.process_latent_out(
|
|
x0_output["x0"])
|
|
if completed_steps - 1 < len(active_factors):
|
|
unpacked = comfy.utils.unpack_latents(
|
|
latent_image, latent_shapes)
|
|
video, audio = ltxav.separate_audio_and_video_latents(
|
|
unpacked, None)
|
|
if denoise_mask is not None:
|
|
unpacked_mask = comfy.utils.unpack_latents(
|
|
denoise_mask, latent_shapes)
|
|
audio_mask = ltxav.separate_audio_and_video_latents(
|
|
unpacked_mask, None)[1]
|
|
audio = (
|
|
audio * audio_mask * active_factors[completed_steps - 1]
|
|
+ audio * (1 - audio_mask)
|
|
)
|
|
else:
|
|
audio = audio * active_factors[completed_steps - 1]
|
|
latent_image = comfy.utils.pack_latents(
|
|
ltxav.recombine_audio_and_video_latents(video, audio))[0]
|
|
return latent_image
|
|
|
|
model = patcher.clone()
|
|
model.add_wrapper_with_key(
|
|
WrappersMP.OUTER_SAMPLE,
|
|
"ltx2_audio_normalization",
|
|
wrapper,
|
|
)
|
|
return model
|
|
|
|
|
|
def _ltx_nag_compute(
|
|
module, query, context, transformer_options, mask=None,
|
|
):
|
|
from comfy.ldm.modules.attention import (
|
|
attention_pytorch, optimized_attention,
|
|
)
|
|
|
|
key = module.k_norm(module.to_k(context)).to(query.dtype)
|
|
value = module.to_v(context).to(query.dtype)
|
|
if mask is None:
|
|
out = optimized_attention(
|
|
query, key, value, heads=module.heads,
|
|
attn_precision=module.attn_precision,
|
|
transformer_options=transformer_options)
|
|
else:
|
|
out = attention_pytorch(
|
|
query, key, value, heads=module.heads, mask=mask,
|
|
attn_precision=module.attn_precision,
|
|
_inside_attn_wrapper=True,
|
|
transformer_options=transformer_options)
|
|
return out.flatten(2)
|
|
|
|
|
|
def _ltx_nag_forward(
|
|
module, x, context, nag_context, nag_scale: float, nag_alpha: float,
|
|
nag_tau: float, inplace: bool, mask=None, transformer_options=None,
|
|
):
|
|
import torch
|
|
from comfy.ldm.modules.attention import optimized_attention
|
|
|
|
transformer_options = transformer_options or {}
|
|
if mask is None:
|
|
mask_provider = transformer_options.get("promptrelay_mask_fn")
|
|
if mask_provider is not None:
|
|
mask = mask_provider(
|
|
x.shape[1], context.shape[1], x.dtype, x.device,
|
|
transformer_options)
|
|
if context.shape[0] == 1:
|
|
x_positive, context_positive = x, context
|
|
x_negative = context_negative = None
|
|
else:
|
|
x_positive, x_negative = torch.chunk(x, 2, dim=0)
|
|
context_positive, context_negative = torch.chunk(context, 2, dim=0)
|
|
query_positive = module.q_norm(module.to_q(x_positive))
|
|
positive = _ltx_nag_compute(
|
|
module, query_positive, context_positive,
|
|
transformer_options, mask)
|
|
negative = _ltx_nag_compute(
|
|
module, query_positive, nag_context, transformer_options)
|
|
positive_output = _wan_nag_guidance(
|
|
positive, negative, nag_scale, nag_alpha, nag_tau, inplace)
|
|
if x_negative is not None and context_negative is not None:
|
|
query_negative = module.q_norm(module.to_q(x_negative))
|
|
key_negative = module.k_norm(module.to_k(context_negative))
|
|
value_negative = module.to_v(context_negative)
|
|
negative_output = optimized_attention(
|
|
query_negative, key_negative, value_negative,
|
|
heads=module.heads, attn_precision=module.attn_precision,
|
|
transformer_options=transformer_options)
|
|
out = torch.cat([positive_output, negative_output], dim=0)
|
|
else:
|
|
out = positive_output
|
|
if module.to_gate_logits is not None:
|
|
gate_logits = module.to_gate_logits(x)
|
|
batch, sequence, _ = out.shape
|
|
out = out.view(batch, sequence, module.heads, module.dim_head)
|
|
gates = 2.0 * torch.sigmoid(gate_logits)
|
|
out = (out * gates.unsqueeze(-1)).view(
|
|
batch, sequence, module.heads * module.dim_head)
|
|
return module.to_out(out)
|
|
|
|
|
|
def _ltx2_nag(
|
|
patcher, nag_scale: float, nag_alpha: float, nag_tau: float,
|
|
video_conditioning, audio_conditioning, inplace: bool,
|
|
):
|
|
import torch
|
|
import types
|
|
from comfy import model_management
|
|
|
|
if nag_scale == 0:
|
|
return patcher
|
|
for name, conditioning in (
|
|
("video_conditioning", video_conditioning),
|
|
("audio_conditioning", audio_conditioning),
|
|
):
|
|
if conditioning is not None and (
|
|
not isinstance(conditioning, (list, tuple))
|
|
or not conditioning
|
|
or not isinstance(conditioning[0], (list, tuple))
|
|
or not conditioning[0]
|
|
or not isinstance(conditioning[0][0], torch.Tensor)
|
|
):
|
|
raise TransformError(
|
|
f"ltx2_nag.{name} must contain an embedding tensor")
|
|
|
|
model = patcher.clone()
|
|
diffusion_model = model.get_model_object("diffusion_model")
|
|
blocks = getattr(diffusion_model, "transformer_blocks", None)
|
|
if blocks is None:
|
|
raise TransformError("ltx2_nag needs LTX2 transformer blocks")
|
|
dtype = getattr(patcher.model, "manual_cast_dtype", None)
|
|
if dtype is None:
|
|
dtype = diffusion_model.dtype
|
|
device = model_management.get_torch_device()
|
|
offload_device = model_management.unet_offload_device()
|
|
|
|
def project(conditioning, *, audio: bool):
|
|
context = conditioning[0][0].to(device, dtype)
|
|
video_width = getattr(diffusion_model, "cross_attention_dim", None)
|
|
audio_width = diffusion_model.audio_cross_attention_dim
|
|
if video_width is not None and context.shape[-1] == video_width + audio_width:
|
|
context = context[:, :, video_width:] if audio else context[:, :, :video_width]
|
|
if (
|
|
diffusion_model.caption_proj_before_connector
|
|
and diffusion_model.caption_projection_first_linear
|
|
):
|
|
projection = (
|
|
diffusion_model.audio_caption_projection
|
|
if audio else diffusion_model.caption_projection
|
|
)
|
|
projection.to(device)
|
|
try:
|
|
context = projection(context)
|
|
finally:
|
|
projection.to(offload_device)
|
|
connector_name = (
|
|
"audio_embeddings_connector" if audio
|
|
else "video_embeddings_connector"
|
|
)
|
|
if hasattr(diffusion_model, connector_name):
|
|
connector = getattr(diffusion_model, connector_name)
|
|
connector.to(device)
|
|
try:
|
|
context = connector(context)[0]
|
|
finally:
|
|
connector.to(offload_device)
|
|
width = (
|
|
diffusion_model.audio_inner_dim if audio
|
|
else diffusion_model.inner_dim
|
|
)
|
|
return context.view(1, -1, width)
|
|
|
|
contexts = (
|
|
("attn2", project(video_conditioning, audio=False))
|
|
if video_conditioning is not None else None,
|
|
("audio_attn2", project(audio_conditioning, audio=True))
|
|
if (
|
|
audio_conditioning is not None
|
|
and diffusion_model.audio_caption_projection is not None
|
|
) else None,
|
|
)
|
|
for target in contexts:
|
|
if target is None:
|
|
continue
|
|
attribute, nag_context = target
|
|
for index, block in enumerate(blocks):
|
|
attention = getattr(block, attribute)
|
|
|
|
def forward(
|
|
self_module, x, context, mask=None, transformer_options=None,
|
|
_nag_context=nag_context, _scale=nag_scale,
|
|
_alpha=nag_alpha, _tau=nag_tau, _inplace=inplace,
|
|
):
|
|
return _ltx_nag_forward(
|
|
self_module, x, context, _nag_context, _scale, _alpha,
|
|
_tau, _inplace, mask, transformer_options)
|
|
|
|
model.add_object_patch(
|
|
f"diffusion_model.transformer_blocks.{index}."
|
|
f"{attribute}.forward",
|
|
types.MethodType(forward, attention))
|
|
return model
|
|
|
|
|
|
def _ideogram4_optimizations(
|
|
patcher, chunk_ffn: bool, ffn_chunks: int,
|
|
ffn_seq_threshold: int, bf16_rope: bool,
|
|
):
|
|
import types
|
|
|
|
import torch
|
|
from comfy.ldm.lumina.model import FeedForward
|
|
from comfy.ldm.modules.attention import optimized_attention_masked
|
|
|
|
if not chunk_ffn and not bf16_rope:
|
|
return patcher
|
|
model = patcher.clone()
|
|
diffusion_model = model.get_model_object("diffusion_model")
|
|
layers = getattr(diffusion_model, "layers", None)
|
|
if (
|
|
not layers
|
|
or not hasattr(layers[0], "feed_forward")
|
|
or not hasattr(layers[0], "attention")
|
|
):
|
|
return patcher
|
|
|
|
def ffn_forward(module, value, *, chunks, threshold):
|
|
if value.shape[1] > threshold and chunks > 1:
|
|
outputs = [
|
|
FeedForward.forward(module, part)
|
|
for part in torch.chunk(value, chunks, dim=1)
|
|
]
|
|
return torch.cat(outputs, dim=1)
|
|
return FeedForward.forward(module, value)
|
|
|
|
def apply_rope(query, key, frequencies):
|
|
cosine = frequencies[0].to(query.dtype)
|
|
sine = frequencies[1].to(query.dtype)
|
|
negative_sine = frequencies[2].to(query.dtype)
|
|
query_output = query * cosine
|
|
query_split = query_output.shape[-1] // 2
|
|
query_output[..., :query_split].addcmul_(
|
|
query[..., query_split:], negative_sine)
|
|
query_output[..., query_split:].addcmul_(
|
|
query[..., :query_split], sine)
|
|
key_output = key * cosine
|
|
key_split = key_output.shape[-1] // 2
|
|
key_output[..., :key_split].addcmul_(
|
|
key[..., key_split:], negative_sine)
|
|
key_output[..., key_split:].addcmul_(
|
|
key[..., :key_split], sine)
|
|
return query_output, key_output
|
|
|
|
def attention_forward(
|
|
module, value, attention_mask, frequencies,
|
|
transformer_options=None,
|
|
):
|
|
batch, sequence, _ = value.shape
|
|
query, key, result = module.qkv(value).view(
|
|
batch, sequence, 3, module.num_heads,
|
|
module.head_dim).unbind(dim=2)
|
|
query = module.norm_q(query).transpose(1, 2)
|
|
key = module.norm_k(key).transpose(1, 2)
|
|
result = result.transpose(1, 2)
|
|
query, key = apply_rope(query, key, frequencies)
|
|
result = optimized_attention_masked(
|
|
query, key, result, module.num_heads, attention_mask,
|
|
skip_reshape=True,
|
|
transformer_options=transformer_options or {})
|
|
return module.o(result)
|
|
|
|
for index, block in enumerate(layers):
|
|
if chunk_ffn and ffn_chunks > 1:
|
|
def patched_ffn(
|
|
self_module, value, _chunks=ffn_chunks,
|
|
_threshold=ffn_seq_threshold,
|
|
):
|
|
return ffn_forward(
|
|
self_module, value, chunks=_chunks,
|
|
threshold=_threshold)
|
|
|
|
model.add_object_patch(
|
|
f"diffusion_model.layers.{index}.feed_forward.forward",
|
|
types.MethodType(patched_ffn, block.feed_forward))
|
|
if bf16_rope:
|
|
model.add_object_patch(
|
|
f"diffusion_model.layers.{index}.attention.forward",
|
|
types.MethodType(attention_forward, block.attention))
|
|
return model
|
|
|
|
|
|
def _ltx2_attention_tuner(
|
|
patcher, blocks: list[int], video_scale: float, audio_scale: float,
|
|
audio_to_video_scale: float, video_to_audio_scale: float,
|
|
triton_kernels: bool,
|
|
):
|
|
model = patcher.clone()
|
|
diffusion_model = model.get_model_object("diffusion_model")
|
|
transformer_blocks = getattr(
|
|
diffusion_model, "transformer_blocks", None)
|
|
if transformer_blocks is None:
|
|
raise TransformError(
|
|
"ltx2_attention_tuner needs LTX2 transformer blocks")
|
|
if blocks:
|
|
selected = set(blocks)
|
|
if any(index < 0 or index >= len(transformer_blocks)
|
|
for index in selected):
|
|
raise TransformError(
|
|
"ltx2_attention_tuner block index is out of range")
|
|
else:
|
|
selected = set(range(len(transformer_blocks)))
|
|
for index in range(len(transformer_blocks)):
|
|
scales = (
|
|
video_scale,
|
|
audio_scale,
|
|
audio_to_video_scale,
|
|
video_to_audio_scale,
|
|
) if index in selected else (1.0, 1.0, 1.0, 1.0)
|
|
prefix = f"diffusion_model.transformer_blocks.{index}"
|
|
for name, value in zip((
|
|
"video_scale",
|
|
"audio_scale",
|
|
"audio_to_video_scale",
|
|
"video_to_audio_scale",
|
|
), scales):
|
|
model.add_object_patch(f"{prefix}.{name}", value)
|
|
model.add_object_patch(
|
|
f"{prefix}.use_triton_kernels", triton_kernels)
|
|
return model
|
|
|
|
|
|
def _memory_efficient_sage(
|
|
patcher, architecture: str, triton_kernels: bool,
|
|
):
|
|
from . import _memory_attention
|
|
|
|
if architecture == "ltx2":
|
|
return _memory_attention.apply_ltx2(patcher, triton_kernels)
|
|
if architecture == "minimax":
|
|
return _memory_attention.apply_minimax(patcher)
|
|
if architecture == "wan":
|
|
return _memory_attention.apply_wan(patcher)
|
|
raise TransformError(
|
|
f"unsupported memory-efficient Sage architecture {architecture!r}")
|
|
|
|
|
|
def _minimax_chunk_feed_forward(
|
|
patcher, chunks: int, seq_threshold: int,
|
|
):
|
|
import types
|
|
|
|
import torch
|
|
import comfy.ops
|
|
|
|
if chunks == 1:
|
|
return patcher
|
|
|
|
model = patcher.clone()
|
|
diffusion_model = model.get_model_object("diffusion_model")
|
|
blocks = getattr(diffusion_model, "blocks", None)
|
|
if (
|
|
not blocks
|
|
or not hasattr(blocks[0], "mlp")
|
|
or not hasattr(blocks[0].mlp, "fc1")
|
|
):
|
|
return patcher
|
|
|
|
linear_input_act = getattr(comfy.ops, "linear_input_act", None)
|
|
if not callable(linear_input_act):
|
|
raise TransformError(
|
|
"MiniMax feed-forward chunking requires core "
|
|
"comfy.ops.linear_input_act; update ComfyUI for MiniMax H3 support")
|
|
|
|
for index, block in enumerate(blocks):
|
|
def chunked(
|
|
self_module, value, _chunks=chunks,
|
|
_threshold=seq_threshold,
|
|
):
|
|
self_module.kj_num_chunks = _chunks
|
|
self_module.kj_seq_threshold = _threshold
|
|
if value.shape[0] > _threshold and _chunks > 1:
|
|
output = torch.empty_like(value)
|
|
offset = 0
|
|
for part in torch.chunk(value, _chunks, dim=0):
|
|
end = offset + part.shape[0]
|
|
output[offset:end] = linear_input_act(
|
|
self_module.fc2, self_module.fc1(part), "swiglu")
|
|
offset = end
|
|
return output
|
|
return linear_input_act(
|
|
self_module.fc2, self_module.fc1(value), "swiglu")
|
|
|
|
model.add_object_patch(
|
|
f"diffusion_model.blocks.{index}.mlp.forward",
|
|
types.MethodType(chunked, block.mlp))
|
|
return model
|
|
|
|
|
|
def _minimax_low_vram_attention(patcher, head_chunks: int):
|
|
import types
|
|
|
|
import torch
|
|
import comfy.model_management as model_management
|
|
import comfy.quant_ops
|
|
from comfy.ldm.modules.attention import optimized_attention
|
|
|
|
model = patcher.clone()
|
|
diffusion_model = model.get_model_object("diffusion_model")
|
|
blocks = getattr(diffusion_model, "blocks", None)
|
|
if (
|
|
not blocks
|
|
or not hasattr(blocks[0], "attn")
|
|
or not hasattr(blocks[0].attn, "qkv_proj")
|
|
):
|
|
return patcher
|
|
|
|
try:
|
|
from comfy.ldm.minimax.model import _mod_gate, _mod_scale_shift
|
|
except ImportError as error:
|
|
raise TransformError(
|
|
"MiniMax low-VRAM attention requires core MiniMax H3 support; "
|
|
"update ComfyUI") from error
|
|
|
|
def attention_forward(
|
|
self_module, value, rope_freqs=None, transformer_options={},
|
|
):
|
|
if isinstance(value, list):
|
|
value = value.pop()
|
|
sequence = value.shape[0]
|
|
device = value.device
|
|
dtype = value.dtype
|
|
qkv = self_module.qkv_proj(value)
|
|
del value
|
|
query, key, result = qkv.split(
|
|
self_module.heads * self_module.head_dim, dim=-1)
|
|
result = result.view(
|
|
sequence, self_module.heads, self_module.head_dim)
|
|
if rope_freqs is not None:
|
|
query = query.view(
|
|
1, sequence, self_module.heads, self_module.head_dim)
|
|
key = key.view(
|
|
1, sequence, self_module.heads, self_module.head_dim)
|
|
query_weight = model_management.cast_to(
|
|
self_module.q_norm.weight, device=device)
|
|
key_weight = model_management.cast_to(
|
|
self_module.k_norm.weight, device=device)
|
|
rotation = rope_freqs.shape[-3] * 2
|
|
if model_management.in_training:
|
|
query, key = comfy.quant_ops.ck.rms_rope_split_half(
|
|
query, key, rope_freqs, query_weight, key_weight,
|
|
epsilon=self_module.q_norm.eps, rot_dim=rotation)
|
|
else:
|
|
comfy.quant_ops.ck.rms_rope_split_half_(
|
|
query, key, rope_freqs, query_weight, key_weight,
|
|
epsilon=self_module.q_norm.eps, rot_dim=rotation)
|
|
query = query[0]
|
|
key = key[0]
|
|
else:
|
|
query = self_module.q_norm(query.view(
|
|
sequence, self_module.heads, self_module.head_dim))
|
|
key = self_module.k_norm(key.view(
|
|
sequence, self_module.heads, self_module.head_dim))
|
|
query = query.transpose(0, 1).unsqueeze(0)
|
|
key = key.transpose(0, 1).unsqueeze(0)
|
|
result = result.transpose(0, 1).unsqueeze(0)
|
|
groups = (
|
|
min(transformer_options.get("minimax_head_chunks", 1),
|
|
self_module.heads)
|
|
if isinstance(transformer_options, dict) else 1
|
|
)
|
|
if groups <= 1:
|
|
output = optimized_attention(
|
|
query, key, result, self_module.heads, mask=None,
|
|
skip_reshape=True,
|
|
transformer_options=transformer_options).squeeze(0)
|
|
else:
|
|
output = torch.empty(
|
|
(sequence, self_module.heads * self_module.head_dim),
|
|
dtype=dtype, device=device)
|
|
start = 0
|
|
sizes = [
|
|
self_module.heads // groups
|
|
+ (1 if index < self_module.heads % groups else 0)
|
|
for index in range(groups)
|
|
]
|
|
for size in sizes:
|
|
stop = start + size
|
|
current = optimized_attention(
|
|
query[:, start:stop], key[:, start:stop],
|
|
result[:, start:stop], size, mask=None,
|
|
skip_reshape=True,
|
|
transformer_options=transformer_options)
|
|
output[:, start * self_module.head_dim:
|
|
stop * self_module.head_dim] = current.squeeze(0)
|
|
start = stop
|
|
del query, key, result, qkv
|
|
return self_module.out_proj(output)
|
|
|
|
attention_forward._uses_optimized_attention = True
|
|
|
|
def block_forward(
|
|
self_module, value, timestep, mod_segments, rope_freqs,
|
|
transformer_options={},
|
|
):
|
|
(shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp,
|
|
gate_mlp) = self_module.adaln_proj(timestep)
|
|
hidden = [_mod_scale_shift(
|
|
self_module.norm1(value), shift_msa, scale_msa, mod_segments)]
|
|
value = _mod_gate(
|
|
value, gate_msa,
|
|
self_module.attn(
|
|
hidden, rope_freqs=rope_freqs,
|
|
transformer_options=transformer_options),
|
|
mod_segments)
|
|
hidden = _mod_scale_shift(
|
|
self_module.norm2(value), shift_mlp, scale_mlp, mod_segments)
|
|
return _mod_gate(
|
|
value, gate_mlp, self_module.mlp(hidden), mod_segments)
|
|
|
|
options = model.model_options.setdefault("transformer_options", {})
|
|
if head_chunks > 1:
|
|
options["minimax_head_chunks"] = head_chunks
|
|
options["sol_take_forward"] = attention_forward
|
|
for index, block in enumerate(blocks):
|
|
prefix = f"diffusion_model.blocks.{index}"
|
|
model.add_object_patch(
|
|
f"{prefix}.forward", types.MethodType(block_forward, block))
|
|
attention_key = f"{prefix}.attn.forward"
|
|
if attention_key not in getattr(model, "object_patches", {}):
|
|
model.add_object_patch(
|
|
attention_key,
|
|
types.MethodType(attention_forward, block.attn))
|
|
return model
|
|
|
|
|
|
def _matmul_fp16_accumulation(patcher, enabled: bool):
|
|
"""Select fp16 matmul accumulation for each application of this model."""
|
|
import torch
|
|
|
|
if not hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"):
|
|
if enabled:
|
|
raise TransformError(
|
|
"this torch build has no matmul.allow_fp16_accumulation "
|
|
"(requires torch 2.7.0 or newer)")
|
|
return patcher.clone()
|
|
|
|
m = patcher.clone()
|
|
options = m.model_options.setdefault("transformer_options", {})
|
|
flags = dict(options.get("model_backend_flags", {}))
|
|
flags["allow_fp16_accumulation"] = enabled
|
|
options["model_backend_flags"] = flags
|
|
return m
|
|
|
|
|
|
def _memory_usage_factor(patcher, factor: float):
|
|
"""Override the model's memory-usage estimate during sampling.
|
|
|
|
The estimate drives how much core keeps resident. It is a hint, so a node
|
|
correcting it for an architecture core estimates badly is legitimate — and
|
|
it is a float, which is why this needs no new machinery at all.
|
|
"""
|
|
from comfy.patcher_extension import WrappersMP
|
|
|
|
def wrapper(executor, model, *args, **kwargs):
|
|
adjusted = model.clone()
|
|
original = adjusted.model.memory_usage_factor
|
|
adjusted.model.memory_usage_factor = factor
|
|
try:
|
|
return executor(adjusted, *args, **kwargs)
|
|
finally:
|
|
adjusted.model.memory_usage_factor = original
|
|
|
|
m = patcher.clone()
|
|
m.add_wrapper_with_key(WrappersMP.PREPARE_SAMPLING,
|
|
"comfy.model_transform.memory_usage_factor", wrapper)
|
|
return m
|
|
|
|
|
|
_FFN_TARGETS = ("blocks_ffn", "ltx_transformer_ff")
|
|
|
|
|
|
def _ffn_chunking(
|
|
patcher, chunks: int, dim_threshold: int, target: str = "blocks_ffn",
|
|
):
|
|
"""Trade time for peak VRAM in the feed-forward blocks.
|
|
|
|
Chunking activations along the token dimension is arithmetically identical
|
|
to not chunking — the same function applied to slices of a sum-free
|
|
elementwise path — so this changes memory, not results, up to float
|
|
non-associativity.
|
|
|
|
The target is a closed core-owned model layout, not an object path supplied
|
|
by the guest. A model shaped differently is refused rather than silently
|
|
left unpatched.
|
|
"""
|
|
import torch
|
|
|
|
if chunks == 1:
|
|
return patcher.clone()
|
|
|
|
m = patcher.clone()
|
|
diffusion_model = m.get_model_object("diffusion_model")
|
|
if target == "ltx_transformer_ff":
|
|
import types
|
|
|
|
blocks = getattr(diffusion_model, "transformer_blocks", None)
|
|
if blocks is None:
|
|
raise TransformError(
|
|
"ffn_chunking target ltx_transformer_ff needs "
|
|
"diffusion_model.transformer_blocks")
|
|
feed_forwards = [getattr(block, "ff", None) for block in blocks]
|
|
if not feed_forwards or any(
|
|
feed_forward is None or not hasattr(feed_forward, "net")
|
|
for feed_forward in feed_forwards
|
|
):
|
|
raise TransformError(
|
|
"ffn_chunking target ltx_transformer_ff needs every "
|
|
"transformer block to expose ff.net")
|
|
for index, feed_forward in enumerate(feed_forwards):
|
|
def chunked(self_module, x, _chunks=chunks,
|
|
_threshold=dim_threshold):
|
|
self_module.num_chunks = _chunks
|
|
self_module.dim_threshold = _threshold
|
|
if x.shape[1] > self_module.dim_threshold:
|
|
chunk_size = x.shape[1] // self_module.num_chunks
|
|
for chunk_index in range(self_module.num_chunks):
|
|
start = chunk_index * chunk_size
|
|
end = (
|
|
(chunk_index + 1) * chunk_size
|
|
if chunk_index < self_module.num_chunks - 1
|
|
else x.shape[1]
|
|
)
|
|
x[:, start:end] = self_module.net(x[:, start:end])
|
|
return x
|
|
return self_module.net(x)
|
|
|
|
m.add_object_patch(
|
|
f"diffusion_model.transformer_blocks.{index}.ff.forward",
|
|
types.MethodType(chunked, feed_forward),
|
|
)
|
|
return m
|
|
|
|
if target != "blocks_ffn":
|
|
raise TransformError(f"unknown ffn_chunking target {target!r}")
|
|
|
|
blocks = getattr(diffusion_model, "blocks", None)
|
|
if blocks is None:
|
|
raise TransformError(
|
|
"ffn_chunking needs a model with a `blocks` transformer stack; "
|
|
f"{type(diffusion_model).__name__} has none")
|
|
|
|
patched = 0
|
|
for idx, block in enumerate(blocks):
|
|
ffn = getattr(block, "ffn", None)
|
|
if ffn is None:
|
|
continue
|
|
|
|
def chunked(*args, _ffn=ffn, **kwargs):
|
|
x = args[0]
|
|
if x.shape[1] <= dim_threshold:
|
|
return _ffn.__class__.forward(_ffn, x, *args[1:], **kwargs)
|
|
parts = [_ffn.__class__.forward(_ffn, c, *args[1:], **kwargs)
|
|
for c in torch.chunk(x, chunks, dim=1)]
|
|
return torch.cat(parts, dim=1)
|
|
|
|
m.add_object_patch(f"diffusion_model.blocks.{idx}.ffn.forward", chunked)
|
|
patched += 1
|
|
|
|
if patched == 0:
|
|
raise TransformError(
|
|
"ffn_chunking found no `ffn` submodule in any block of "
|
|
f"{type(diffusion_model).__name__}")
|
|
return m
|
|
|
|
|
|
_COMPILE_SCOPES = (
|
|
"whole",
|
|
"known_transformer_blocks",
|
|
"flux_blocks",
|
|
"wan_blocks",
|
|
)
|
|
|
|
|
|
def _compile_guard_filter(guard_entries):
|
|
return [("transformer_options" not in entry.name)
|
|
for entry in guard_entries]
|
|
|
|
|
|
_compile_aimdo_patched = False
|
|
_COMPILE_DYNAMO_POLICY_KEY = "comfy.model_transform.compile.dynamo_policy"
|
|
|
|
|
|
def _patch_aimdo_for_compile():
|
|
global _compile_aimdo_patched
|
|
if _compile_aimdo_patched:
|
|
return
|
|
_compile_aimdo_patched = True
|
|
|
|
import torch
|
|
import comfy.ops
|
|
|
|
names = (
|
|
"cast_bias_weight",
|
|
"uncast_bias_weight",
|
|
"cast_modules_with_vbar",
|
|
"resolve_cast_module_with_vbar",
|
|
)
|
|
for name in names:
|
|
fn = getattr(comfy.ops, name, None)
|
|
if fn is not None:
|
|
setattr(comfy.ops, name, torch._dynamo.disable(fn))
|
|
try:
|
|
import comfy_aimdo.torch as aimdo_torch
|
|
except ImportError:
|
|
return
|
|
aimdo_torch.get_tensor_from_raw_ptr = torch._dynamo.disable(
|
|
aimdo_torch.get_tensor_from_raw_ptr)
|
|
|
|
|
|
def _compile_keys(diffusion_model, scope: str, double_blocks: bool,
|
|
single_blocks: bool) -> list[str]:
|
|
if scope == "whole":
|
|
return ["diffusion_model"]
|
|
|
|
keys = []
|
|
if scope == "known_transformer_blocks":
|
|
layer_types = (
|
|
"double_blocks",
|
|
"single_blocks",
|
|
"layers",
|
|
"transformer_blocks",
|
|
"blocks",
|
|
"visual_transformer_blocks",
|
|
"text_transformer_blocks",
|
|
"patch_blocks",
|
|
"pixel_blocks",
|
|
)
|
|
for layer_name in layer_types:
|
|
blocks = getattr(diffusion_model, layer_name, None)
|
|
if blocks is not None:
|
|
keys.extend(
|
|
f"diffusion_model.{layer_name}.{i}"
|
|
for i in range(len(blocks)))
|
|
elif scope == "flux_blocks":
|
|
if double_blocks:
|
|
keys.extend(
|
|
f"diffusion_model.double_blocks.{i}"
|
|
for i in range(len(diffusion_model.double_blocks)))
|
|
if single_blocks:
|
|
keys.extend(
|
|
f"diffusion_model.single_blocks.{i}"
|
|
for i in range(len(diffusion_model.single_blocks)))
|
|
elif scope == "wan_blocks":
|
|
keys.extend(
|
|
f"diffusion_model.blocks.{i}"
|
|
for i in range(len(diffusion_model.blocks)))
|
|
|
|
if not keys:
|
|
if scope == "known_transformer_blocks":
|
|
logging.warning(
|
|
"No known transformer blocks found to compile, compiling "
|
|
"entire diffusion model instead")
|
|
return ["diffusion_model"]
|
|
return keys
|
|
|
|
|
|
def _install_compile_dynamo_policy(model, settings: dict[str, Any]) -> None:
|
|
from comfy.patcher_extension import WrappersMP
|
|
|
|
model.remove_wrappers_with_key(
|
|
WrappersMP.APPLY_MODEL, _COMPILE_DYNAMO_POLICY_KEY)
|
|
if not settings:
|
|
return
|
|
|
|
def apply_with_dynamo_policy(executor, *args, **kwargs):
|
|
import torch
|
|
|
|
with torch._dynamo.config.patch(**settings):
|
|
return executor(*args, **kwargs)
|
|
|
|
model.add_wrapper_with_key(
|
|
WrappersMP.APPLY_MODEL,
|
|
_COMPILE_DYNAMO_POLICY_KEY,
|
|
apply_with_dynamo_policy,
|
|
)
|
|
|
|
|
|
def _compile(patcher, backend: str, mode: str, fullgraph: bool,
|
|
dynamic: bool | None, scope: str = "whole",
|
|
double_blocks: bool = True, single_blocks: bool = True,
|
|
dynamo_cache_size_limit: int | None = None,
|
|
force_parameter_static_shapes: bool | None = None,
|
|
dynamic_vram: str = "disable", guard_filter: bool = False,
|
|
debug_compile_keys: bool = False,
|
|
default_mode: str = "omit"):
|
|
"""torch.compile the diffusion model.
|
|
|
|
Core's compile wrapper swaps compiled modules only while BaseModel applies
|
|
the model, preserving ModelPatcher's offload and low-VRAM lifecycle.
|
|
"""
|
|
from comfy_api.torch_helpers import set_torch_compile_wrapper
|
|
|
|
if dynamic_vram == "disable":
|
|
m = patcher.clone(disable_dynamic=True)
|
|
else:
|
|
m = patcher.clone()
|
|
|
|
diffusion_model = m.get_model_object("diffusion_model")
|
|
keys = _compile_keys(diffusion_model, scope, double_blocks, single_blocks)
|
|
if debug_compile_keys and keys != ["diffusion_model"]:
|
|
logging.info("TorchCompileModelAdvanced: Compile key list:")
|
|
for key in keys:
|
|
logging.info(" - %s", key)
|
|
|
|
if dynamic_vram == "stabilize" and m.is_dynamic():
|
|
_patch_aimdo_for_compile()
|
|
|
|
compile_kwargs = {
|
|
"backend": backend,
|
|
"mode": (None if mode == "default" and default_mode == "omit"
|
|
else mode),
|
|
"fullgraph": fullgraph,
|
|
"dynamic": dynamic,
|
|
"keys": keys,
|
|
}
|
|
if guard_filter and mode == "default":
|
|
compile_kwargs["options"] = {
|
|
"guard_filter_fn": _compile_guard_filter,
|
|
}
|
|
|
|
dynamo_settings = {}
|
|
if dynamo_cache_size_limit is not None:
|
|
dynamo_settings["cache_size_limit"] = dynamo_cache_size_limit
|
|
if force_parameter_static_shapes is not None:
|
|
dynamo_settings["force_parameter_static_shapes"] = (
|
|
force_parameter_static_shapes)
|
|
|
|
if dynamo_settings:
|
|
import torch
|
|
|
|
with torch._dynamo.config.patch(**dynamo_settings):
|
|
set_torch_compile_wrapper(m, **compile_kwargs)
|
|
else:
|
|
set_torch_compile_wrapper(m, **compile_kwargs)
|
|
_install_compile_dynamo_policy(m, dynamo_settings)
|
|
return m
|
|
|
|
|
|
_CONTEXT_SCHEDULES = (
|
|
"standard_static",
|
|
"standard_static_balanced",
|
|
"standard_uniform",
|
|
"looped_uniform",
|
|
"batched",
|
|
"batched_shifted",
|
|
)
|
|
_CONTEXT_FUSE_METHODS = (
|
|
"pyramid",
|
|
"relative",
|
|
"flat",
|
|
"overlap-linear",
|
|
"hann",
|
|
"gaussian",
|
|
)
|
|
|
|
|
|
def _context_windows_static_balanced(
|
|
num_frames, handler, model_options=None,
|
|
):
|
|
import math
|
|
|
|
frame_count = int(num_frames)
|
|
length = int(handler.context_length)
|
|
overlap = int(handler.context_overlap)
|
|
if frame_count <= length:
|
|
return [list(range(frame_count))]
|
|
stride = max(1, length - overlap)
|
|
count = -(-(frame_count - length) // stride) + 1
|
|
return [
|
|
list(range(
|
|
math.floor(index * (frame_count - length) / (count - 1) + 0.5),
|
|
math.floor(index * (frame_count - length) / (count - 1) + 0.5)
|
|
+ length,
|
|
))
|
|
for index in range(count)
|
|
]
|
|
|
|
|
|
def _context_windows_batched_shifted(
|
|
num_frames, handler, model_options=None,
|
|
):
|
|
from comfy.context_windows import ordered_halving
|
|
|
|
frame_count = int(num_frames)
|
|
length = int(handler.context_length)
|
|
if frame_count <= length:
|
|
return [list(range(frame_count))]
|
|
offset = round(frame_count * ordered_halving(int(handler._step))) % length
|
|
windows = []
|
|
start = 0
|
|
if offset > 0:
|
|
windows.append(list(range(offset)))
|
|
start = offset
|
|
while start < frame_count:
|
|
end = min(start + length, frame_count)
|
|
windows.append(list(range(start, end)))
|
|
start = end
|
|
return windows
|
|
|
|
|
|
def _context_weights_hann(
|
|
length, full_length=None, idxs=None, handler=None, **kwargs,
|
|
):
|
|
import math
|
|
|
|
weights = [1.0] * length
|
|
overlap = min(
|
|
max(int(getattr(handler, "context_overlap", 0) or 0), 0), length)
|
|
if overlap >= 1 and idxs:
|
|
denominator = max(overlap - 1, 1)
|
|
if min(idxs) > 0:
|
|
for index in range(overlap):
|
|
weights[index] = max(
|
|
0.5 * (1 - math.cos(math.pi * index / denominator)),
|
|
1e-37)
|
|
if full_length is not None and max(idxs) < full_length - 1:
|
|
for index in range(overlap):
|
|
weights[length - overlap + index] = max(
|
|
0.5 * (1 - math.cos(
|
|
math.pi * (denominator - index) / denominator)),
|
|
1e-37)
|
|
return weights
|
|
|
|
|
|
def _context_weights_gaussian(length, **kwargs):
|
|
import math
|
|
|
|
if length <= 1:
|
|
return [1.0] * length
|
|
center = (length - 1) / 2.0
|
|
standard_deviation = max(length / 4.0, 1e-6)
|
|
return [
|
|
math.exp(-0.5 * ((index - center) / standard_deviation) ** 2)
|
|
for index in range(length)
|
|
]
|
|
|
|
|
|
def _context_windows(
|
|
patcher, context_schedule: str, fuse_method: str,
|
|
context_length: int, context_overlap: int, context_stride: int,
|
|
closed_loop: bool, dim: int, freenoise: bool,
|
|
causal_window_fix: bool, cond_retain_indices: list[int],
|
|
):
|
|
import comfy.context_windows as context_windows
|
|
|
|
local_schedules = {
|
|
"standard_static_balanced": _context_windows_static_balanced,
|
|
"batched_shifted": _context_windows_batched_shifted,
|
|
}
|
|
local_fuse_methods = {
|
|
"hann": _context_weights_hann,
|
|
"gaussian": _context_weights_gaussian,
|
|
}
|
|
if context_schedule in local_schedules:
|
|
schedule = context_windows.ContextSchedule(
|
|
context_schedule, local_schedules[context_schedule])
|
|
else:
|
|
schedule = context_windows.get_matching_context_schedule(
|
|
context_schedule)
|
|
if fuse_method in local_fuse_methods:
|
|
fuse = context_windows.ContextFuseMethod(
|
|
fuse_method, local_fuse_methods[fuse_method])
|
|
else:
|
|
fuse = context_windows.get_matching_fuse_method(fuse_method)
|
|
|
|
model = patcher.clone()
|
|
handler = context_windows.IndexListContextHandler(
|
|
context_schedule=schedule,
|
|
fuse_method=fuse,
|
|
context_length=context_length,
|
|
context_overlap=context_overlap,
|
|
context_stride=context_stride,
|
|
closed_loop=closed_loop,
|
|
dim=dim,
|
|
freenoise=freenoise,
|
|
causal_window_fix=causal_window_fix,
|
|
cond_retain_index_list=",".join(
|
|
str(index) for index in cond_retain_indices),
|
|
)
|
|
model.model_options["context_handler"] = handler
|
|
context_windows.create_prepare_sampling_wrapper(model)
|
|
if freenoise:
|
|
context_windows.create_sampler_sample_wrapper(model)
|
|
return model
|
|
|
|
|
|
def _cfg_zero_star(patcher, use_zero_init: bool, zero_init_steps: int):
|
|
import torch
|
|
|
|
def cfg_zero_star(args):
|
|
cond = args["cond"]
|
|
timestep = args["timestep"]
|
|
sigmas = args["model_options"]["transformer_options"]["sample_sigmas"]
|
|
matched = (sigmas == timestep[0]).nonzero()
|
|
if len(matched) > 0:
|
|
current_step = matched.item()
|
|
else:
|
|
current_step = 0
|
|
for index in range(len(sigmas) - 1):
|
|
if ((sigmas[index] - timestep[0])
|
|
* (sigmas[index + 1] - timestep[0])) <= 0:
|
|
current_step = index
|
|
break
|
|
|
|
if use_zero_init and current_step <= zero_init_steps:
|
|
return cond * 0
|
|
|
|
uncond = args["uncond"]
|
|
cond_scale = args["cond_scale"]
|
|
batch_size = cond.shape[0]
|
|
positive_flat = cond.view(batch_size, -1)
|
|
negative_flat = uncond.view(batch_size, -1)
|
|
dot_product = torch.sum(
|
|
positive_flat * negative_flat, dim=1, keepdim=True)
|
|
squared_norm = torch.sum(
|
|
negative_flat ** 2, dim=1, keepdim=True) + 1e-8
|
|
alpha = (dot_product / squared_norm).view(
|
|
batch_size, *([1] * (len(cond.shape) - 1)))
|
|
return uncond * alpha + cond_scale * (cond - uncond * alpha)
|
|
|
|
model = patcher.clone()
|
|
model.set_model_sampler_cfg_function(cfg_zero_star)
|
|
return model
|
|
|
|
|
|
_PID_BIAS_COEF_FLUX2 = (
|
|
(-0.130306, +0.127184, +0.014058),
|
|
(-0.053279, -0.408929, +0.004243),
|
|
(-0.009386, +0.109546, -0.134091),
|
|
(-0.033373, -0.011615, -0.026129),
|
|
(+0.180052, +0.062021, +0.071317),
|
|
(-0.067958, -0.058595, -0.098645),
|
|
(-0.248116, -0.240633, -0.105600),
|
|
(+0.304035, +0.322566, +0.093224),
|
|
(-0.157648, -0.227127, -0.112368),
|
|
(-0.062814, +0.030765, +0.062735),
|
|
)
|
|
|
|
|
|
def _pid_color_bias(patcher, strength: float, backbone: str):
|
|
import torch
|
|
|
|
if strength == 0.0:
|
|
return patcher
|
|
|
|
def correct(args):
|
|
denoised = args["denoised"]
|
|
try:
|
|
sigmas = args["model_options"]["transformer_options"][
|
|
"sample_sigmas"]
|
|
sigma = args.get("sigma", args.get("timestep"))
|
|
if sigma is None or not torch.isclose(
|
|
sigma.max(), sigmas[0]).item():
|
|
return denoised
|
|
except (KeyError, AttributeError):
|
|
sigma = args.get("sigma")
|
|
if sigma is None or sigma.max().item() < 0.95:
|
|
return denoised
|
|
|
|
coef = torch.tensor(
|
|
_PID_BIAS_COEF_FLUX2,
|
|
device=denoised.device,
|
|
dtype=denoised.dtype,
|
|
)
|
|
rgb_mean = denoised.mean(dim=(0, 2, 3))
|
|
rgb_std = denoised.std(dim=(0, 2, 3))
|
|
features = torch.stack((
|
|
rgb_mean[0], rgb_mean[1], rgb_mean[2],
|
|
rgb_std[0], rgb_std[1], rgb_std[2],
|
|
rgb_mean[0] * rgb_mean[1],
|
|
rgb_mean[0] * rgb_mean[2],
|
|
rgb_mean[1] * rgb_mean[2],
|
|
denoised.new_tensor(1.0),
|
|
))
|
|
bias = features @ coef
|
|
return denoised - strength * bias.view(1, 3, 1, 1)
|
|
|
|
model = patcher.clone()
|
|
model.set_model_sampler_post_cfg_function(correct)
|
|
return model
|
|
|
|
|
|
_DYNAMIC_THRESHOLD_MODES = (
|
|
"Constant",
|
|
"Linear Down",
|
|
"Cosine Down",
|
|
"Half Cosine Down",
|
|
"Linear Up",
|
|
"Cosine Up",
|
|
"Half Cosine Up",
|
|
"Power Up",
|
|
"Power Down",
|
|
"Linear Repeating",
|
|
"Cosine Repeating",
|
|
"Sawtooth",
|
|
)
|
|
|
|
|
|
def _dynamic_threshold_scale(
|
|
value: float, minimum: float, mode: str, fraction: float,
|
|
schedule_value: float,
|
|
) -> float:
|
|
"""Evaluate one bounded Dynamic Thresholding schedule."""
|
|
import math
|
|
|
|
amplitude = value - minimum
|
|
if mode == "Linear Down":
|
|
amplitude *= 1.0 - fraction
|
|
elif mode == "Half Cosine Down":
|
|
amplitude *= math.cos(fraction)
|
|
elif mode == "Cosine Down":
|
|
amplitude *= math.cos(fraction * 1.5707)
|
|
elif mode == "Linear Up":
|
|
amplitude *= fraction
|
|
elif mode == "Half Cosine Up":
|
|
amplitude *= 1.0 - math.cos(fraction)
|
|
elif mode == "Cosine Up":
|
|
amplitude *= 1.0 - math.cos(fraction * 1.5707)
|
|
elif mode == "Power Up":
|
|
amplitude *= math.pow(fraction, schedule_value)
|
|
elif mode == "Power Down":
|
|
amplitude *= 1.0 - math.pow(fraction, schedule_value)
|
|
elif mode == "Linear Repeating":
|
|
portion = (fraction * schedule_value) % 1.0
|
|
amplitude *= (
|
|
(0.5 - portion) * 2.0
|
|
if portion < 0.5
|
|
else (portion - 0.5) * 2.0
|
|
)
|
|
elif mode == "Cosine Repeating":
|
|
amplitude *= (
|
|
math.cos(fraction * 6.28318 * schedule_value) * 0.5
|
|
+ 0.5
|
|
)
|
|
elif mode == "Sawtooth":
|
|
amplitude *= (fraction * schedule_value) % 1.0
|
|
return amplitude + minimum
|
|
|
|
|
|
def _dynamic_threshold_result(
|
|
cond, uncond, cfg_scale,
|
|
*,
|
|
fraction: float,
|
|
mimic_scale: float,
|
|
threshold_percentile: float,
|
|
mimic_mode: str,
|
|
mimic_scale_min: float,
|
|
cfg_mode: str,
|
|
cfg_scale_min: float,
|
|
schedule_value: float,
|
|
separate_feature_channels: bool,
|
|
scaling_startpoint: str,
|
|
variability_measure: str,
|
|
interpolate_phi: float,
|
|
):
|
|
"""Return canonical Dynamic Thresholding guidance for one sample step."""
|
|
import torch
|
|
|
|
if cond.ndim < 3 or uncond.ndim != cond.ndim:
|
|
raise ValueError(
|
|
"dynamic_thresholding needs matching batched latent predictions")
|
|
if uncond.shape[0] < 1 or cond.shape[0] % uncond.shape[0] != 0:
|
|
raise ValueError(
|
|
"dynamic_thresholding needs a constant number of conditions "
|
|
"per batch item")
|
|
if tuple(cond.shape[1:]) != tuple(uncond.shape[1:]):
|
|
raise ValueError(
|
|
"dynamic_thresholding conditional and unconditional shapes differ")
|
|
|
|
mimic = _dynamic_threshold_scale(
|
|
mimic_scale, mimic_scale_min, mimic_mode, fraction, schedule_value)
|
|
cfg = _dynamic_threshold_scale(
|
|
float(cfg_scale), cfg_scale_min, cfg_mode, fraction, schedule_value)
|
|
conditions_per_batch = cond.shape[0] // uncond.shape[0]
|
|
cond_stacked = cond.reshape(
|
|
(-1, conditions_per_batch) + tuple(uncond.shape[1:]))
|
|
relative = (cond_stacked - uncond.unsqueeze(1)).sum(1)
|
|
mimic_target = uncond + relative * mimic
|
|
cfg_target = uncond + relative * cfg
|
|
|
|
mimic_flat = mimic_target.flatten(2)
|
|
cfg_flat = cfg_target.flatten(2)
|
|
mimic_mean = mimic_flat.mean(dim=2, keepdim=True)
|
|
cfg_mean = cfg_flat.mean(dim=2, keepdim=True)
|
|
mimic_centered = mimic_flat - mimic_mean
|
|
cfg_centered = cfg_flat - cfg_mean
|
|
|
|
if separate_feature_channels:
|
|
if variability_measure == "STD":
|
|
mimic_reference = mimic_centered.std(
|
|
dim=2, keepdim=True)
|
|
cfg_reference = cfg_centered.std(
|
|
dim=2, keepdim=True)
|
|
else:
|
|
mimic_reference = mimic_centered.abs().amax(
|
|
dim=2, keepdim=True)
|
|
cfg_reference = torch.quantile(
|
|
cfg_centered.abs(), threshold_percentile,
|
|
dim=2, keepdim=True)
|
|
elif variability_measure == "STD":
|
|
mimic_reference = mimic_centered.std()
|
|
cfg_reference = cfg_centered.std()
|
|
else:
|
|
mimic_reference = mimic_centered.abs().amax()
|
|
cfg_reference = torch.quantile(
|
|
cfg_centered.abs(), threshold_percentile)
|
|
|
|
if scaling_startpoint == "ZERO":
|
|
result = cfg_flat * (mimic_reference / cfg_reference)
|
|
elif variability_measure == "STD":
|
|
result = (
|
|
cfg_centered / cfg_reference * mimic_reference
|
|
+ cfg_mean
|
|
)
|
|
else:
|
|
maximum = torch.maximum(mimic_reference, cfg_reference)
|
|
result = (
|
|
cfg_centered.clamp(-maximum, maximum)
|
|
/ maximum
|
|
* mimic_reference
|
|
+ cfg_mean
|
|
)
|
|
|
|
result = result.reshape_as(mimic_target)
|
|
if interpolate_phi != 1.0:
|
|
result = (
|
|
result * interpolate_phi
|
|
+ cfg_target * (1.0 - interpolate_phi)
|
|
)
|
|
return result
|
|
|
|
|
|
def _dynamic_thresholding(
|
|
patcher,
|
|
mimic_scale: float,
|
|
threshold_percentile: float,
|
|
mimic_mode: str,
|
|
mimic_scale_min: float,
|
|
cfg_mode: str,
|
|
cfg_scale_min: float,
|
|
schedule_value: float,
|
|
separate_feature_channels: bool,
|
|
scaling_startpoint: str,
|
|
variability_measure: str,
|
|
interpolate_phi: float,
|
|
):
|
|
"""Install core's closed, data-configured Dynamic Thresholding behavior."""
|
|
import torch
|
|
|
|
model_sampling = patcher.get_model_object("model_sampling")
|
|
|
|
def dynamic_cfg(args):
|
|
input_value = args["input"]
|
|
cond = input_value - args["cond"]
|
|
uncond = input_value - args["uncond"]
|
|
sigma = args.get("sigma")
|
|
if not isinstance(sigma, torch.Tensor) or sigma.numel() == 0:
|
|
raise ValueError(
|
|
"dynamic_thresholding needs the current diffusion sigma")
|
|
timestep = model_sampling.timestep(sigma)
|
|
if not isinstance(timestep, torch.Tensor) or timestep.numel() == 0:
|
|
raise ValueError(
|
|
"dynamic_thresholding model did not produce a diffusion "
|
|
"timestep")
|
|
# The pinned implementation intentionally uses max_steps=999 and then
|
|
# divides by max_steps-1. Its final t=0 point is therefore slightly
|
|
# above one; preserve that endpoint instead of silently clamping it.
|
|
fraction = (
|
|
999.0 - float(timestep.reshape(-1)[0].item())
|
|
) / 998.0
|
|
if float(args["cond_scale"]) == mimic_scale:
|
|
return input_value - (
|
|
uncond + (cond - uncond) * float(args["cond_scale"])
|
|
)
|
|
guided = _dynamic_threshold_result(
|
|
cond,
|
|
uncond,
|
|
args["cond_scale"],
|
|
fraction=fraction,
|
|
mimic_scale=mimic_scale,
|
|
threshold_percentile=threshold_percentile,
|
|
mimic_mode=mimic_mode,
|
|
mimic_scale_min=mimic_scale_min,
|
|
cfg_mode=cfg_mode,
|
|
cfg_scale_min=cfg_scale_min,
|
|
schedule_value=schedule_value,
|
|
separate_feature_channels=separate_feature_channels,
|
|
scaling_startpoint=scaling_startpoint,
|
|
variability_measure=variability_measure,
|
|
interpolate_phi=interpolate_phi,
|
|
)
|
|
return input_value - guided
|
|
|
|
model = patcher.clone()
|
|
model.set_model_sampler_cfg_function(dynamic_cfg)
|
|
return model
|
|
|
|
|
|
def _style_aligned_expand_reference(value, scale: float = 1.0):
|
|
"""Expand the first item in each CFG half across that half's batch."""
|
|
import torch
|
|
|
|
batch = int(value.shape[0])
|
|
if batch < 2 or batch % 2:
|
|
return value
|
|
half = batch // 2
|
|
references = torch.stack((value[0], value[half]), dim=0).unsqueeze(1)
|
|
references = references.expand(
|
|
(2, half) + tuple(value.shape[1:]))
|
|
if scale != 1.0 and half > 1:
|
|
references = references.clone()
|
|
references[:, 1:] *= scale
|
|
return references.reshape_as(value)
|
|
|
|
|
|
def _style_aligned_adain(value):
|
|
mean = value.mean(dim=-2, keepdim=True)
|
|
std = value.var(dim=-2, keepdim=True, correction=0).add(1e-5).sqrt()
|
|
reference_mean = _style_aligned_expand_reference(mean)
|
|
reference_std = _style_aligned_expand_reference(std)
|
|
return (value - mean) / std * reference_std + reference_mean
|
|
|
|
|
|
def _style_aligned_batch(
|
|
patcher, share_norm: str, share_attention: str, scale: float,
|
|
):
|
|
"""Share reference style statistics within a classic UNet image batch."""
|
|
import types
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
diffusion = getattr(getattr(patcher, "model", None), "diffusion_model", None)
|
|
if (
|
|
diffusion is None
|
|
or not hasattr(diffusion, "input_blocks")
|
|
or not callable(getattr(diffusion, "named_modules", None))
|
|
):
|
|
raise TransformError(
|
|
"style_aligned_batch needs a classic latent-diffusion UNet")
|
|
|
|
model = patcher.clone()
|
|
if hasattr(model, "disable_model_cfg1_optimization"):
|
|
model.disable_model_cfg1_optimization()
|
|
|
|
share_group = share_norm in {"group", "both"}
|
|
share_layer = share_norm in {"layer", "both"}
|
|
for name, module in diffusion.named_modules():
|
|
if not name or not (
|
|
(share_group and isinstance(module, nn.GroupNorm))
|
|
or (share_layer and isinstance(module, nn.LayerNorm))
|
|
):
|
|
continue
|
|
key = f"diffusion_model.{name}.forward"
|
|
original = patcher.get_model_object(key)
|
|
|
|
def shared_norm(
|
|
self_module, hidden_states, *args,
|
|
_original=original, **kwargs,
|
|
):
|
|
if hidden_states.ndim < 3 or hidden_states.shape[0] % 2:
|
|
return _original(hidden_states, *args, **kwargs)
|
|
tokens = hidden_states.shape[-2]
|
|
shared = _style_aligned_expand_reference(
|
|
hidden_states, scale=1.0)
|
|
shared = torch.cat((hidden_states, shared), dim=-2)
|
|
normalized = _original(shared, *args, **kwargs)
|
|
return normalized[..., :tokens, :]
|
|
|
|
model.add_object_patch(key, types.MethodType(shared_norm, module))
|
|
|
|
if share_attention != "disabled":
|
|
adain_queries = "q" in share_attention
|
|
adain_keys = "k" in share_attention
|
|
adain_values = "v" in share_attention
|
|
|
|
def shared_attention(q, k, v, _extra_options=None):
|
|
if q.shape[0] % 2:
|
|
return q, k, v
|
|
if adain_queries:
|
|
q = _style_aligned_adain(q)
|
|
if adain_keys:
|
|
k = _style_aligned_adain(k)
|
|
if adain_values:
|
|
v = _style_aligned_adain(v)
|
|
k = torch.cat(
|
|
(k, _style_aligned_expand_reference(k, scale=scale)),
|
|
dim=-2,
|
|
)
|
|
v = torch.cat(
|
|
(v, _style_aligned_expand_reference(v)), dim=-2)
|
|
return q, k, v
|
|
|
|
model.set_model_attn1_patch(shared_attention)
|
|
return model
|
|
|
|
|
|
def _controlnet_lllite(
|
|
patcher, adapter: str, image, strength: float, steps: int,
|
|
start_percent: float, end_percent: float,
|
|
):
|
|
"""Apply the canonical legacy SD ControlNet-LLLite attention adapter."""
|
|
import sys
|
|
|
|
import folder_paths
|
|
import nodes
|
|
import torch
|
|
|
|
if (
|
|
not isinstance(image, torch.Tensor)
|
|
or image.ndim != 4
|
|
or not 1 <= image.shape[0] <= 4096
|
|
or image.shape[-1] < 3
|
|
):
|
|
raise TransformError(
|
|
"controlnet_lllite needs a bounded BHWC RGB image batch")
|
|
height, width = map(int, image.shape[1:3])
|
|
if (
|
|
height <= 0
|
|
or width <= 0
|
|
or height * width * int(image.shape[0]) > 268_435_456
|
|
):
|
|
raise TransformError("controlnet_lllite image dimensions are invalid")
|
|
effective_end = 100.0 if end_percent == 0.0 else end_percent
|
|
if start_percent > effective_end:
|
|
raise TransformError(
|
|
"controlnet_lllite start_percent must not exceed end_percent")
|
|
|
|
path = folder_paths.get_full_path_or_raise("controlnet", adapter)
|
|
node_class = getattr(nodes, "NODE_CLASS_MAPPINGS", {}).get("LLLiteLoader")
|
|
module = (
|
|
None if node_class is None
|
|
else sys.modules.get(getattr(node_class, "__module__", ""))
|
|
)
|
|
load_patch = getattr(module, "load_control_net_lllite_patch", None)
|
|
if not callable(load_patch):
|
|
raise TransformError(
|
|
"controlnet_lllite requires the host-installed canonical "
|
|
"kohya-ss ControlNet-LLLite-ComfyUI extension")
|
|
patch = load_patch(
|
|
path,
|
|
image[..., :3],
|
|
strength,
|
|
steps,
|
|
start_percent,
|
|
end_percent,
|
|
)
|
|
if not callable(patch):
|
|
raise TransformError("ControlNet-LLLite returned no attention patch")
|
|
model = patcher.clone()
|
|
model.set_model_attn1_patch(patch)
|
|
model.set_model_attn2_patch(patch)
|
|
return model
|
|
|
|
|
|
def _differential_diffusion(patcher, strength: float):
|
|
from comfy_extras.nodes_differential_diffusion import DifferentialDiffusion
|
|
|
|
return DifferentialDiffusion.execute(patcher, strength).result[0]
|
|
|
|
|
|
def _fooocus_inpaint(patcher, latent, head: str, patch: str):
|
|
"""Apply Fooocus' canonical SDXL inpaint head and quantized delta.
|
|
|
|
This is a closed model transform because the only irreducible part of the
|
|
behavior is installing a host-side UNet callback. Packs still choose and
|
|
declare the weights and own their inpaint-pipeline orchestration.
|
|
"""
|
|
import torch
|
|
import torch.nn.functional as F
|
|
import comfy.lora
|
|
import comfy.utils
|
|
import folder_paths
|
|
|
|
if not isinstance(latent, dict):
|
|
raise TransformError("fooocus_inpaint.latent is not a latent payload")
|
|
samples = latent.get("samples")
|
|
noise_mask = latent.get("noise_mask")
|
|
if (
|
|
not isinstance(samples, torch.Tensor)
|
|
or samples.ndim != 4
|
|
or samples.shape[1] != 4
|
|
or not isinstance(noise_mask, torch.Tensor)
|
|
or noise_mask.ndim not in (3, 4)
|
|
):
|
|
raise TransformError(
|
|
"fooocus_inpaint needs four-channel samples and a noise mask")
|
|
if noise_mask.ndim == 3:
|
|
noise_mask = noise_mask.unsqueeze(1)
|
|
if noise_mask.shape[1] != 1:
|
|
raise TransformError("fooocus_inpaint noise mask must have one channel")
|
|
if noise_mask.shape[0] not in (1, samples.shape[0]):
|
|
raise TransformError(
|
|
"fooocus_inpaint sample and noise-mask batches do not match")
|
|
head_path = folder_paths.get_full_path_or_raise("inpaint", head)
|
|
patch_path = folder_paths.get_full_path_or_raise("inpaint", patch)
|
|
head_state = comfy.utils.load_torch_file(head_path, safe_load=True)
|
|
head_weight = head_state.get("head") if isinstance(head_state, dict) else None
|
|
if (
|
|
not isinstance(head_weight, torch.Tensor)
|
|
or tuple(head_weight.shape) != (320, 5, 3, 3)
|
|
or len(head_state) != 1
|
|
):
|
|
raise TransformError("Fooocus inpaint head has an invalid state dict")
|
|
|
|
base_model = patcher.model
|
|
latent_pixels = base_model.process_latent_in(samples)
|
|
if tuple(noise_mask.shape[-2:]) == tuple(latent_pixels.shape[-2:]):
|
|
latent_mask = noise_mask.round().to(latent_pixels)
|
|
else:
|
|
latent_mask = F.max_pool2d(
|
|
noise_mask.round(), (8, 8)).round().to(latent_pixels)
|
|
if latent_mask.shape[0] == 1 and latent_pixels.shape[0] > 1:
|
|
latent_mask = latent_mask.repeat(latent_pixels.shape[0], 1, 1, 1)
|
|
if tuple(latent_mask.shape[-2:]) != tuple(latent_pixels.shape[-2:]):
|
|
raise TransformError(
|
|
"Fooocus mask pooling does not match the model latent dimensions")
|
|
feed = torch.cat([latent_mask, latent_pixels], dim=1)
|
|
feature = F.conv2d(
|
|
F.pad(feed, (1, 1, 1, 1), "replicate"),
|
|
head_weight.to(device=feed.device, dtype=feed.dtype),
|
|
)
|
|
|
|
patch_state = comfy.utils.load_torch_file(patch_path, safe_load=True)
|
|
if not isinstance(patch_state, dict):
|
|
raise TransformError("Fooocus inpaint patch is not a state dict")
|
|
model_keys = comfy.lora.model_lora_keys_unet(base_model, {})
|
|
model_keys.update({key: key for key in base_model.state_dict().keys()})
|
|
loaded = {}
|
|
for key in model_keys.values():
|
|
value = patch_state.get(key)
|
|
if value is None:
|
|
continue
|
|
if (
|
|
not isinstance(value, (tuple, list))
|
|
or len(value) != 3
|
|
or not all(isinstance(item, torch.Tensor) for item in value)
|
|
):
|
|
raise TransformError(
|
|
f"Fooocus patch entry {key!r} is not a quantized weight")
|
|
loaded[key] = ("fooocus", tuple(value))
|
|
if not loaded:
|
|
raise TransformError(
|
|
"Fooocus inpaint patch has no weights for this model")
|
|
|
|
def input_block_patch(hidden, transformer_options):
|
|
block = transformer_options.get("block")
|
|
if isinstance(block, (tuple, list)) and len(block) > 1 and block[1] == 0:
|
|
if hidden.shape[1:] != feature.shape[1:]:
|
|
raise RuntimeError(
|
|
"Fooocus inpaint feature does not match the UNet input block")
|
|
if hidden.shape[0] % feature.shape[0] != 0:
|
|
raise RuntimeError(
|
|
"Fooocus inpaint feature batch does not match the UNet batch")
|
|
repeated = feature.to(hidden).repeat(
|
|
hidden.shape[0] // feature.shape[0], 1, 1, 1)
|
|
hidden = hidden + repeated
|
|
return hidden
|
|
|
|
model = patcher.clone()
|
|
model.set_model_input_block_patch(input_block_patch)
|
|
patched = set(model.add_patches(loaded, 1.0))
|
|
missing = set(loaded) - patched
|
|
if missing:
|
|
logging.warning(
|
|
"Fooocus inpaint could not attach %d model weights", len(missing))
|
|
model.model_options.setdefault("transformer_options", {})["fooocus"] = True
|
|
return model
|
|
|
|
|
|
def _diffusion_weight_delta(
|
|
patcher, model_patch: str, strength: float,
|
|
pad_input_channels: bool,
|
|
):
|
|
"""Apply a shape-checked SafeTensors diffusion-model delta.
|
|
|
|
This is deliberately architecture-neutral. The file may only change keys
|
|
already present in the model. The sole permitted shape change is widening
|
|
the first convolution's input channels when explicitly requested.
|
|
"""
|
|
import torch
|
|
import comfy.utils
|
|
import folder_paths
|
|
|
|
path = folder_paths.get_full_path_or_raise("model_patches", model_patch)
|
|
state = comfy.utils.load_torch_file(path, safe_load=True)
|
|
if not isinstance(state, dict) or not state or len(state) > 100_000:
|
|
raise TransformError(
|
|
"diffusion_weight_delta requires a non-empty tensor state dict")
|
|
|
|
prefixes = ("model.diffusion_model.", "diffusion_model.")
|
|
normalized = {}
|
|
for source_key, value in state.items():
|
|
if not isinstance(source_key, str) or not isinstance(value, torch.Tensor):
|
|
raise TransformError(
|
|
"diffusion_weight_delta accepts tensor-only SafeTensors weights")
|
|
key = source_key
|
|
for prefix in prefixes:
|
|
if key.startswith(prefix):
|
|
key = key[len(prefix):]
|
|
break
|
|
if key in normalized:
|
|
raise TransformError(
|
|
f"diffusion_weight_delta has duplicate key {key!r}")
|
|
normalized[key] = value
|
|
|
|
diffusion = getattr(getattr(patcher, "model", None), "diffusion_model", None)
|
|
if diffusion is None or not callable(getattr(diffusion, "state_dict", None)):
|
|
raise TransformError(
|
|
"diffusion_weight_delta needs a model with diffusion weights")
|
|
target = diffusion.state_dict()
|
|
input_key = "input_blocks.0.0.weight"
|
|
patches = {}
|
|
for key, value in normalized.items():
|
|
existing = target.get(key)
|
|
if existing is None:
|
|
raise TransformError(
|
|
f"diffusion_weight_delta key {key!r} is absent from the model")
|
|
can_pad = bool(pad_input_channels and key == input_key)
|
|
if tuple(value.shape) != tuple(existing.shape):
|
|
valid_padding = (
|
|
can_pad
|
|
and value.ndim == existing.ndim == 4
|
|
and value.shape[0] == existing.shape[0]
|
|
and value.shape[1] >= existing.shape[1]
|
|
and tuple(value.shape[2:]) == tuple(existing.shape[2:])
|
|
)
|
|
if not valid_padding:
|
|
raise TransformError(
|
|
f"diffusion_weight_delta shape mismatch for {key!r}: "
|
|
f"{tuple(value.shape)} != {tuple(existing.shape)}")
|
|
patches["diffusion_model." + key] = (
|
|
"diff", (value, {"pad_weight": can_pad}))
|
|
|
|
result = patcher.clone()
|
|
loaded = result.add_patches(patches, float(strength))
|
|
if loaded is not None and len(loaded) != len(patches):
|
|
raise TransformError(
|
|
"diffusion_weight_delta did not match every model weight")
|
|
return result
|
|
|
|
|
|
def _serialized_model_patch(
|
|
patcher, model_patch: str, strength: float, pad_diff_weights: bool,
|
|
):
|
|
"""Apply a tensor-only serialized Comfy model patch.
|
|
|
|
Some model authors publish Comfy patch tuples in SafeTensors by encoding
|
|
``model-key::patch-type::slot`` in each tensor name. Parsing that small
|
|
interchange format is a reusable engine capability; the model-specific
|
|
orchestration that chooses a file and conditioning remains pack-side.
|
|
Only diffusion-model ``diff`` and ``lora`` patches are accepted here.
|
|
"""
|
|
import torch
|
|
import comfy.utils
|
|
import folder_paths
|
|
|
|
path = folder_paths.get_full_path_or_raise("model_patches", model_patch)
|
|
state = comfy.utils.load_torch_file(path, safe_load=True)
|
|
if not isinstance(state, dict) or not state or len(state) > 100_000:
|
|
raise TransformError(
|
|
"serialized_model_patch requires a non-empty tensor state dict")
|
|
|
|
grouped: dict[str, tuple[str, list[Any]]] = {}
|
|
for encoded_key, tensor in state.items():
|
|
if not isinstance(encoded_key, str) or not isinstance(tensor, torch.Tensor):
|
|
raise TransformError(
|
|
"serialized_model_patch accepts tensor-only SafeTensors weights")
|
|
parts = encoded_key.rsplit("::", 2)
|
|
if len(parts) != 3:
|
|
raise TransformError(
|
|
"serialized_model_patch tensor names must use "
|
|
"model-key::patch-type::slot")
|
|
model_key, patch_type, slot_text = parts
|
|
if (
|
|
not model_key.startswith("diffusion_model.")
|
|
or len(model_key) > 1024
|
|
or patch_type not in {"diff", "lora"}
|
|
):
|
|
raise TransformError(
|
|
"serialized_model_patch contains an unsupported model key or "
|
|
"patch type")
|
|
try:
|
|
slot = int(slot_text)
|
|
except ValueError as exc:
|
|
raise TransformError(
|
|
"serialized_model_patch slots must be integers") from exc
|
|
if not 0 <= slot < 16 or str(slot) != slot_text:
|
|
raise TransformError(
|
|
"serialized_model_patch slots must be canonical integers in "
|
|
"[0, 15]")
|
|
current = grouped.get(model_key)
|
|
if current is None:
|
|
current = (patch_type, [None] * 16)
|
|
grouped[model_key] = current
|
|
elif current[0] != patch_type:
|
|
raise TransformError(
|
|
f"serialized_model_patch mixes patch types for {model_key!r}")
|
|
if current[1][slot] is not None:
|
|
raise TransformError(
|
|
f"serialized_model_patch repeats slot {slot} for {model_key!r}")
|
|
current[1][slot] = tensor
|
|
|
|
patches = {}
|
|
for model_key, (patch_type, values) in grouped.items():
|
|
required_slots = (0,) if patch_type == "diff" else (0, 1)
|
|
if any(values[index] is None for index in required_slots):
|
|
raise TransformError(
|
|
f"serialized_model_patch has an incomplete {patch_type} patch "
|
|
f"for {model_key!r}")
|
|
if patch_type == "diff" and pad_diff_weights:
|
|
patches[model_key] = (
|
|
"diff", [values[0], {"pad_weight": True}])
|
|
else:
|
|
patches[model_key] = (patch_type, values)
|
|
|
|
result = patcher.clone()
|
|
loaded = result.add_patches(patches, float(strength))
|
|
if loaded is not None and len(loaded) != len(patches):
|
|
raise TransformError(
|
|
"serialized_model_patch did not match every model weight")
|
|
return result
|
|
|
|
|
|
def _layer_diffusion_attention_sharing(
|
|
patcher, model_patch: str, frames: int, control_image=None,
|
|
first_conditioning=None, second_conditioning=None,
|
|
third_conditioning=None,
|
|
):
|
|
"""Bridge the canonical SD1 Layer Diffusion attention-sharing patcher.
|
|
|
|
The attention implementation remains owned by ComfyUI-layerdiffuse. Core
|
|
supplies only a confined SafeTensor state dict and bounded typed inputs;
|
|
it does not carry a copy of the extension's model algorithm.
|
|
"""
|
|
import sys
|
|
import torch
|
|
import comfy.supported_models
|
|
import comfy.utils
|
|
import folder_paths
|
|
import nodes
|
|
|
|
config = getattr(getattr(patcher, "model", None), "model_config", None)
|
|
if not isinstance(
|
|
config, (comfy.supported_models.SD15, comfy.supported_models.SD20),
|
|
):
|
|
raise TransformError(
|
|
"layer_diffusion_attention_sharing requires an SD1.x model")
|
|
|
|
path = folder_paths.get_full_path_or_raise("model_patches", model_patch)
|
|
state = comfy.utils.load_torch_file(path, safe_load=True)
|
|
if (
|
|
not isinstance(state, dict)
|
|
or not state
|
|
or len(state) > 100_000
|
|
or any(
|
|
not isinstance(key, str) or not isinstance(value, torch.Tensor)
|
|
for key, value in state.items()
|
|
)
|
|
):
|
|
raise TransformError(
|
|
"Layer Diffusion attention weights must be a tensor-only "
|
|
"SafeTensors state dict")
|
|
|
|
node_class = getattr(nodes, "NODE_CLASS_MAPPINGS", {}).get(
|
|
"LayeredDiffusionApply")
|
|
module = (
|
|
None if node_class is None
|
|
else sys.modules.get(getattr(node_class, "__module__", ""))
|
|
)
|
|
attention_patcher = getattr(module, "AttentionSharingPatcher", None)
|
|
if not callable(attention_patcher):
|
|
raise TransformError(
|
|
"layer_diffusion_attention_sharing requires the host-installed "
|
|
"canonical ComfyUI-layerdiffuse extension")
|
|
|
|
control = None
|
|
if control_image is not None:
|
|
if (
|
|
not isinstance(control_image, torch.Tensor)
|
|
or control_image.ndim != 4
|
|
or not 1 <= control_image.shape[0] <= 64
|
|
or control_image.shape[-1] < 3
|
|
or control_image.numel() > 268_435_456
|
|
):
|
|
raise TransformError(
|
|
"Layer Diffusion control_image must be a bounded BHWC image")
|
|
control = control_image[..., :3].movedim(-1, 1)
|
|
|
|
result = patcher.clone()
|
|
adapter = attention_patcher(
|
|
result, int(frames), use_control=control is not None)
|
|
adapter.load_state_dict(state, strict=True)
|
|
if control is not None:
|
|
adapter.set_control(control)
|
|
|
|
conditionings = (
|
|
first_conditioning, second_conditioning, third_conditioning)
|
|
active = conditionings[:int(frames)]
|
|
if int(frames) > 1 or any(value is not None for value in active):
|
|
overwritten = []
|
|
for index, conditioning in enumerate(active):
|
|
if conditioning is None:
|
|
overwritten.append(None)
|
|
continue
|
|
if (
|
|
not isinstance(conditioning, (list, tuple))
|
|
or not conditioning
|
|
or not isinstance(conditioning[0], (list, tuple))
|
|
or not conditioning[0]
|
|
or not isinstance(conditioning[0][0], torch.Tensor)
|
|
):
|
|
raise TransformError(
|
|
"Layer Diffusion conditioning must contain a host "
|
|
f"embedding row at position {index}")
|
|
overwritten.append(conditioning[0][0])
|
|
result.model_options.setdefault("transformer_options", {})[
|
|
"cond_overwrite"] = overwritten
|
|
return result
|
|
|
|
|
|
def _concat_latent_input(patcher, latent):
|
|
"""Inject an encoded latent as bounded model ``c_concat`` input."""
|
|
import torch
|
|
|
|
samples = latent.get("samples") if isinstance(latent, dict) else None
|
|
if (
|
|
not isinstance(samples, torch.Tensor)
|
|
or samples.ndim != 4
|
|
or not 1 <= samples.shape[0] <= 64
|
|
or not 1 <= samples.shape[1] <= 64
|
|
or samples.shape[2] <= 0
|
|
or samples.shape[3] <= 0
|
|
or samples.numel() > 268_435_456
|
|
):
|
|
raise TransformError(
|
|
"concat_latent_input needs a bounded BCHW latent tensor")
|
|
latent_format = getattr(
|
|
getattr(getattr(patcher, "model", None), "model_config", None),
|
|
"latent_format", None)
|
|
scale_factor = getattr(latent_format, "scale_factor", None)
|
|
if not isinstance(scale_factor, (int, float)):
|
|
raise TransformError(
|
|
"concat_latent_input needs a model latent scale factor")
|
|
concat = torch.cat(
|
|
[item.detach().unsqueeze(0) for item in samples], dim=1,
|
|
) * float(scale_factor)
|
|
|
|
result = patcher.clone()
|
|
existing_wrapper = result.model_options.get("model_function_wrapper")
|
|
|
|
def wrapper(apply_model, args):
|
|
updated = dict(args)
|
|
conditioning = dict(args["c"])
|
|
sample = args["input"]
|
|
conditioning["c_concat"] = concat.to(sample).repeat(
|
|
sample.shape[0], 1, 1, 1)
|
|
updated["c"] = conditioning
|
|
if existing_wrapper is not None:
|
|
return existing_wrapper(apply_model, updated)
|
|
return apply_model(
|
|
x=updated["input"], t=updated["timestep"], **conditioning)
|
|
|
|
result.set_model_unet_function_wrapper(wrapper)
|
|
return result
|
|
|
|
|
|
def _sampling_memory_report(patcher):
|
|
import torch
|
|
from comfy import model_management
|
|
from comfy.patcher_extension import CallbacksMP
|
|
|
|
device = model_management.get_torch_device()
|
|
|
|
def reset(_model):
|
|
torch.cuda.reset_peak_memory_stats(device)
|
|
|
|
def report(_model):
|
|
allocated = torch.cuda.max_memory_allocated(device) / 1024 ** 3
|
|
reserved = torch.cuda.max_memory_reserved(device) / 1024 ** 3
|
|
logging.info(
|
|
"Sampling max allocated memory: max_memory=%.3f GB", allocated)
|
|
logging.info(
|
|
"Sampling max reserved memory: max_reserved=%.3f GB", reserved)
|
|
|
|
model = patcher.clone()
|
|
model.add_callback(CallbacksMP.ON_PRE_RUN, reset)
|
|
model.add_callback(CallbacksMP.ON_CLEANUP, report)
|
|
return model
|
|
|
|
|
|
def _riflex_rope(
|
|
patcher, architecture: str, num_frames: int, intrinsic_frequency: int,
|
|
):
|
|
import torch
|
|
from comfy import model_management
|
|
|
|
diffusion_model = patcher.model.diffusion_model
|
|
if architecture == "wan":
|
|
dim = diffusion_model.dim // diffusion_model.num_heads
|
|
theta = 10000.0
|
|
axes_dim = [
|
|
dim - 4 * (dim // 6),
|
|
2 * (dim // 6),
|
|
2 * (dim // 6),
|
|
]
|
|
patch_key = "diffusion_model.rope_embedder"
|
|
else:
|
|
params = diffusion_model.params
|
|
dim = params.hidden_size // params.num_heads
|
|
theta = params.theta
|
|
axes_dim = list(params.axes_dim)
|
|
patch_key = "diffusion_model.pe_embedder"
|
|
|
|
frequency_count = axes_dim[0] // 2
|
|
if intrinsic_frequency > frequency_count:
|
|
raise TransformError(
|
|
f"riflex_rope.intrinsic_frequency={intrinsic_frequency} exceeds "
|
|
f"the temporal axis's {frequency_count} frequencies")
|
|
|
|
class EmbedNDRifleX(torch.nn.Module):
|
|
def forward(self, ids):
|
|
embeddings = []
|
|
for axis in range(ids.shape[-1]):
|
|
pos = ids[..., axis]
|
|
if (model_management.is_device_mps(pos.device)
|
|
or model_management.is_intel_xpu()
|
|
or model_management.is_directml_enabled()):
|
|
device = torch.device("cpu")
|
|
else:
|
|
device = pos.device
|
|
axis_dim = axes_dim[axis]
|
|
scale = torch.linspace(
|
|
0, (axis_dim - 2) / axis_dim,
|
|
steps=axis_dim // 2,
|
|
dtype=torch.float64,
|
|
device=device,
|
|
)
|
|
omega = 1.0 / (theta ** scale)
|
|
if axis == 0:
|
|
omega[intrinsic_frequency - 1] = (
|
|
0.9 * 2 * torch.pi / num_frames)
|
|
out = torch.einsum(
|
|
"...n,d->...nd",
|
|
pos.to(dtype=torch.float32, device=device),
|
|
omega,
|
|
)
|
|
out = torch.stack(
|
|
(torch.cos(out), -torch.sin(out),
|
|
torch.sin(out), torch.cos(out)),
|
|
dim=-1,
|
|
)
|
|
embeddings.append(out.reshape(*out.shape[:-1], 2, 2).to(
|
|
dtype=torch.float32, device=pos.device))
|
|
return torch.cat(embeddings, dim=-3).unsqueeze(1)
|
|
|
|
model = patcher.clone()
|
|
model.add_object_patch(patch_key, EmbedNDRifleX())
|
|
return model
|
|
|
|
|
|
def _wan_skip_layer_guidance(
|
|
patcher, blocks: list[int], start_percent: float, end_percent: float,
|
|
):
|
|
import torch
|
|
|
|
def skip(args, extra_args):
|
|
transformer_options = extra_args.get("transformer_options", {})
|
|
original_block = extra_args["original_block"]
|
|
if not transformer_options:
|
|
raise ValueError(
|
|
"transformer_options is required for Wan skip-layer guidance")
|
|
current = transformer_options["current_percent"]
|
|
if not start_percent <= current <= end_percent:
|
|
return original_block(args)
|
|
if args["img"].shape[0] == 2:
|
|
previous_uncond = args["img"][0].unsqueeze(0)
|
|
conditional = {
|
|
"img": args["img"][1].unsqueeze(0),
|
|
"txt": args["txt"][1].unsqueeze(0),
|
|
"vec": args["vec"][1].unsqueeze(0),
|
|
"pe": args["pe"][1].unsqueeze(0),
|
|
}
|
|
block_out = original_block(conditional)
|
|
return {
|
|
"img": torch.cat((previous_uncond, block_out["img"]), dim=0),
|
|
"txt": args["txt"],
|
|
"vec": args["vec"],
|
|
"pe": args["pe"],
|
|
}
|
|
if transformer_options.get("cond_or_uncond") == [0]:
|
|
return original_block(args)
|
|
return args
|
|
|
|
model = patcher.clone()
|
|
for block_index in blocks:
|
|
transformer_options = model.model_options["transformer_options"].copy()
|
|
patches_replace = transformer_options.get("patches_replace", {}).copy()
|
|
dit = patches_replace.get("dit", {}).copy()
|
|
dit[("double_block", block_index)] = skip
|
|
patches_replace["dit"] = dit
|
|
transformer_options["patches_replace"] = patches_replace
|
|
model.model_options["transformer_options"] = transformer_options
|
|
return model
|
|
|
|
|
|
def _perturb_weights(
|
|
patcher, joint_blocks: float, final_layer: float,
|
|
rest_of_the_blocks: float, seed: int,
|
|
):
|
|
import copy
|
|
import torch
|
|
from comfy import model_management
|
|
from comfy.utils import ProgressBar
|
|
|
|
device = model_management.get_torch_device()
|
|
model = copy.deepcopy(patcher)
|
|
model.model.to(device)
|
|
state_dict = model.model.diffusion_model.state_dict()
|
|
generator = torch.Generator(device=device).manual_seed(seed)
|
|
progress = ProgressBar(len(state_dict))
|
|
for key, value in state_dict.items():
|
|
if key.startswith("joint_blocks"):
|
|
multiplier = joint_blocks
|
|
elif key.startswith("final_layer"):
|
|
multiplier = final_layer
|
|
else:
|
|
multiplier = rest_of_the_blocks
|
|
mean = torch.zeros_like(value) * value.mean()
|
|
std = torch.ones_like(value) * value.std() * multiplier
|
|
value.add_(torch.normal(mean, std, generator=generator).to(device))
|
|
progress.update(1)
|
|
model.model.diffusion_model.load_state_dict(state_dict)
|
|
return model
|
|
|
|
|
|
def _hunyuan_concat_image(patcher):
|
|
model = patcher.clone()
|
|
model.add_object_patch("concat_keys", ("concat_image",))
|
|
return model
|
|
|
|
|
|
def _flux_block_scales(patcher, double_blocks, single_blocks):
|
|
"""Scale complete Flux double/single blocks by bounded layer tables."""
|
|
import re
|
|
|
|
double = list(double_blocks) + [1.0] * (19 - len(double_blocks))
|
|
single = list(single_blocks) + [1.0] * (38 - len(single_blocks))
|
|
model = patcher.clone()
|
|
for key in patcher.model_state_dict():
|
|
match = re.search(
|
|
r"double_blocks\.(\d+)\.(img|txt)_(mod|attn|mlp)\."
|
|
r"(lin|qkv|proj|0|2)\.(weight|bias)",
|
|
key,
|
|
)
|
|
scale = None
|
|
if match is not None:
|
|
index = int(match.group(1))
|
|
if index < len(double):
|
|
scale = double[index]
|
|
else:
|
|
match = re.search(
|
|
r"single_blocks\.(\d+)\."
|
|
r"(linear[12]|modulation\.lin)\.(weight|bias)",
|
|
key,
|
|
)
|
|
if match is not None:
|
|
index = int(match.group(1))
|
|
if index < len(single):
|
|
scale = single[index]
|
|
if scale is not None and scale != 1.0:
|
|
model.add_patches({key: (None,)}, 0.0, scale)
|
|
return model
|
|
|
|
|
|
def _guidance_timestepping(patcher, value, start_at, end_at):
|
|
sigma_start = patcher.get_model_object(
|
|
"model_sampling").percent_to_sigma(start_at)
|
|
sigma_end = patcher.get_model_object(
|
|
"model_sampling").percent_to_sigma(end_at)
|
|
|
|
def guidance(args):
|
|
cond = args["cond"]
|
|
uncond = args["uncond"]
|
|
cond_scale = args["cond_scale"]
|
|
sigma = args["sigma"].detach().cpu()[0].item()
|
|
if sigma <= sigma_start and sigma > sigma_end:
|
|
cond_scale = value
|
|
return uncond + (cond - uncond) * cond_scale
|
|
|
|
model = patcher.clone()
|
|
model.set_model_sampler_cfg_function(guidance)
|
|
return model
|
|
|
|
|
|
def _sd3_advanced_sampling(patcher, shift, cut_off, shift_multiplier):
|
|
import torch
|
|
import comfy.model_sampling
|
|
|
|
class ModelSamplingDiscreteFlowCustom(torch.nn.Module):
|
|
def __init__(self, model_config=None):
|
|
super().__init__()
|
|
settings = (
|
|
model_config.sampling_settings
|
|
if model_config is not None else {}
|
|
)
|
|
self.set_parameters(
|
|
shift=settings.get("shift", 1.0),
|
|
multiplier=settings.get("multiplier", 1000),
|
|
)
|
|
|
|
def set_parameters(
|
|
self, shift=1.0, timesteps=1000, multiplier=1000,
|
|
cut_off=1.0, shift_multiplier=0,
|
|
):
|
|
self.shift = shift
|
|
self.multiplier = multiplier
|
|
self.cut_off = cut_off
|
|
self.shift_multiplier = shift_multiplier
|
|
timesteps_tensor = self.sigma(
|
|
(torch.arange(1, timesteps + 1, 1) / timesteps) * multiplier
|
|
)
|
|
self.register_buffer("sigmas", timesteps_tensor)
|
|
|
|
@property
|
|
def sigma_min(self):
|
|
return self.sigmas[0]
|
|
|
|
@property
|
|
def sigma_max(self):
|
|
return self.sigmas[-1]
|
|
|
|
def timestep(self, sigma):
|
|
return sigma * self.multiplier
|
|
|
|
def sigma(self, timestep):
|
|
current_shift = self.shift
|
|
if timestep.dim() == 0:
|
|
normalized = timestep.cpu().item() / self.multiplier
|
|
if normalized <= self.cut_off:
|
|
current_shift *= self.shift_multiplier
|
|
return comfy.model_sampling.time_snr_shift(
|
|
current_shift, timestep / self.multiplier
|
|
)
|
|
|
|
def percent_to_sigma(self, percent):
|
|
if percent <= 0.0:
|
|
return 1.0
|
|
if percent >= 1.0:
|
|
return 0.0
|
|
return 1.0 - percent
|
|
|
|
class ModelSamplingAdvanced(
|
|
ModelSamplingDiscreteFlowCustom, comfy.model_sampling.CONST
|
|
):
|
|
pass
|
|
|
|
model = patcher.clone()
|
|
model_sampling = ModelSamplingAdvanced(patcher.model.model_config)
|
|
model_sampling.set_parameters(
|
|
shift=shift,
|
|
multiplier=1000,
|
|
cut_off=cut_off,
|
|
shift_multiplier=shift_multiplier,
|
|
)
|
|
model.add_object_patch("model_sampling", model_sampling)
|
|
return model
|
|
|
|
|
|
def _flux_sampler_sampling(patcher, max_shift, base_shift, width, height):
|
|
import comfy.model_base
|
|
import comfy.model_sampling
|
|
|
|
if patcher.model.model_type == comfy.model_base.ModelType.FLOW:
|
|
sampling_base = comfy.model_sampling.ModelSamplingDiscreteFlow
|
|
shift = base_shift
|
|
multiplier = 1.0
|
|
else:
|
|
x1 = 256
|
|
x2 = 4096
|
|
slope = (max_shift - base_shift) / (x2 - x1)
|
|
intercept = base_shift - slope * x1
|
|
shift = (width * height / (8 * 8 * 2 * 2)) * slope + intercept
|
|
sampling_base = comfy.model_sampling.ModelSamplingFlux
|
|
multiplier = None
|
|
|
|
class ModelSamplingAdvanced(sampling_base, comfy.model_sampling.CONST):
|
|
pass
|
|
|
|
model = patcher.clone()
|
|
model_sampling = ModelSamplingAdvanced(patcher.model.model_config)
|
|
if multiplier is None:
|
|
model_sampling.set_parameters(shift=shift)
|
|
else:
|
|
model_sampling.set_parameters(shift=shift, multiplier=multiplier)
|
|
original = model.get_model_object("model_sampling")
|
|
if hasattr(original, "noise_scale"):
|
|
model_sampling.set_noise_scale(original.noise_scale)
|
|
model.add_object_patch("model_sampling", model_sampling)
|
|
return model
|
|
|
|
|
|
def _latent_inpaint_ttm(patcher, steps: int, mask=None):
|
|
import torch
|
|
from comfy.patcher_extension import WrappersMP
|
|
from comfy.sampler_helpers import prepare_mask
|
|
|
|
class ApplyModelWrapper:
|
|
def __init__(
|
|
self, reference_samples, noise, motion_mask, scale_latent_inpaint,
|
|
):
|
|
self.reference_samples = reference_samples
|
|
self.noise = noise
|
|
self.motion_mask = motion_mask
|
|
self.scale_latent_inpaint = scale_latent_inpaint
|
|
|
|
def __call__(
|
|
self, executor, x, timestep, c_concat, c_crossattn, control,
|
|
transformer_options, **kwargs,
|
|
):
|
|
sigmas = transformer_options["sample_sigmas"]
|
|
matched = (sigmas == timestep).nonzero(as_tuple=True)[0]
|
|
if matched.numel() > 0:
|
|
current_step = matched.item()
|
|
else:
|
|
crossing = (
|
|
(sigmas[:-1] - timestep)
|
|
* (sigmas[1:] - timestep) <= 0
|
|
).nonzero(as_tuple=True)[0]
|
|
current_step = crossing.item() if crossing.numel() > 0 else 0
|
|
next_sigma = sigmas[
|
|
current_step + 1
|
|
if current_step < len(sigmas) - 1 else current_step]
|
|
if current_step != 0 and current_step < steps:
|
|
noisy_latent = self.scale_latent_inpaint(
|
|
x=x,
|
|
sigma=torch.tensor([next_sigma]),
|
|
noise=self.noise.to(x),
|
|
latent_image=self.reference_samples.to(x),
|
|
)
|
|
if self.motion_mask is None:
|
|
x = noisy_latent
|
|
else:
|
|
motion_mask = self.motion_mask.to(x)
|
|
x = x * (1 - motion_mask) + noisy_latent * motion_mask
|
|
return executor(
|
|
x, timestep, c_concat, c_crossattn, control,
|
|
transformer_options, **kwargs)
|
|
|
|
class OuterSampleWrapper:
|
|
def __call__(
|
|
self, executor, noise, latent_image, sampler, sigmas,
|
|
denoise_mask, callback, disable_pbar, seed, latent_shapes,
|
|
):
|
|
guider = executor.class_obj
|
|
wrappers = guider.model_options["transformer_options"][
|
|
"wrappers"]
|
|
apply_wrappers = wrappers.setdefault(WrappersMP.APPLY_MODEL, {})
|
|
motion_mask = None
|
|
if mask is not None:
|
|
motion_mask = mask.reshape(
|
|
(-1, 1, mask.shape[-2], mask.shape[-1]))
|
|
motion_mask = prepare_mask(
|
|
motion_mask, latent_shapes[0], noise.device)
|
|
scale_latent_inpaint = (
|
|
guider.model_patcher.model.scale_latent_inpaint)
|
|
apply_wrappers["TTM_ApplyModel_Wrapper"] = [ApplyModelWrapper(
|
|
latent_image, noise, motion_mask, scale_latent_inpaint)]
|
|
return executor(
|
|
noise, latent_image, sampler, sigmas, denoise_mask, callback,
|
|
disable_pbar, seed, latent_shapes=latent_shapes)
|
|
|
|
model = patcher.clone()
|
|
model.add_wrapper_with_key(
|
|
WrappersMP.OUTER_SAMPLE,
|
|
"TTM_OuterSampleWrapper",
|
|
OuterSampleWrapper(),
|
|
)
|
|
return model
|
|
|
|
|
|
def _leapfusion_hunyuan_i2v(
|
|
patcher, latent, index: int, strength: float,
|
|
start_percent: float, end_percent: float,
|
|
):
|
|
import torch
|
|
|
|
samples = latent.get("samples") if isinstance(latent, dict) else None
|
|
if not isinstance(samples, torch.Tensor) or samples.ndim != 5:
|
|
raise TransformError(
|
|
"leapfusion_hunyuan_i2v.latent must contain a 5D samples tensor")
|
|
if not -samples.shape[2] <= index < samples.shape[2]:
|
|
raise TransformError(
|
|
f"leapfusion_hunyuan_i2v.index={index} is outside "
|
|
f"the latent's {samples.shape[2]} frames")
|
|
replacement = samples * 0.476986 * strength
|
|
|
|
def unet_wrapper(apply_model, args):
|
|
sigmas = args["c"]["transformer_options"]["sample_sigmas"]
|
|
image = args["input"]
|
|
timestep = args["timestep"]
|
|
conditioning = args["c"]
|
|
matched = (sigmas == timestep).nonzero()
|
|
if len(matched) > 0:
|
|
current_step = matched.item()
|
|
else:
|
|
current_step = 0
|
|
for step in range(len(sigmas) - 1):
|
|
if ((sigmas[step] - timestep[0])
|
|
* (sigmas[step + 1] - timestep[0])) <= 0:
|
|
current_step = step
|
|
break
|
|
current_percent = current_step / (len(sigmas) - 1)
|
|
if start_percent <= current_percent <= end_percent:
|
|
image[:, :, [index], :, :] = replacement[:, :, [0], :, :].to(
|
|
image)
|
|
else:
|
|
image[:, :, [index], :, :] = torch.zeros(1)
|
|
return apply_model(image, timestep, **conditioning)
|
|
|
|
model = patcher.clone()
|
|
model.set_model_unet_function_wrapper(unet_wrapper)
|
|
return model
|
|
|
|
|
|
def _spatial_tiled_evaluation(
|
|
patcher, rows: int, columns: int, overlap: float,
|
|
overlap_x: int, overlap_y: int, blend: str,
|
|
preserve_existing: bool,
|
|
):
|
|
"""Evaluate one denoise prediction over a bounded overlapping grid.
|
|
|
|
This is the common MultiDiffusion primitive only: callers still own image
|
|
encoding, grid options, sampling, decoding, and output policy. Spatially
|
|
aware model patches receive a closed tile descriptor through transformer
|
|
options so vendor integrations can crop their own control inputs.
|
|
"""
|
|
if rows * columns > 256:
|
|
raise TransformError(
|
|
"spatial_tiled_evaluation supports at most 256 tiles")
|
|
if blend != "linear": # guarded by OneOf; defensive for direct callers
|
|
raise TransformError("unsupported spatial tile blend policy")
|
|
|
|
old_wrapper = patcher.model_options.get("model_function_wrapper")
|
|
|
|
def spans(total: int, count: int, extension: int):
|
|
if count > total:
|
|
raise TransformError(
|
|
"spatial tile grid exceeds the latent dimensions")
|
|
base = max(1, total // count)
|
|
return [
|
|
(
|
|
max(0, 0 if index == 0 else index * base - extension),
|
|
min(total, total if index == count - 1
|
|
else (index + 1) * base + extension),
|
|
)
|
|
for index in range(count)
|
|
]
|
|
|
|
def weight_1d(length, taper_left, taper_right, device, dtype):
|
|
import torch
|
|
|
|
weight = torch.ones(length, device=device, dtype=dtype)
|
|
left = min(int(taper_left), length // 2)
|
|
right = min(int(taper_right), length // 2)
|
|
if left:
|
|
weight[:left] = torch.linspace(
|
|
0, 1, left + 2, device=device, dtype=dtype)[1:-1]
|
|
if right:
|
|
weight[length - right:] = torch.linspace(
|
|
1, 0, right + 2, device=device, dtype=dtype)[1:-1]
|
|
return weight
|
|
|
|
cache: dict[tuple, list[dict[str, Any]]] = {}
|
|
|
|
def wrapper(apply_model, args):
|
|
import torch
|
|
|
|
x_in = args.get("input")
|
|
timestep = args.get("timestep")
|
|
conditioning = args.get("c")
|
|
if (not isinstance(x_in, torch.Tensor) or x_in.ndim < 4
|
|
or not isinstance(conditioning, dict)):
|
|
raise TransformError(
|
|
"spatial_tiled_evaluation requires a spatial model input")
|
|
height, width = int(x_in.shape[-2]), int(x_in.shape[-1])
|
|
if rows > height or columns > width:
|
|
raise TransformError(
|
|
"spatial tile grid exceeds the latent dimensions")
|
|
tile_height = max(1, height // rows)
|
|
tile_width = max(1, width // columns)
|
|
extend_y = (0 if rows == 1 else min(
|
|
int(tile_height * overlap) + overlap_y, tile_height // 2))
|
|
extend_x = (0 if columns == 1 else min(
|
|
int(tile_width * overlap) + overlap_x, tile_width // 2))
|
|
key = (
|
|
x_in.device, x_in.dtype, height, width,
|
|
rows, columns, extend_y, extend_x,
|
|
)
|
|
tiles = cache.get(key)
|
|
if tiles is None:
|
|
ys = spans(height, rows, extend_y)
|
|
xs = spans(width, columns, extend_x)
|
|
tiles = []
|
|
for row, (top, bottom) in enumerate(ys):
|
|
taper_top = ys[row - 1][1] - top if row else 0
|
|
taper_bottom = (
|
|
bottom - ys[row + 1][0] if row < rows - 1 else 0)
|
|
wy = weight_1d(
|
|
bottom - top, taper_top, taper_bottom,
|
|
x_in.device, x_in.dtype)
|
|
for column, (left, right) in enumerate(xs):
|
|
taper_left = xs[column - 1][1] - left if column else 0
|
|
taper_right = (
|
|
right - xs[column + 1][0]
|
|
if column < columns - 1 else 0)
|
|
wx = weight_1d(
|
|
right - left, taper_left, taper_right,
|
|
x_in.device, x_in.dtype)
|
|
tiles.append({
|
|
"top": top, "bottom": bottom,
|
|
"left": left, "right": right,
|
|
"weight": wy[:, None] * wx[None, :],
|
|
})
|
|
cache[key] = tiles
|
|
|
|
accumulated = torch.zeros_like(x_in)
|
|
leading = (1,) * (x_in.ndim - 2)
|
|
weight_sum = torch.zeros(
|
|
leading + (height, width),
|
|
device=x_in.device, dtype=x_in.dtype)
|
|
for tile in tiles:
|
|
top, bottom = tile["top"], tile["bottom"]
|
|
left, right = tile["left"], tile["right"]
|
|
tile_input = x_in[..., top:bottom, left:right]
|
|
tile_conditioning = dict(conditioning)
|
|
transformer_options = dict(
|
|
conditioning.get("transformer_options") or {})
|
|
transformer_options["spatial_tile"] = {
|
|
"top": top, "bottom": bottom,
|
|
"left": left, "right": right,
|
|
"source_height": height, "source_width": width,
|
|
}
|
|
tile_conditioning["transformer_options"] = transformer_options
|
|
if preserve_existing and old_wrapper is not None:
|
|
prediction = old_wrapper(apply_model, {
|
|
"input": tile_input,
|
|
"timestep": timestep,
|
|
"c": tile_conditioning,
|
|
})
|
|
else:
|
|
prediction = apply_model(
|
|
tile_input, timestep, **tile_conditioning)
|
|
if (not isinstance(prediction, torch.Tensor)
|
|
or prediction.shape != tile_input.shape):
|
|
raise TransformError(
|
|
"spatial tile model prediction has an invalid shape")
|
|
tile_weight = tile["weight"].view(
|
|
leading + tile["weight"].shape)
|
|
accumulated[..., top:bottom, left:right] += (
|
|
prediction * tile_weight)
|
|
weight_sum[..., top:bottom, left:right] += tile_weight
|
|
return accumulated / weight_sum.clamp(min=1e-6)
|
|
|
|
model = patcher.clone()
|
|
model.set_model_unet_function_wrapper(wrapper)
|
|
return model
|
|
|
|
|
|
TRANSFORMS: dict[str, Transform] = {
|
|
t.name: t for t in (
|
|
Transform(
|
|
"block_swap",
|
|
"Keep only some transformer blocks on the compute device, "
|
|
"offloading the rest to system RAM so a large video model fits.",
|
|
{
|
|
"blocks_to_swap": Int(
|
|
0, 64,
|
|
doc="How many leading transformer blocks to offload. "
|
|
"Clamped to the model's actual block count."),
|
|
"offload_img_emb": Bool(
|
|
default=False,
|
|
doc="Also offload the image embedding module."),
|
|
"offload_txt_emb": Bool(
|
|
default=False,
|
|
doc="Also offload the text embedding module."),
|
|
"use_non_blocking": Bool(
|
|
default=False,
|
|
doc="Use non-blocking transfers: faster, more pinned RAM."),
|
|
},
|
|
_block_swap,
|
|
),
|
|
Transform(
|
|
"attention_impl",
|
|
"Select which attention implementation this model uses.",
|
|
{
|
|
"mode": OneOf(_ATTENTION_IMPLS,
|
|
doc="Which of core's attention implementations to use."),
|
|
"allow_compile": Bool(default=False,
|
|
doc="Let torch.compile trace into the attention "
|
|
"function. Off by default, matching core."),
|
|
},
|
|
_attention_impl,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"sage_attention_variant",
|
|
"Select an exact KJ SageAttention kernel for this model.",
|
|
{
|
|
"mode": OneOf(
|
|
_SAGE_VARIANTS,
|
|
doc="Exact SageAttention kernel variant, or disabled."),
|
|
"allow_compile": Bool(
|
|
default=False,
|
|
doc="Let torch.compile trace into the selected kernel."),
|
|
},
|
|
_sage_attention_variant,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"strict_flash_attention",
|
|
"Use FlashAttention 2 or 3 without an SDPA fallback.",
|
|
{
|
|
"allow_compile": Bool(
|
|
default=False,
|
|
doc="Let torch.compile trace into the FlashAttention call."),
|
|
},
|
|
_strict_flash_attention,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"kohya_deep_shrink",
|
|
"Apply core's Kohya Deep Shrink UNet patch.",
|
|
{
|
|
"block_number": Int(1, 32, doc="UNet input block index."),
|
|
"downscale_factor": Float(
|
|
0.1, 9.0, doc="Temporary feature downscale factor."),
|
|
"start_percent": Float(
|
|
0.0, 1.0, doc="First sampling percentage."),
|
|
"end_percent": Float(
|
|
0.0, 1.0, doc="Last sampling percentage."),
|
|
"downscale_after_skip": Bool(
|
|
doc="Patch after the skip connection."),
|
|
"downscale_method": OneOf(
|
|
("bicubic", "nearest-exact", "bilinear", "area", "bislerp"),
|
|
doc="Core feature downscale method."),
|
|
"upscale_method": OneOf(
|
|
("bicubic", "nearest-exact", "bilinear", "area", "bislerp"),
|
|
doc="Core feature restore method."),
|
|
},
|
|
_kohya_deep_shrink,
|
|
),
|
|
Transform(
|
|
"nabla_sparse_attention",
|
|
"Apply KJ's NABLA spatiotemporal sparse-attention policy.",
|
|
{
|
|
"latent": RefOf(
|
|
"LATENT",
|
|
doc="Latent whose video dimensions define the sparse mask."),
|
|
"window_time": Int(
|
|
1, 100000, doc="Temporal local-attention window."),
|
|
"window_width": Int(
|
|
1, 100000, doc="Horizontal local-attention window."),
|
|
"window_height": Int(
|
|
1, 100000, doc="Vertical local-attention window."),
|
|
"sparsity": Float(
|
|
0.0, 1.0, doc="Attention mass retained per sparse block."),
|
|
"compile_attention": Bool(
|
|
default=True,
|
|
doc="Compile the host-owned NABLA attention override."),
|
|
},
|
|
_nabla_sparse_attention,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"enhance_a_video",
|
|
"Apply KJ's temporal Enhance-A-Video attention policy.",
|
|
{
|
|
"latent": RefOf(
|
|
"LATENT", doc="Latent defining the video frame count."),
|
|
"architecture": OneOf(
|
|
("wan", "ltx"), doc="Core-owned attention layout."),
|
|
"weight": Float(
|
|
0.0, 100.0, doc="Temporal-attention enhancement strength."),
|
|
},
|
|
_enhance_a_video,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"wan_video_nag",
|
|
"Apply KJ's normalized-attention guidance to Wan cross-attention.",
|
|
{
|
|
"conditioning": RefOf(
|
|
"CONDITIONING",
|
|
doc="Negative-guidance conditioning embedding."),
|
|
"nag_scale": Float(
|
|
0.0, 100.0, doc="Negative-guidance strength."),
|
|
"nag_alpha": Float(
|
|
0.0, 1.0, doc="Mix with the positive attention output."),
|
|
"nag_tau": Float(
|
|
0.0, 10.0, doc="L1 guidance clipping threshold."),
|
|
"input_type": OneOf(
|
|
("default", "batch"), default="default",
|
|
doc="Wan conditioning batch interpretation."),
|
|
"inplace": Bool(
|
|
default=False,
|
|
doc="Use KJ's lower-memory in-place arithmetic."),
|
|
},
|
|
_wan_video_nag,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"krea2_token_weights",
|
|
"Apply Krea2 per-token attention value and key weighting.",
|
|
{
|
|
"weights": TokenWeights(
|
|
4096,
|
|
doc="Conditioning positions with value factor and key bias."),
|
|
},
|
|
_krea2_token_weights,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"ltx2_audio_normalization",
|
|
"Normalize LTX2 audio latents between configured sample steps.",
|
|
{
|
|
"factors": FloatList(
|
|
-1000.0, 1000.0, 10000,
|
|
doc="Per-step audio-latent multipliers."),
|
|
},
|
|
_ltx2_audio_normalization,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"ltx2_nag",
|
|
"Apply normalized-attention guidance to LTX2 video or audio.",
|
|
{
|
|
"nag_scale": Float(
|
|
0.0, 100.0, doc="Negative-guidance strength."),
|
|
"nag_alpha": Float(
|
|
0.0, 1.0, doc="Mix with positive attention."),
|
|
"nag_tau": Float(
|
|
0.0, 10.0, doc="L1 guidance clipping threshold."),
|
|
"video_conditioning": RefOf(
|
|
"CONDITIONING", default=None,
|
|
doc="Optional LTX2 video negative conditioning."),
|
|
"audio_conditioning": RefOf(
|
|
"CONDITIONING", default=None,
|
|
doc="Optional LTX2 audio negative conditioning."),
|
|
"inplace": Bool(
|
|
default=True,
|
|
doc="Use KJ's lower-memory in-place arithmetic."),
|
|
},
|
|
_ltx2_nag,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"ideogram4_optimizations",
|
|
"Bound Ideogram4 feed-forward and RoPE activation memory.",
|
|
{
|
|
"chunk_ffn": Bool(
|
|
default=True,
|
|
doc="Chunk long feed-forward token sequences."),
|
|
"ffn_chunks": Int(
|
|
1, 64, default=2,
|
|
doc="Number of token-sequence chunks."),
|
|
"ffn_seq_threshold": Int(
|
|
256, 65536, default=1024,
|
|
doc="Minimum sequence length to chunk."),
|
|
"bf16_rope": Bool(
|
|
default=True,
|
|
doc="Keep rotary activations in the input dtype."),
|
|
},
|
|
_ideogram4_optimizations,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"ltx2_attention_tuner",
|
|
"Scale LTX2 video and audio attention paths per block.",
|
|
{
|
|
"blocks": IntList(
|
|
4096,
|
|
doc="Selected transformer blocks; empty selects all."),
|
|
"video_scale": Float(
|
|
0.0, 100.0, doc="Video self and text-attention scale."),
|
|
"audio_scale": Float(
|
|
0.0, 100.0, doc="Audio self and text-attention scale."),
|
|
"audio_to_video_scale": Float(
|
|
0.0, 100.0, doc="Audio-to-video attention scale."),
|
|
"video_to_audio_scale": Float(
|
|
0.0, 100.0, doc="Video-to-audio attention scale."),
|
|
"triton_kernels": Bool(
|
|
default=True,
|
|
doc="Use fused kernels when the host supports them."),
|
|
},
|
|
_ltx2_attention_tuner,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"memory_efficient_sage",
|
|
"Use architecture-specific SageAttention with bounded buffers.",
|
|
{
|
|
"architecture": OneOf(
|
|
("ltx2", "minimax", "wan"),
|
|
doc="Core-owned model architecture policy."),
|
|
"triton_kernels": Bool(
|
|
default=True,
|
|
doc="Use fused rotary kernels when supported."),
|
|
},
|
|
_memory_efficient_sage,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"minimax_chunk_feed_forward",
|
|
"Chunk MiniMax H3 packed-token feed-forward activations.",
|
|
{
|
|
"chunks": Int(
|
|
1, 64, doc="Number of packed-token chunks."),
|
|
"seq_threshold": Int(
|
|
256, 262144,
|
|
doc="Only chunk longer packed-token sequences."),
|
|
},
|
|
_minimax_chunk_feed_forward,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"minimax_low_vram_attention",
|
|
"Release MiniMax H3 attention buffers early and group heads.",
|
|
{
|
|
"head_chunks": Int(
|
|
1, 56, doc="Number of independent head groups."),
|
|
},
|
|
_minimax_low_vram_attention,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"matmul_fp16_accumulation",
|
|
"Set torch's fp16 matmul accumulation for the duration of the run.",
|
|
{"enabled": Bool(doc="Whether to allow fp16 accumulation.")},
|
|
_matmul_fp16_accumulation,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"memory_usage_factor",
|
|
"Override the model's memory-usage estimate during sampling.",
|
|
{"factor": Float(0.0, 100.0,
|
|
doc="Multiplier replacing the model's own estimate.")},
|
|
_memory_usage_factor,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"ffn_chunking",
|
|
"Chunk feed-forward activations to cut peak VRAM.",
|
|
{
|
|
"chunks": Int(1, 100, doc="Number of chunks. 1 is a no-op."),
|
|
"dim_threshold": Int(0, 16384, default=4096,
|
|
doc="Only chunk sequences longer than this."),
|
|
"target": OneOf(
|
|
_FFN_TARGETS, default="blocks_ffn",
|
|
doc="Core-owned feed-forward model layout."),
|
|
},
|
|
_ffn_chunking,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"compile",
|
|
"torch.compile the diffusion model.",
|
|
{
|
|
"backend": OneOf(("inductor", "cudagraphs", "eager", "aot_eager"),
|
|
default="inductor", doc="torch.compile backend."),
|
|
"mode": OneOf(("default", "reduce-overhead", "max-autotune",
|
|
"max-autotune-no-cudagraphs"),
|
|
default="default", doc="torch.compile mode."),
|
|
"fullgraph": Bool(default=False,
|
|
doc="Require a single graph with no breaks."),
|
|
"dynamic": NullableBool(
|
|
default=False,
|
|
doc="Allow dynamic shapes, or null for torch's automatic mode."),
|
|
"scope": OneOf(
|
|
_COMPILE_SCOPES,
|
|
default="whole",
|
|
doc="Core-owned model-module selection policy."),
|
|
"double_blocks": Bool(
|
|
default=True,
|
|
doc="Include Flux double blocks when scope is flux_blocks."),
|
|
"single_blocks": Bool(
|
|
default=True,
|
|
doc="Include Flux single blocks when scope is flux_blocks."),
|
|
"dynamo_cache_size_limit": Int(
|
|
0, 1024, default=None,
|
|
doc="Dynamo cache limit applied while compiling, or null to preserve it."),
|
|
"force_parameter_static_shapes": NullableBool(
|
|
default=None,
|
|
doc="Dynamo parameter-shape policy applied while compiling."),
|
|
"dynamic_vram": OneOf(
|
|
("disable", "preserve", "stabilize"),
|
|
default="disable",
|
|
doc="Clone policy for ComfyUI dynamic VRAM models."),
|
|
"guard_filter": Bool(
|
|
default=False,
|
|
doc="Ignore transformer_options guards in default compile mode."),
|
|
"debug_compile_keys": Bool(
|
|
default=False,
|
|
doc="Log the core-selected module keys before compiling."),
|
|
"default_mode": OneOf(
|
|
("omit", "explicit"),
|
|
default="omit",
|
|
doc="Whether to pass torch.compile's default mode explicitly."),
|
|
},
|
|
_compile,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"context_windows",
|
|
"Apply a closed context-window schedule and fuse policy.",
|
|
{
|
|
"context_schedule": OneOf(
|
|
_CONTEXT_SCHEDULES,
|
|
doc="Core-owned context-window placement policy."),
|
|
"fuse_method": OneOf(
|
|
_CONTEXT_FUSE_METHODS,
|
|
doc="Core-owned overlap blending policy."),
|
|
"context_length": Int(
|
|
1, 100000, doc="Latent frames in each window."),
|
|
"context_overlap": Int(
|
|
0, 100000, doc="Latent-frame overlap between windows."),
|
|
"context_stride": Int(
|
|
1, 32, doc="Maximum stride power for uniform schedules."),
|
|
"closed_loop": Bool(
|
|
doc="Allow looped schedules to wrap to frame zero."),
|
|
"dim": Int(
|
|
0, 5, doc="Temporal dimension in the model latent."),
|
|
"freenoise": Bool(
|
|
doc="Install core's FreeNoise sampler wrapper."),
|
|
"causal_window_fix": Bool(
|
|
doc="Prepend and then strip the prior causal frame."),
|
|
"cond_retain_indices": IntList(
|
|
4096, default=(),
|
|
doc="Window-relative conditioning indices to retain."),
|
|
},
|
|
_context_windows,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"cfg_zero_star",
|
|
"Apply CFG-Zero* guidance and optional initial-step zeroing.",
|
|
{
|
|
"use_zero_init": Bool(
|
|
default=True, doc="Zero the initial guided predictions."),
|
|
"zero_init_steps": Int(
|
|
0, 100000, default=0,
|
|
doc="Last zero-based step whose prediction is zeroed."),
|
|
},
|
|
_cfg_zero_star,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"pid_color_bias",
|
|
"Apply the calibrated PiD Flux2 first-step color correction.",
|
|
{
|
|
"strength": Float(
|
|
-20.0, 20.0, default=1.0,
|
|
doc="Multiplier applied to the calibrated RGB bias."),
|
|
"backbone": OneOf(
|
|
("flux2",), default="flux2",
|
|
doc="Calibrated diffusion backbone."),
|
|
},
|
|
_pid_color_bias,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"dynamic_thresholding",
|
|
"Apply canonical Dynamic Thresholding to classifier-free guidance.",
|
|
{
|
|
"mimic_scale": Float(
|
|
0.0, 100.0, default=7.0,
|
|
doc="Target CFG scale whose variability is mimicked."),
|
|
"threshold_percentile": Float(
|
|
0.0, 1.0, default=1.0,
|
|
doc="Absolute-deviation quantile used for clipping."),
|
|
"mimic_mode": OneOf(
|
|
_DYNAMIC_THRESHOLD_MODES, default="Constant",
|
|
doc="Schedule applied to mimic_scale."),
|
|
"mimic_scale_min": Float(
|
|
0.0, 100.0, default=0.0,
|
|
doc="Minimum scheduled mimic scale."),
|
|
"cfg_mode": OneOf(
|
|
_DYNAMIC_THRESHOLD_MODES, default="Constant",
|
|
doc="Schedule applied to the sampler CFG scale."),
|
|
"cfg_scale_min": Float(
|
|
0.0, 100.0, default=0.0,
|
|
doc="Minimum scheduled CFG scale."),
|
|
"schedule_value": Float(
|
|
0.0, 100.0, default=1.0,
|
|
doc="Power or repetition value for scheduled modes."),
|
|
"separate_feature_channels": Bool(
|
|
default=True,
|
|
doc="Measure variability independently per channel."),
|
|
"scaling_startpoint": OneOf(
|
|
("MEAN", "ZERO"), default="MEAN",
|
|
doc="Center scaling on each channel mean or zero."),
|
|
"variability_measure": OneOf(
|
|
("AD", "STD"), default="AD",
|
|
doc="Use absolute deviation or standard deviation."),
|
|
"interpolate_phi": Float(
|
|
0.0, 1.0, default=1.0,
|
|
doc="Blend thresholded guidance with ordinary CFG."),
|
|
},
|
|
_dynamic_thresholding,
|
|
),
|
|
Transform(
|
|
"style_aligned_batch",
|
|
"Share reference style statistics across a classic UNet batch.",
|
|
{
|
|
"share_norm": OneOf(
|
|
("both", "group", "layer", "disabled"),
|
|
default="both",
|
|
doc="Normalization families that share reference stats."),
|
|
"share_attention": OneOf(
|
|
("q+k", "q+k+v", "disabled"),
|
|
default="q+k",
|
|
doc="Attention tensors receiving reference AdaIN."),
|
|
"scale": Float(
|
|
0.0, 1.0, default=1.0,
|
|
doc="Reference-key scale for non-reference images."),
|
|
},
|
|
_style_aligned_batch,
|
|
),
|
|
Transform(
|
|
"controlnet_lllite",
|
|
"Apply a canonical legacy SD ControlNet-LLLite adapter.",
|
|
{
|
|
"adapter": SafeTensorName(
|
|
doc="Logical adapter name in the controlnet catalogue."),
|
|
"image": RefOf(
|
|
"IMAGE", doc="RGB control image batch."),
|
|
"strength": Float(
|
|
0.0, 10.0, default=1.0,
|
|
doc="LLLite residual multiplier."),
|
|
"steps": Int(
|
|
0, 200, default=0,
|
|
doc="Sampler step count, or zero for no step window."),
|
|
"start_percent": Float(
|
|
0.0, 100.0, default=0.0,
|
|
doc="First active percentage of the sampling run."),
|
|
"end_percent": Float(
|
|
0.0, 100.0, default=0.0,
|
|
doc="Last active percentage; zero means the run end."),
|
|
},
|
|
_controlnet_lllite,
|
|
),
|
|
Transform(
|
|
"differential_diffusion",
|
|
"Apply core's canonical differential-diffusion mask behavior.",
|
|
{"strength": Float(
|
|
0.0, 1.0, default=1.0,
|
|
doc="Blend strength for the binary differential mask.")},
|
|
_differential_diffusion,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"fooocus_inpaint",
|
|
"Apply Fooocus' canonical SDXL inpaint head and quantized delta.",
|
|
{
|
|
"latent": RefOf(
|
|
"LATENT", doc="Inpaint latent with a noise mask."),
|
|
"head": WeightArchiveName(
|
|
doc="Logical Fooocus head name in the inpaint catalogue."),
|
|
"patch": WeightArchiveName(
|
|
doc="Logical Fooocus patch name in the inpaint catalogue."),
|
|
},
|
|
_fooocus_inpaint,
|
|
),
|
|
Transform(
|
|
"diffusion_weight_delta",
|
|
"Apply a shape-checked SafeTensors diffusion-model delta.",
|
|
{
|
|
"model_patch": SafeTensorName(
|
|
doc="Logical name in the model_patches catalogue."),
|
|
"strength": Float(
|
|
-10.0, 10.0, default=1.0,
|
|
doc="Delta multiplier."),
|
|
"pad_input_channels": Bool(
|
|
default=False,
|
|
doc="Allow only the first convolution input to widen."),
|
|
},
|
|
_diffusion_weight_delta,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"serialized_model_patch",
|
|
"Apply a tensor-only serialized Comfy diffusion-model patch.",
|
|
{
|
|
"model_patch": SafeTensorName(
|
|
doc="Logical name in the model_patches catalogue."),
|
|
"strength": Float(
|
|
-10.0, 10.0, default=1.0,
|
|
doc="Patch multiplier."),
|
|
"pad_diff_weights": Bool(
|
|
default=False,
|
|
doc="Allow serialized diff patches to widen inputs."),
|
|
},
|
|
_serialized_model_patch,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"layer_diffusion_attention_sharing",
|
|
"Apply canonical SD1 Layer Diffusion attention sharing.",
|
|
{
|
|
"model_patch": SafeTensorName(
|
|
doc="Logical SafeTensor name in model_patches."),
|
|
"frames": Int(
|
|
1, 3, default=1,
|
|
doc="Interleaved Layer Diffusion frame count."),
|
|
"control_image": RefOf(
|
|
"IMAGE", default=None,
|
|
doc="Optional bounded RGB control image."),
|
|
"first_conditioning": RefOf(
|
|
"CONDITIONING", default=None,
|
|
doc="Optional first per-frame conditioning."),
|
|
"second_conditioning": RefOf(
|
|
"CONDITIONING", default=None,
|
|
doc="Optional second per-frame conditioning."),
|
|
"third_conditioning": RefOf(
|
|
"CONDITIONING", default=None,
|
|
doc="Optional third per-frame conditioning."),
|
|
},
|
|
_layer_diffusion_attention_sharing,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"concat_latent_input",
|
|
"Inject one encoded latent as model c_concat input.",
|
|
{
|
|
"latent": RefOf(
|
|
"LATENT", doc="Encoded latent to inject."),
|
|
},
|
|
_concat_latent_input,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"sampling_memory_report",
|
|
"Log peak allocated and reserved accelerator memory after sampling.",
|
|
{},
|
|
_sampling_memory_report,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"riflex_rope",
|
|
"Install a RIFLEx temporal rotary-position embedder.",
|
|
{
|
|
"architecture": OneOf(
|
|
("wan", "hunyuan"),
|
|
doc="Core-owned rotary embedder layout."),
|
|
"num_frames": Int(
|
|
1, 100000, doc="Target latent frame count."),
|
|
"intrinsic_frequency": Int(
|
|
1, 100, doc="One-based temporal frequency index."),
|
|
},
|
|
_riflex_rope,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"wan_skip_layer_guidance",
|
|
"Skip selected Wan unconditional double blocks in a step range.",
|
|
{
|
|
"blocks": IntList(
|
|
4096, doc="Wan double-block indices to skip."),
|
|
"start_percent": Float(
|
|
0.0, 1.0, doc="First sampling fraction to apply."),
|
|
"end_percent": Float(
|
|
0.0, 1.0, doc="Last sampling fraction to apply."),
|
|
},
|
|
_wan_skip_layer_guidance,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"perturb_weights",
|
|
"Add deterministic Gaussian perturbations to diffusion weights.",
|
|
{
|
|
"joint_blocks": Float(
|
|
0.001, 10.0,
|
|
doc="Noise standard-deviation multiplier for joint blocks."),
|
|
"final_layer": Float(
|
|
0.001, 10.0,
|
|
doc="Noise standard-deviation multiplier for final layers."),
|
|
"rest_of_the_blocks": Float(
|
|
0.001, 10.0,
|
|
doc="Noise multiplier for all other diffusion weights."),
|
|
"seed": Int(
|
|
0, 0xffffffffffffffff,
|
|
doc="Execution-local random seed."),
|
|
},
|
|
_perturb_weights,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"hunyuan_concat_image",
|
|
"Configure Hunyuan Video conditioning to use concat_image.",
|
|
{},
|
|
_hunyuan_concat_image,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"flux_block_scales",
|
|
"Scale complete Flux double and single transformer blocks.",
|
|
{
|
|
"double_blocks": FloatList(
|
|
0.0, 5.0, 19,
|
|
doc="Scale table for Flux double blocks 0 through 18."),
|
|
"single_blocks": FloatList(
|
|
0.0, 5.0, 38,
|
|
doc="Scale table for Flux single blocks 0 through 37."),
|
|
},
|
|
_flux_block_scales,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"guidance_timestepping",
|
|
"Override CFG within a model-sigma percentage range.",
|
|
{
|
|
"value": Float(0.0, 100.0, doc="CFG value in the range."),
|
|
"start_at": Float(
|
|
0.0, 1.0, doc="First sampling percentage."),
|
|
"end_at": Float(
|
|
0.0, 1.0, doc="Last sampling percentage."),
|
|
},
|
|
_guidance_timestepping,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"sd3_advanced_sampling",
|
|
"Install Essentials' cut-off SD3 flow schedule.",
|
|
{
|
|
"shift": Float(0.0, 100.0, doc="Base flow shift."),
|
|
"cut_off": Float(
|
|
0.0, 1.0, doc="Normalized timestep cut-off."),
|
|
"shift_multiplier": Float(
|
|
0.0, 10.0, doc="Shift multiplier below the cut-off."),
|
|
},
|
|
_sd3_advanced_sampling,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"flux_sampler_sampling",
|
|
"Install the Flux or AuraFlow resolution-aware schedule.",
|
|
{
|
|
"max_shift": Float(0.0, 100.0, doc="Maximum Flux shift."),
|
|
"base_shift": Float(0.0, 100.0, doc="Base Flux shift."),
|
|
"width": Int(1, 16384, doc="Target image width."),
|
|
"height": Int(1, 16384, doc="Target image height."),
|
|
},
|
|
_flux_sampler_sampling,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"latent_inpaint_ttm",
|
|
"Apply Time-To-Move latent inpainting during early sample steps.",
|
|
{
|
|
"steps": Int(
|
|
0, 888, doc="Number of early sample steps to modify."),
|
|
"mask": RefOf(
|
|
"MASK", default=None,
|
|
doc="Optional motion-region mask."),
|
|
},
|
|
_latent_inpaint_ttm,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"leapfusion_hunyuan_i2v",
|
|
"Replace one Hunyuan latent frame during a sampling step range.",
|
|
{
|
|
"latent": RefOf(
|
|
"LATENT", doc="Source latent supplying the replacement frame."),
|
|
"index": Int(
|
|
-1, 1000, doc="Target latent frame index."),
|
|
"strength": Float(
|
|
-10.0, 10.0, doc="Replacement latent multiplier."),
|
|
"start_percent": Float(
|
|
0.0, 1.0, doc="First sampling fraction to replace."),
|
|
"end_percent": Float(
|
|
0.0, 1.0, doc="Last sampling fraction to replace."),
|
|
},
|
|
_leapfusion_hunyuan_i2v,
|
|
experimental=True,
|
|
),
|
|
Transform(
|
|
"spatial_tiled_evaluation",
|
|
"Evaluate one denoise prediction over bounded overlapping tiles.",
|
|
{
|
|
"rows": Int(1, 256, doc="Latent tile rows."),
|
|
"columns": Int(1, 256, doc="Latent tile columns."),
|
|
"overlap": Float(
|
|
0.0, 0.5, default=0.0,
|
|
doc="Fractional overlap added to each tile."),
|
|
"overlap_x": Int(
|
|
0, 8192, default=0,
|
|
doc="Additional horizontal overlap in latent cells."),
|
|
"overlap_y": Int(
|
|
0, 8192, default=0,
|
|
doc="Additional vertical overlap in latent cells."),
|
|
"blend": OneOf(
|
|
("linear",), default="linear",
|
|
doc="Canonical overlap blend policy."),
|
|
"preserve_existing": Bool(
|
|
default=True,
|
|
doc="Delegate through an existing model wrapper."),
|
|
},
|
|
_spatial_tiled_evaluation,
|
|
),
|
|
)
|
|
}
|
|
|
|
|
|
def describe_all() -> list[dict]:
|
|
"""The vocabulary, as data — for documentation and for the guest's error
|
|
messages, so both are generated from the table rather than restated."""
|
|
return [t.describe() for t in TRANSFORMS.values()]
|