Files

166 lines
4.8 KiB
Python

import torch
from typing import Any, Dict, List, Tuple
def _as_list(x):
return x if isinstance(x, list) else [x]
def _pairwise(pos_list, neg_list):
if len(neg_list) == 0:
raise ValueError("negative conditioning is empty")
if len(neg_list) == 1 and len(pos_list) > 1:
neg_list = neg_list * len(pos_list)
return pos_list, neg_list
def _match_batch(x: torch.Tensor, target_b: int) -> torch.Tensor:
b = x.shape[0]
if b == target_b:
return x
if b == 1:
return x.expand(target_b, *x.shape[1:])
if target_b % b == 0:
rep = target_b // b
return x.repeat(rep, *([1] * (x.ndim - 1)))
raise ValueError(f"Batch mismatch: {b} -> {target_b}")
def _align_tokens(neg: torch.Tensor, target_t: int) -> torch.Tensor:
# neg: (B, Tn, D) -> (B, target_t, D)
b, tn, d = neg.shape
if tn == target_t:
return neg
if tn > target_t:
return neg[:, :target_t, :]
pad = neg.new_zeros((b, target_t - tn, d))
return torch.cat((neg, pad), dim=1)
def _fold_tensor(
pos: torch.Tensor,
neg: torch.Tensor,
scale: float,
rescale: float,
eps: float,
) -> torch.Tensor:
# pos/neg: (B,T,D) or pooled: (B,D)
neg = neg.to(device=pos.device, dtype=pos.dtype)
if pos.ndim == 3:
neg = _match_batch(neg, pos.shape[0])
neg = _align_tokens(neg, pos.shape[1])
pf = pos.float()
nf = neg.float()
# fold: x = pos + s*(pos - neg)
xf = pf + float(scale) * (pf - nf)
if rescale > 0.0:
std_p = pf.std(dim=(1, 2), keepdim=True)
std_x = xf.std(dim=(1, 2), keepdim=True)
xf = xf * (std_p / (std_x + eps))
n_p = pf.norm(dim=2, keepdim=True)
n_x = xf.norm(dim=2, keepdim=True)
ratio = n_p / (n_x + eps)
ratio = ratio.clamp(0.25, 4.0)
xf = xf * ratio
xf = pf + float(rescale) * (xf - pf)
return xf.to(dtype=pos.dtype)
elif pos.ndim == 2:
neg = _match_batch(neg, pos.shape[0])
pf = pos.float()
nf = neg.float()
xf = pf + float(scale) * (pf - nf)
if rescale > 0.0:
std_p = pf.std(dim=1, keepdim=True)
std_x = xf.std(dim=1, keepdim=True)
xf = xf * (std_p / (std_x + eps))
n_p = pf.norm(dim=1, keepdim=True)
n_x = xf.norm(dim=1, keepdim=True)
ratio = (n_p / (n_x + eps)).clamp(0.25, 4.0)
xf = xf * ratio
xf = pf + float(rescale) * (xf - pf)
return xf.to(dtype=pos.dtype)
else:
raise ValueError(f"Unsupported tensor rank: {pos.ndim}")
def fold_conditioning(
positive: List,
negative: List,
scale: float,
rescale: float = 0.7,
eps: float = 1e-6,
fold_pooled: bool = True,
) -> List:
pos_list = _as_list(positive)
neg_list = _as_list(negative)
pos_list, neg_list = _pairwise(pos_list, neg_list)
out = []
for (pos_cond, pos_meta), (neg_cond, neg_meta) in zip(pos_list, neg_list):
new_cond = _fold_tensor(pos_cond, neg_cond, scale=scale, rescale=rescale, eps=eps)
new_meta: Dict[str, Any] = dict(pos_meta) if isinstance(pos_meta, dict) else {}
if fold_pooled and isinstance(pos_meta, dict) and isinstance(neg_meta, dict):
if "pooled_output" in pos_meta and "pooled_output" in neg_meta:
ppo = pos_meta["pooled_output"]
npo = neg_meta["pooled_output"]
if isinstance(ppo, torch.Tensor) and isinstance(npo, torch.Tensor):
new_meta["pooled_output"] = _fold_tensor(ppo, npo, scale=scale, rescale=rescale, eps=eps)
out.append([new_cond, new_meta])
return out
class FoldNegativeIntoPositiveConditioning:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"scale": ("FLOAT", {"default": 5.0, "min": -20.0, "max": 20.0, "step": 0.1}),
"rescale": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.05}),
},
"optional": {
"fold_pooled": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("CONDITIONING",)
FUNCTION = "apply"
CATEGORY = "conditioning"
def apply(self, positive, negative, scale, rescale, fold_pooled=True):
folded = fold_conditioning(
positive=positive,
negative=negative,
scale=float(scale),
rescale=float(rescale),
fold_pooled=bool(fold_pooled),
)
return (folded,)
NODE_CLASS_MAPPINGS = {
"FoldNegativeIntoPositiveConditioning": FoldNegativeIntoPositiveConditioning,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FoldNegativeIntoPositiveConditioning": "Fold Negative Into Positive (Conditioning)",
}