Files
thatname-Comfyui_CharaConsist/node.py
T
2025-11-18 20:29:44 +08:00

1001 lines
46 KiB
Python

import gc
import torch
import torch.nn.functional as F
import numpy as np
from torch import Tensor
from comfy.ldm.modules import attention as comfy_attention
from comfy.ldm.flux import math as flux_math
from comfy.ldm.flux import layers as flux_layers
from comfy.ldm.chroma import layers as chroma_layers
from comfy import model_management
from typing import List, Dict, Optional
from functools import partial
from einops import rearrange
from nodes import CLIPTextEncode
import latent_preview
import copy
import comfy.ldm.qwen_image.model as qwen_image_model
#import comfy.ldm.wan.model as wan_model
orig_attention = comfy_attention.optimized_attention
orig_apply_rope1 = flux_math.apply_rope1
orig_prepare_callback = latent_preview.prepare_callback
def convert_masks_to_patches(masks: torch.Tensor, pad) -> tuple:
"""
Convert pixel masks to patch-level masks for attention.
Args:
masks: [N, H, W] tensor - subject regions in images
Returns:
patch_masks: [N, h*w] tensor where h=H_latent*8/16, w=W_latent*8/16
h, w: patch dimensions
"""
N = masks.shape[0]
H, W = masks.shape[1], masks.shape[2]
# Calculate patch dimensions (assuming 16x16 pixels per patch, but 1x1 in latent space)
h = H // 16
w = W // 16
patch_masks = torch.zeros(N + pad, h, w, device=masks.device, dtype=masks.dtype)
for i in range(N):
# Convert to patch level using nearest-neighbor interpolation
mask_resized = F.interpolate(
masks[i:i+1, None, :, :],
size=(h, w),
mode='nearest-exact'
)
# Flatten to patch sequence
patch_masks[i] = mask_resized[0][0]
for i in range(pad):
patch_masks[i + N] = torch.ones((h, w), device=masks.device, dtype=masks.dtype)
return patch_masks, h, w
def match_condition(reference_conditions: List, target_condition: List) -> torch.Tensor:
"""
Match reference conditions to target condition using dot product similarity.
Args:
reference_conditions: List of N conditioning tensors (one per reference subject)
target_condition: Conditioning tensor for target images (shared)
Returns:
conditions_mask: 2D tensor [num_references, max_embedding_seq_length]
with 1s where reference embeddings match target subsequences
"""
# Extract embeddings
ref_embeds = [cond[0][0] for cond in reference_conditions] # text embeddings
target_embed = target_condition[0][0] # [1, seq_len, embed_dim]
num_references = len(ref_embeds)
target_seq_len = target_embed.shape[1]
# Initialize mask tensor
conditions_mask = torch.zeros(num_references, target_seq_len, dtype=target_embed.dtype, device=target_embed.device)
# Process each reference embedding
for i, ref_embed in enumerate(ref_embeds):
ref_seq_len = ref_embed.shape[1]
# If reference is longer than target, skip (shouldn't happen per spec)
if ref_seq_len > target_seq_len:
continue
# Get reference embedding (remove batch dimension)
ref_vec = ref_embed[0] # [ref_seq_len, embed_dim]
# Use unfold to get all subsequences of target with length ref_seq_len
# target_embed[0] is [target_seq_len, embed_dim]
# unfolded will be [target_seq_len - ref_seq_len + 1, ref_seq_len, embed_dim]
unfolded = target_embed[0].unfold(0, ref_seq_len, 1).permute(0,2,1) # [num_subseq, ref_seq_len, embed_dim]
# Normalize for cosine similarity
ref_normalized = F.normalize(ref_vec, p=2, dim=-1) # [ref_seq_len, embed_dim]
target_normalized = F.normalize(unfolded, p=2, dim=-1) # [num_subseq, ref_seq_len, embed_dim]
# Compute cosine similarity for all subsequences at once
# Result: [num_subseq]
similarities = torch.mean(torch.sum(ref_normalized.unsqueeze(0) * target_normalized, dim=-1), dim=-1)
# Find the best matching subsequence
best_match_idx = torch.argmax(similarities)
# Mask the best matching subsequence
start_idx = best_match_idx.item()
conditions_mask[i, start_idx:start_idx + ref_seq_len] = 1.0
return conditions_mask
def prepare_batched_conditions(reference_conditions: List, target_condition: List, num_targets) -> tuple:
"""
Prepare the batched conditions for reference and target images.
Args:
reference_conditions: List of N conditioning tensors (one per reference subject)
target_condition: Conditioning tensor for target images (shared)
Returns:
extended_condition: Batched conditioning tensor [B, num_reference + 1, max_seq, 4096]
conditions_mask: Mask tensor [B, num_references + 1, max_seq]
pooled_output: CLIP pooled output (None for Chroma, tensor for Flux)
"""
# Extract text embeddings from conditions
ref_embeds = []
ref_pooled = []
seq_lens = []
for cond in reference_conditions:
if cond is None:
continue
ref_embeds.append(cond[0][0]) # text embedding
ref_pooled.append(cond[0][1].get('pooled_output', None))
# Target condition
target_embed = target_condition[0][0]
target_pooled = target_condition[0][1].get('pooled_output', None)
# Calculate max sequence length
all_embeds = [target_embed] + ref_embeds
max_seq_len = max(embed.shape[1] for embed in all_embeds)
# Get batch size and embedding dimension
embed_dim = target_embed.shape[2] # Should be 4096
# Create padded tensors with shape [B, num_reference + 1, max_seq, 4096]
num_references = len(ref_embeds)
padded_embeds = torch.zeros(num_references + num_targets, max_seq_len, embed_dim, dtype=target_embed.dtype, device=target_embed.device)
conditions_mask = torch.zeros(num_references + num_targets, max_seq_len, dtype=target_embed.dtype, device=target_embed.device)
# Fill in target embeds (first position)
target_seq_len = target_embed.shape[1]
# Fill in reference embeds
for i, ref_embed in enumerate(ref_embeds):
ref_seq_len = ref_embed.shape[1]
padded_embeds[i, :ref_seq_len, :] = ref_embed[0]
conditions_mask[i, :ref_seq_len] = 1.0
seq_lens.append(ref_seq_len)
for i in range(num_targets):
padded_embeds[i + num_references, :target_seq_len, :] = target_embed[0]
conditions_mask[i + num_references, :target_seq_len] = 1.0
seq_lens.append(target_seq_len)
# Handle pooled output (only used by Flux)
if target_pooled is not None:
# For Flux, use the target pooled output
pooled_output = target_pooled
else:
pooled_output = None
extended_condition = [[
padded_embeds,
{'pooled_output': pooled_output}
]]
return extended_condition, conditions_mask, max_seq_len, seq_lens
class ReferenceConditionCombine:
RETURN_TYPES = ("CONDITIONING", "MASK")
RETURN_NAMES = ("condition", "condition_mask")
FUNCTION = "go"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"num_targets": ("INT",),
},
"optional": {
"reference_condition1": ("CONDITIONING",),
"reference_condition2": ("CONDITIONING",),
"reference_condition3": ("CONDITIONING",),
"reference_condition4": ("CONDITIONING",),
"reference_condition5": ("CONDITIONING",),
"target_condition": ("CONDITIONING",),
}
}
@classmethod
def go(cls, *,
num_targets, reference_condition1=None, reference_condition2=None,
reference_condition3=None, reference_condition4=None, reference_condition5=None,
target_condition=None):
reference_conditions = [reference_condition1, reference_condition2, reference_condition3,
reference_condition4, reference_condition5]
extended_condition, conditions_mask, max_seq_len, seq_lens = prepare_batched_conditions(reference_conditions, target_condition, num_targets)
return extended_condition, conditions_mask.unsqueeze(0)
class BatchedMaskedReferenceGen:
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "go"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"source_patch_masks": ("MASK",),
"target_patch_masks": ("MASK",),
"conditions_mask": ("MASK",),
"conditions_ref_mask": ("MASK",),
},
}
@classmethod
def go(cls, *, model: object, source_patch_masks: torch.Tensor, target_patch_masks: torch.Tensor,
conditions_mask: torch.Tensor, conditions_ref_mask=torch.Tensor):
print(f'BatchedMaskedReferenceGen - Processing with model: {model}')
if conditions_mask.dim() == 3:
conditions_mask = conditions_mask[0]
if conditions_ref_mask.dim() == 3:
conditions_ref_mask = conditions_ref_mask[0]
# Detect model type
model_type = "flux"
if model.model.__class__.__name__ == "Chroma":
model_type = "chroma"
print(f'Detected model type: {model_type}')
num_references = conditions_ref_mask.shape[0]
num_targets = conditions_mask.shape[0] - num_references
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
attn_dtype = torch.bfloat16 if model_management.should_use_bf16(device=device) else torch.float16
w = source_patch_masks.shape[-1]
h = source_patch_masks.shape[1]
patch_count = h * w
source_patch_masks = source_patch_masks.flatten(start_dim=1)
target_patch_masks = target_patch_masks.flatten(start_dim=1)
reference_seq_mask = torch.log(torch.cat([conditions_mask[:num_references], torch.ones((num_references, patch_count), dtype=attn_dtype)], dim=1))
source_seq_masks = torch.log(torch.cat([conditions_mask[:num_references+1], source_patch_masks], dim=1))
target_seq_masks = torch.log(torch.cat([conditions_mask[num_references].unsqueeze(0).repeat(num_references+1,1), target_patch_masks], dim=1))
def masked_attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, reference_seq_mask, target_seq_masks, source_seq_masks) -> Tensor:
"""
Implementation using PyTorch's scaled_dot_product_attention with custom masking.
Args:
q: Query tensor of shape [B, H, L, D]
k: Key tensor of shape [B, H, L, D]
v: Value tensor of shape [B, H, L, D]
pe: Positional encoding tensor
attn_mask: Optional attention mask of shape [B, H, L, L]
Returns:
Output tensor of shape [B, L, H*D]
"""
target_count = q.shape[0] - num_references
# Apply rotary positional encoding for Flux models
if pe is not None:
q, k = flux_math.apply_rope(q, k, pe)
# Get actual sequence length from QKV tensors
actual_seq_len = q.shape[2]
#if attn_mask is None or attn_mask.shape[-2] != actual_seq_len:
# this_source = torch.cat([torch.zeros([num_references, text_len]), source_patch_masks], dim=-1)
# this_target = torch.cat([torch.zeros([num_references, text_len]), source_patch_masks], dim=-1)
# attn_mask = torch.stack(torch.min(this_source.unsqueeze(1), this_target.unsqueeze(2)), dim=-1).to(device)
reference_seq_mask_ = reference_seq_mask.to(device, dtype=attn_dtype)
reference_attn_mask = torch.min(reference_seq_mask_.unsqueeze(1), reference_seq_mask_.unsqueeze(2))
target_attn_mask = rearrange(torch.min(source_seq_masks.to(device, dtype=attn_dtype).unsqueeze(1), target_seq_masks.to(device, dtype=attn_dtype).unsqueeze(2)), "B H W -> 1 W (B H)")
h = torch.zeros(q.shape, device=device)
# PyTorch's scaled_dot_product_attention
h[:num_references] = F.scaled_dot_product_attention(
q[:num_references], k[:num_references], v[:num_references],
attn_mask=reference_attn_mask
)
B, H, L, D = k.shape
k = k.permute(1, 0, 2, 3).reshape(1, H, B * L, D)
B, H, L, D = v.shape
v = v.permute(1, 0, 2, 3).reshape(1, H, B * L, D)
for i in range(target_count):
target_idx = i + num_references
if i > 0:
k[:, :, num_references*actual_seq_len : num_references*actual_seq_len + actual_seq_len] = k[:, :, target_idx*actual_seq_len : target_idx*actual_seq_len + actual_seq_len]
v[:, :, num_references*actual_seq_len : num_references*actual_seq_len + actual_seq_len] = v[:, :, target_idx*actual_seq_len : target_idx*actual_seq_len + actual_seq_len]
h[target_idx:target_idx + 1] = F.scaled_dot_product_attention(
q[target_idx: target_idx + 1],
k[:, :, :num_references*actual_seq_len + actual_seq_len],
v[:, :, :num_references*actual_seq_len + actual_seq_len], attn_mask=target_attn_mask)
# Reshape back to the expected output format
h = rearrange(h, "B H L D -> B L (H D)")
return h
# Create the attention override function
def batched_masked_attention_override(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, mask, transformer_options, reference_seq_mask, target_seq_masks, source_seq_masks) -> Tensor:
if q.shape[0] != num_references + num_targets or reference_seq_mask is None or reference_seq_mask.shape[-1] != q.shape[2]:
return orig_attention(q, k, v, pe)
else:
return masked_attention(q, k, v, pe, reference_seq_mask, target_seq_masks, source_seq_masks)
# Apply the attention override based on model type
overrider = partial(batched_masked_attention_override, reference_seq_mask=reference_seq_mask, target_seq_masks=target_seq_masks, source_seq_masks=source_seq_masks)
if model_type == "flux":
flux_math.attention = overrider
else: # chroma
chroma_layers.attention = overrider
# Clean up
#del source_patch_masks, target_patch_masks
gc.collect()
torch.cuda.empty_cache()
print('BatchedMaskedReferenceGen processing completed')
return (model,)
class ConditioningMatchMask:
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = "match"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
},
"optional": {
"reference_condition1": ("CONDITIONING",),
"reference_condition2": ("CONDITIONING",),
"reference_condition3": ("CONDITIONING",),
"reference_condition4": ("CONDITIONING",),
"reference_condition5": ("CONDITIONING",),
"target_condition": ("CONDITIONING",),
}
}
def match(self, reference_condition1=None, reference_condition2=None, reference_condition3=None,
reference_condition4=None, reference_condition5=None, target_condition=None):
# Collect reference conditions
reference_conditions = []
ref_conditions = [reference_condition1, reference_condition2, reference_condition3,
reference_condition4, reference_condition5]
for cond in ref_conditions:
if cond is not None:
reference_conditions.append(cond)
if not reference_conditions:
raise ValueError("At least one reference condition must be provided")
if target_condition is None:
raise ValueError("Target condition must be provided")
# Call the match_condition function
conditions_mask = match_condition(reference_conditions, target_condition)
return (conditions_mask.unsqueeze(0),)
class MasksToPatches:
RETURN_TYPES = ("MASK", "INT", "INT")
RETURN_NAMES = ("patch_masks", "h", "w")
FUNCTION = "convert"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"masks": ("MASK",),
"target_count": ("INT",{"default":1,"min":0}),
}
}
def convert(self, masks, target_count):
# Call the convert_masks_to_patches function
patch_masks, h, w = convert_masks_to_patches(masks, target_count)
print(f'Patch dimensions: {h}x{w}, Total patches: {h*w}')
return (patch_masks, h, w)
class PreviewSubjectMask:
RETURN_TYPES = ("MASK","LATENT",)
RETURN_NAMES = ("mask","latent",)
FUNCTION = "go"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"latent": ("LATENT",),
"subject": ("ATTN",),
},
}
@classmethod
def go(cls, latent, subject):
# Iterate through subject[0] and gather all fg_masks
fg_masks = []
for s in subject:
# Now s is a dict where keys are step ids
for step_id, step_data in s.items():
if "fg_mask" in step_data:
fg_masks.append(step_data["fg_mask"])
# Stack all fg_masks into a single tensor
if fg_masks:
# Stack masks to create a tensor with shape [B, H, W]
combined_mask = torch.cat(fg_masks, dim=0)
else:
# If no fg_masks found, return empty tensor
combined_mask = torch.empty(0)
return (combined_mask, latent, )
class MaskToPatchMask:
RETURN_TYPES = ("MASK",)
FUNCTION = "set_mask"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"patch_size": ("INT",{"default": 16}),
"threshold": ("FLOAT",{"default": 1.0}),
}
}
@classmethod
def set_mask(cls, mask: torch.Tensor, patch_size, threshold):
# Get the dimensions of the input mask
B, H, W = mask.shape
# Calculate patch dimensions (divide by 16)
h_patches = H // patch_size
w_patches = W // patch_size
# Reshape mask to [B, H//16, 16, W//16, 16] for processing 16x16 blocks
mask_reshaped = mask.view(B, h_patches, patch_size, w_patches, patch_size)
# Check if any pixel in each 16x16 block is non-zero
# Take max over the 16x16 blocks, then convert to boolean
patch_mask = mask_reshaped.mean(dim=2).mean(dim=3) >= threshold
# Convert to boolean tensor
patch_mask = patch_mask.bool()
return (patch_mask,)
def hook_math(hooked_attention):
save_freq = None
def hook_apply_rope1(x, freqs_cis):
nonlocal save_freq
save_freq = freqs_cis
return x
def hook_optimized_attention(q, k, v, heads, mask=None, skip_reshape = False, skip_output_reshape=False, transformer_options={}, **kwargs) -> Tensor:
nonlocal save_freq
if "cond_or_uncond" in transformer_options and transformer_options["cond_or_uncond"][-1]: # negative prompt, ignore
q = orig_apply_rope1(q, save_freq)
k = orig_apply_rope1(k, save_freq)
return orig_attention(q, k, v, heads, mask=mask, skip_reshape=skip_reshape, skip_output_reshape=skip_output_reshape, transformer_options=transformer_options, **kwargs)
if not skip_reshape: # not B H L D but B L (H D), we must reshape to B H L D
B, L, HD = q.shape
D = HD // heads
q = q.view(B, L, heads, D).permute(0, 2, 1, 3)
B, L, HD = k.shape
D = HD // heads
k = k.view(B, L, heads, D).permute(0, 2, 1, 3)
B, L, HD = v.shape
D = HD // heads
v = v.view(B, L, heads, D).permute(0, 2, 1, 3)
output = hooked_attention(q, k, v, save_freq, mask, **kwargs) # no reshape for hooked attention
if skip_output_reshape: # because we reshaped output to B L (H D) before, we need to reshape back
output = rearrange(output, "B L (H D) -> B H L D", H=heads)
return output
flux_math.apply_rope1 = hook_apply_rope1
flux_math.optimized_attention = hook_optimized_attention
qwen_image_model.apply_rope1 = hook_apply_rope1
qwen_image_model.optimized_attention_masked = hook_optimized_attention
def unhook_math():
flux_math.apply_rope1 = orig_apply_rope1
flux_math.optimized_attention = orig_attention
qwen_image_model.apply_rope1 = orig_apply_rope1
qwen_image_model.optimized_attention_masked = orig_attention
def hook_prepare_callback():
unhook_math()
def prepare_callback(model, max_steps, x0_output_dict=None):
if "prepare_callback" in model.model_options:
return model.model_options["prepare_callback"](model, max_steps, x0_output_dict)
else:
return orig_prepare_callback(model, max_steps, x0_output_dict)
latent_preview.prepare_callback = prepare_callback
def get_shared_fg_mask(id_fg_mask, curr_fg_mask, curr2id_argmax_indices, curr2id_valid_mask):
curr_valid_mask = curr2id_valid_mask.flatten()
id_fg_mask = id_fg_mask.flatten().to(curr2id_argmax_indices.device)
curr_fg_mask = curr_fg_mask.flatten().to(curr2id_argmax_indices.device)
rearrange_id_fg_mask = id_fg_mask[curr2id_argmax_indices]
share_fg_mask = curr_fg_mask & rearrange_id_fg_mask & curr_valid_mask
id_share_fg_indices = curr2id_argmax_indices[share_fg_mask]
curr_share_fg_indices = torch.nonzero(share_fg_mask).squeeze()
return id_share_fg_indices, curr_share_fg_indices
def get_fg_attn_weights(text_len, image_start, q, k, fg_condition_mask):
attn_weights = torch.matmul(q[:, :, image_start:], k[:, :, :text_len].transpose(-2, -1)).to(torch.float16)
attn_weights = attn_weights.softmax(-1).sum(1, keepdim=True)
fg_condition_mask_gpu = fg_condition_mask
bg_condition_mask_gpu = 1.0 - fg_condition_mask_gpu
masks = torch.nn.functional.normalize(torch.stack([fg_condition_mask_gpu, bg_condition_mask_gpu], dim=1), p=1, dim=-1)
attn_weights = (attn_weights * masks).sum(-1)
return attn_weights
class ExtractAttn:
RETURN_TYPES = ("MODEL","ATTN",)
FUNCTION = "go"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"latent": ("LATENT",),
"kv_start_step": ("INT",{"default":1,"min":0}),
"kv_end_step": ("INT",{"default":9,"min":0}),
"x_start_step": ("INT",{"default":1,"min":0}),
"x_end_step": ("INT",{"default":9,"min":0}),
"mask_start_step": ("INT",{"default":8,"min":0}),
"mask_end_step": ("INT",{"default":9,"min":0}),
},
"optional":{
"fg_condition_mask": ("MASK",),
}
}
@classmethod
def go(cls, model, latent, kv_start_step, kv_end_step, x_start_step, x_end_step, mask_start_step, mask_end_step, fg_condition_mask = None):
attn_data = [{}]
index = len(attn_data) - 1
def prepare_callback(model, max_steps, x0_output_dict=None):
nonlocal attn_data, fg_condition_mask, latent
attn_data[index] = {}
sampled_step = -1
sampling_step = -1
layer = 0
w = latent["samples"].shape[-1] // model.model.diffusion_model.patch_size
h = latent["samples"].shape[-2] // model.model.diffusion_model.patch_size
#text_len = fg_condition_mask.shape[-1]
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
attn_dtype = torch.bfloat16 if model_management.should_use_bf16(device=device) else torch.float16
fg_bg_mask = None
if fg_condition_mask is not None:
fg_condition_mask = fg_condition_mask.to(device)
def masked_attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, mask: Tensor) -> Tensor:
"""
Implementation using PyTorch's scaled_dot_product_attention with custom masking.
Args:
q: Query tensor of shape [B, H, L, D]
k: Key tensor of shape [B, H, L, D]
v: Value tensor of shape [B, H, L, D]
pe: Positional encoding tensor
attn_mask: Optional attention mask of shape [B, H, L, L]
Returns:
Output tensor of shape [B, L, H*D]
"""
nonlocal sampling_step, sampled_step, layer, attn_data, w, h, fg_bg_mask
text_len = q.shape[2] - w * h
if sampling_step != sampled_step + 1:
layer = 0
sampling_step = sampled_step + 1
# Initialize step in attn_data dict if not exists
if sampling_step not in attn_data[index]:
attn_data[index][sampling_step] = {"layers": {}}
# Store k, v, and x in a dict with layer id as key
if sampling_step >= kv_start_step and sampling_step < kv_end_step:
attn_data[index][sampling_step]["layers"][layer] = {"k": k.cpu(), "v": v.cpu()}
# Apply rotary positional encoding for Flux models
if pe is not None:
q = orig_apply_rope1(q, pe)
k = orig_apply_rope1(k, pe)
if fg_condition_mask is not None and sampling_step >= mask_start_step and sampling_step < mask_end_step:
if fg_bg_mask is None:
fg_bg_mask = get_fg_attn_weights(text_len, text_len, q, k, fg_condition_mask)
else:
fg_bg_mask += get_fg_attn_weights(text_len, text_len, q, k, fg_condition_mask)
x = F.scaled_dot_product_attention(
q, k, v, attn_mask=mask
)
# Reshape back to the expected output format
x = rearrange(x, "B H L D -> B L (H D)")
# Store attention result x along with k and v
if sampling_step >= x_start_step and sampling_step < x_end_step:
if not layer in attn_data[index][sampling_step]["layers"]:
attn_data[index][sampling_step]["layers"][layer] = {}
attn_data[index][sampling_step]["layers"][layer]["x"] = x[:, text_len:,:].cpu()
layer += 1
return x
hook_math(masked_attention)
origin_callback = orig_prepare_callback(model, max_steps, x0_output_dict)
def callback_change_sampled_step(step, x0, x, total_steps):
nonlocal max_steps
nonlocal sampling_step, sampled_step, layer, fg_bg_mask, model
sampled_step = step
if fg_condition_mask is not None and sampling_step >= mask_start_step and sampling_step < mask_end_step:
B,L,H,W = x0.shape
w = W // model.model.diffusion_model.patch_size
h = H // model.model.diffusion_model.patch_size
bitmask = fg_bg_mask[:,0] >= fg_bg_mask[:,1]
bitmask = rearrange(bitmask, "b (h w) -> b h w", h=h, w=w)
fg_bg_mask = None
# Store fg_mask in a dictionary with step id as key
if sampling_step not in attn_data[index]:
attn_data[index][sampling_step] = {"layers": {}}
attn_data[index][sampling_step]["fg_mask"] = bitmask.cpu()
origin_callback(step, x0, x, total_steps)
#if step == total_steps - 1:
# latent_preview.prepare_callback = orig_prepare_callback
return callback_change_sampled_step
model = copy.copy(model)
model.model_options = copy.copy(model.model_options)
model.model_options["prepare_callback"] = prepare_callback
hook_prepare_callback()
return (model, attn_data,)
class GetCrossSim:
RETURN_TYPES = ("CROSSSIM",)
RETURN_NAMES = ("cross_sim",)
FUNCTION = "compute_cross_sim"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"subject_attn": ("ATTN",),
"curr_attn": ("ATTN",),
"id_mask": ("MASK",),
"curr_mask": ("MASK",),
"cross_sim_step": ("INT",),
"sim_threshold": ("FLOAT", {"default":0.5,"min":0,"max":2}),
}
}
@classmethod
def compute_cross_sim(cls, subject_attn, curr_attn, id_mask, curr_mask, cross_sim_step, sim_threshold):
cross_sim = {}
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
attn_dtype = torch.bfloat16 if model_management.should_use_bf16(device=device) else torch.float16
# Validate inputs
if not subject_attn or len(subject_attn) == 0:
raise ValueError("subject_attn cannot be empty")
if not curr_attn or len(curr_attn) == 0:
raise ValueError("curr_attn cannot be empty")
# Use the first (and only) current image attention data
curr_attn_data = curr_attn[0]
# Check if the specified step exists in current image attention data
if cross_sim_step not in curr_attn_data:
raise ValueError(f"cross_sim_step {cross_sim_step} not found in curr_attn")
# Get current attention layers for the specified step
curr_layers = curr_attn_data[cross_sim_step].get("layers", {})
if not curr_layers:
raise ValueError(f"No layers found for step {cross_sim_step} in curr_attn")
# Initialize accumulators
all_cross_sim = []
all_max_sim = []
all_argmax_indices = []
all_id_fg_inds = []
all_curr_fg_inds = []
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
attn_dtype = torch.bfloat16 if model_management.should_use_bf16(device=device) else torch.float16
# Iterate through each ID subject
for id_idx, id_attn_data in enumerate(subject_attn):
# Check if the specified step exists in this ID subject
if cross_sim_step not in id_attn_data:
continue # Skip this subject if step doesn't exist
# Get ID subject layers for the specified step
id_layers = id_attn_data[cross_sim_step].get("layers", {})
if not id_layers:
continue # Skip this subject if no layers
id_fg_mask = id_mask[id_idx].to(device).flatten()
curr_fg_mask = curr_mask[id_idx].to(device).flatten() # todo multiple images in 1 batch
# Accumulate cross similarity across all layers
layer_sim = None
for layer_idx in curr_layers:
if layer_idx not in id_layers:
continue # Skip layer if not present in both subjects
# Get attention vectors for this layer
curr_x = F.normalize(curr_layers[layer_idx]["x"].to(device)) # [B, D, L] format
id_x = F.normalize(id_layers[layer_idx]["x"].to(device)) # [B, D, L] format
# Compute cross similarity: current @ ID^T
# curr_x is [B, D, L], id_x is [B, D, L]
# We want: curr_x^T @ id_x -> [L, L]
similarity = torch.matmul(curr_x, id_x.transpose(-2, -1)) # [B, L, L]
if layer_sim is None:
layer_sim = similarity
else:
layer_sim += similarity
if layer_sim is not None:
# Average across layers
avg_sim = layer_sim / len(curr_layers)
max_sim, argmax_indices = torch.max(avg_sim, dim=-1)
id_fg_inds, curr_fg_inds = get_shared_fg_mask( # todo multiple batch gen.
id_fg_mask,
curr_fg_mask,
argmax_indices[0],
max_sim[0]>sim_threshold
)
all_id_fg_inds.append(id_fg_inds.cpu())
all_curr_fg_inds.append(curr_fg_inds.cpu())
all_max_sim.append(max_sim)
all_argmax_indices.append(argmax_indices)
all_cross_sim.append(avg_sim)
cross_sim["id_fg_inds"] = all_id_fg_inds
cross_sim["curr_fg_inds"] = all_curr_fg_inds
cross_sim["max_sim"] = torch.cat(all_max_sim).cpu()
cross_sim["argmax_indices"] = torch.cat(all_argmax_indices).cpu()
cross_sim["debug_mask"] = torch.cat(all_cross_sim).cpu()
cross_sim["id_mask"] = id_mask
cross_sim["curr_mask"] = curr_mask
return (cross_sim,)
class CrossSimMaskExtract:
RETURN_TYPES = ("MASK", "MASK")
RETURN_NAMES = ("cross_mask", "fg_mask")
FUNCTION = "extract"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"cross_sim": ("CROSSSIM",),
"latent": ("LATENT",),
}
}
def extract(self, cross_sim, latent):
# Extract fg_mask directly from cross_sim
fg_mask = cross_sim.get("fg_mask", None)
# Create cross_mask from max_sim or debug_mask
# Using debug_mask as it appears to contain the cross attention similarity data
cross_mask = cross_sim.get("debug_mask", None)
# If debug_mask is not available, try to create from max_sim
#if cross_mask is None and "max_sim" in cross_sim:
# # max_sim is likely a tensor with similarity values
# max_sim = cross_sim["max_sim"]
# # Normalize to 0-1 range for mask
# if max_sim is not None:
# cross_mask = (max_sim - max_sim.min()) / (max_sim.max() - max_sim.min() + 1e-8)
# Handle case where neither is available
#if fg_mask is None:
# # Create an empty mask if fg_mask is not available
# fg_mask = torch.zeros(1, 1, 1, dtype=torch.float32)
if cross_mask is None:
# Create an empty mask if cross_mask is not available
cross_mask = torch.zeros(1, 1, 1, dtype=torch.float32)
return (cross_mask, fg_mask)
class GenConsistent:
RETURN_TYPES = ("MODEL",)
FUNCTION = "go"
CATEGORY = "chara_consist"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"subject": ("ATTN",),
"cross_sim": ("CROSSSIM",),
"kv_start_step": ("INT",{"default":1,"min":0}),
"kv_end_step": ("INT",{"default":5,"min":0}),
"x_start_step": ("INT",{"default":1,"min":0}),
"x_end_step": ("INT",{"default":5,"min":0}),
"ada_tome_alpha": ("FLOAT",{"default":0.0,"min":0,"max":1,"step":0.1}),
"latent":("LATENT",),
},
}
@classmethod
def go(cls, model, subject, cross_sim, kv_start_step, kv_end_step, x_start_step, x_end_step, ada_tome_alpha, latent):
def prepare_callback(model, max_steps, x0_output_dict=None):
nonlocal subject, latent
sampled_step = -1
sampling_step = -1
layer = 0
w = latent["samples"].shape[-1] // model.model.diffusion_model.patch_size
h = latent["samples"].shape[-2] // model.model.diffusion_model.patch_size
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
attn_dtype = torch.bfloat16 if model_management.should_use_bf16(device=device) else torch.float16
fg_bg_mask = None
def masked_attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, _: Tensor) -> Tensor:
"""
Implementation using PyTorch's scaled_dot_product_attention with custom masking.
Args:
q: Query tensor of shape [B, H, L, D]
k: Key tensor of shape [B, H, L, D]
v: Value tensor of shape [B, H, L, D]
pe: Positional encoding tensor
attn_mask: Optional attention mask of shape [B, H, L, L]
Returns:
Output tensor of shape [B, L, H*D]
"""
nonlocal sampling_step, sampled_step, layer, subject, w, h, fg_bg_mask
if sampling_step != sampled_step + 1:
layer = 0
sampling_step = sampled_step + 1
text_len = q.shape[2] - w*h
original_seq_len = q.shape[2]
# Apply rotary positional encoding for Flux models
if pe is not None:
q = orig_apply_rope1(q, pe)
k = orig_apply_rope1(k, pe)
# Store original sequence length before K,V concatenation
original_k_len = k.shape[2]
# Store k, v, and x in a dict with layer id as key
if sampling_step >= kv_start_step and sampling_step < kv_end_step:
# For all other steps, we may apply the key-value expansion algorithm
# Get cross-sim data for this layer
id_fg_inds = cross_sim.get("id_fg_inds", None)
curr_fg_inds = cross_sim.get("curr_fg_inds", None)
curr_mask = cross_sim.get("curr_mask", None)
# Process all subjects, not just the last one (index = len(subject) - 1)
for subj_idx in range(len(subject)):
# Check if we have valid cross-sim data and subject data to work with
if (id_fg_inds[subj_idx].shape[0] > 0 and
curr_fg_inds[subj_idx].shape[0] > 0 and
sampling_step in subject[subj_idx] and
layer in subject[subj_idx][sampling_step]["layers"] and
"k" in subject[subj_idx][sampling_step]["layers"][layer] and
"v" in subject[subj_idx][sampling_step]["layers"][layer]):
ori_saved_key = subject[subj_idx][sampling_step]["layers"][layer]["k"].to(k.device)
ori_saved_value = subject[subj_idx][sampling_step]["layers"][layer]["v"].to(v.device)
# Ensure we have valid indices within tensor bounds
# Handle batch dimension properly - cross_sim tensors have [B, ...] form where B = len(subject)
subj_id_fg_inds = id_fg_inds[subj_idx]
subj_curr_fg_inds = curr_fg_inds[subj_idx]
valid_id_inds = subj_id_fg_inds[subj_id_fg_inds < ori_saved_key.shape[2]]
valid_curr_inds = subj_curr_fg_inds[subj_curr_fg_inds < k.shape[2]]
if len(valid_id_inds) > 0 and len(valid_curr_inds) > 0:
# Apply key-value expansion based on cross-sim data
# Extract the saved key-value pairs using valid indices
saved_key_fg = ori_saved_key[:, :, valid_id_inds]
saved_value_fg = ori_saved_value[:, :, valid_id_inds]
# Apply rotary positional encoding to saved keys (fix missing apply_rope)
if pe is not None:
# Extract positional encoding for the saved key positions
# This mimics the approach in attention_processor_characonsist.py
image_rotary_emb_fg = pe[:,:,valid_curr_inds]
saved_key_fg = orig_apply_rope1(saved_key_fg, image_rotary_emb_fg)
# Concatenate the expanded key-value pairs
k = torch.cat([k, saved_key_fg], dim=2)
v = torch.cat([v, saved_value_fg], dim=2)
x = F.scaled_dot_product_attention(
q, k, v, attn_mask=None
)
# Reshape back to the expected output format
x = rearrange(x, "B H L D -> B L (H D)")
# Apply ada_tome blending if within specified step range
if sampling_step >= x_start_step and sampling_step < x_end_step and ada_tome_alpha > 0:
# Get cross-similarity data
id_fg_inds = cross_sim.get("id_fg_inds", None)#.to(device)
curr_fg_inds = cross_sim.get("curr_fg_inds", None)#.to(device)
max_sim = cross_sim.get("max_sim", None).to(device)
if (id_fg_inds is not None and curr_fg_inds is not None and max_sim is not None):
# Process all subjects for blending
for subj_idx in range(len(subject)):
# Check if we have valid subject data for this step and layer
if (sampling_step in subject[subj_idx] and
layer in subject[subj_idx][sampling_step]["layers"] and
"x" in subject[subj_idx][sampling_step]["layers"][layer]):
# Get saved hidden states from subject
saved_x = subject[subj_idx][sampling_step]["layers"][layer]["x"].to(x.device, dtype=attn_dtype)
# Ensure we have valid indices within tensor bounds
if subj_idx < len(id_fg_inds) and subj_idx < len(curr_fg_inds) and subj_idx < max_sim.shape[0]:
subj_id_fg_inds = id_fg_inds[subj_idx]
subj_curr_fg_inds = curr_fg_inds[subj_idx]
subj_max_sim = max_sim[subj_idx]
# Validate indices
valid_id_inds = subj_id_fg_inds[subj_id_fg_inds < saved_x.shape[-2]]
valid_curr_inds = subj_curr_fg_inds[subj_curr_fg_inds < x.shape[-2] - text_len]
if len(valid_id_inds) > 0 and len(valid_curr_inds) > 0:
# Extract vision tokens (after text_len) for blending
vision_hidden_states = x[:, text_len:, :]
# Get matched hidden states from saved subject and current generation
matched_saved_x = saved_x[:, valid_id_inds, :] # [B, D, valid_id_inds]
matched_current_x = vision_hidden_states[:, valid_curr_inds, :] # [B, valid_curr_inds, D]
# Apply ada_tome blending with similarity-weighted alpha
alpha_tensor = torch.ones(len(valid_curr_inds), dtype=attn_dtype, device=x.device)
alpha_tensor = alpha_tensor * ada_tome_alpha
# Weight by similarity scores
sim_weight = subj_max_sim.flatten()[valid_curr_inds] if len(valid_curr_inds) <= subj_max_sim.numel() else torch.ones(len(valid_curr_inds), dtype=attn_dtype, device=x.device)
alpha_tensor = alpha_tensor * sim_weight
alpha_tensor = alpha_tensor.view(1, -1, 1)
# Blend: (1 - alpha) * current + alpha * saved
blended_states = (1 - alpha_tensor) * matched_current_x + alpha_tensor * matched_saved_x
# Update current vision hidden states with blended values
vision_hidden_states[:, valid_curr_inds, :] = blended_states
# Reconstruct full hidden states with text + blended vision
x = torch.cat([x[:, :text_len, :], vision_hidden_states], dim=-2)
layer += 1
return x
hook_math(masked_attention)
origin_callback = orig_prepare_callback(model, max_steps, x0_output_dict)
def callback_change_sampled_step(step, x0, x, total_steps):
nonlocal max_steps
nonlocal sampling_step, sampled_step, layer, fg_bg_mask, model
sampled_step = step
origin_callback(step, x0, x, total_steps)
return callback_change_sampled_step
model = copy.copy(model)
model.model_options = copy.copy(model.model_options)
model.model_options["prepare_callback"] = prepare_callback
hook_prepare_callback()
return (model, subject,)