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 orig_attention = flux_math.attention 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": ("SUBJECT",), }, } @classmethod def go(cls, latent, subject): # Iterate through subject[0] and gather all fg_masks fg_masks = [] for s in subject: for item in s: if "fg_mask" in item: fg_masks.append(item["fg_mask"]) # Stack all fg_masks into a single tensor if fg_masks: # Stack the 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, ) def hook_prepare_callback(): flux_layers.attention = orig_attention chroma_layers.attention = orig_attention 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_interpolate_weight(weight, start_step, decay_step, end_step): steps = np.arange(0, end_step - decay_step) decay_weights = weight * 0.5 * (1 + np.cos(np.pi * steps / (end_step - decay_step))) decay_weights = decay_weights.tolist() constant_weights = [weight] * (decay_step - start_step) weight_list = constant_weights + decay_weights weight_dict = dict() for ind, interpolate_step in enumerate(range(start_step, end_step)): weight_dict[interpolate_step] = weight_list[ind] return weight_dict 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[0]] share_fg_mask = curr_fg_mask & rearrange_id_fg_mask & curr_valid_mask id_share_fg_indices = curr2id_argmax_indices[0][share_fg_mask] curr_share_fg_indices = torch.nonzero(share_fg_mask).squeeze() return id_share_fg_indices, curr_share_fg_indices class GenSubject: RETURN_TYPES = ("MODEL","LATENT","SUBJECT",) FUNCTION = "go" CATEGORY = "chara_consist" @classmethod def INPUT_TYPES(cls): return { "required": { "model": ("MODEL",), "latent": ("LATENT",), "fg_condition_mask": ("MASK",), "kv_start_step": ("INT",{"default":1,"min":0}), "kv_end_step": ("INT",{"default":5,"min":0}), "mask_start_step": ("INT",{"default":5,"min":0}), "mask_end_step": ("INT",{"default":6,"min":0}), } } @classmethod def go(cls, model, latent: torch.Tensor, fg_condition_mask, kv_start_step, kv_end_step, mask_start_step, mask_end_step): B,L,H,W = latent["samples"].shape subject = [] def prepare_callback(model, max_steps, x0_output_dict=None): nonlocal subject, W, H subject.append([]) sampled_step = -1 sampling_step = -1 layer = 0 model_type = "flux" w = W // model.model.diffusion_model.patch_size h = H // 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 def get_fg_attn_weights(text_len, image_start, q, k): nonlocal w, h, 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.to(device) 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) attn_weights = rearrange(attn_weights, "b m (h w) -> b m h w", h=h, w=w) return attn_weights 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, subject, text_len, fg_bg_mask if sampling_step != sampled_step + 1: layer = 0 sampling_step = sampled_step + 1 if len(subject[-1]) == 0 or subject[-1][-1]["step"] != sampling_step: subject[-1].append({"step":sampling_step, "layers": []}) if sampling_step >= kv_start_step and sampling_step < kv_end_step: subject[-1][-1]["layers"].append({"layer": layer,"k": k.cpu(), "v": v.cpu()}) # 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 full_len = q.shape[2] text_len = full_len - h * w # because the latent size is equal to patch count for flux and chroma. if 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) else: fg_bg_mask += get_fg_attn_weights(text_len, text_len, q, k) 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)") layer += 1 return x # Create the attention override function def attention_override(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, mask: Tensor, transformer_options) -> Tensor: if transformer_options["cond_or_uncond"][-1]:#q.shape[2] != text_len + (w * h) return orig_attention(q, k, v, pe) else: return masked_attention(q, k, v, pe, mask) # Apply the attention override based on model type overrider = attention_override if model.model.__class__.__name__ == "Chroma": model_type = "chroma" print(f'GenSubject Detected model type: {model_type}') if model_type == "flux": flux_layers.attention = overrider else: # chroma chroma_layers.attention = overrider 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 sampled_step = step if sampling_step >= mask_start_step and sampling_step < mask_end_step: bitmask = fg_bg_mask[:,0] >= fg_bg_mask[:,1] fg_bg_mask = None subject[-1][-1]["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.model_options["prepare_callback"] = prepare_callback hook_prepare_callback() return (model, latent,subject,)