Files

184 lines
7.2 KiB
Python

import torch
import comfy.samplers
import comfy.model_management
import comfy.utils
from comfy.model_patcher import ModelPatcher
import random
class SDXLAblatingDataset:
def __init__(self, images, clip, placeholder_token, vae, concept_type="object", device="cpu", batch_size=2):
self.templates = [
"a photo of a {}",
"a rendering of a {}",
"the photo of a {}",
"a photo of the {}",
] if concept_type == "object" else [
"a painting in the style of {}",
"artwork in the style of {}",
"a rendering in the style of {}",
]
self.vae = vae
self.clip = clip
self.tokenizer = clip.tokenizer
self.placeholder_token = placeholder_token
self.device = device
# SDXL uses 0.13025 scaling factor instead of 0.18215
with torch.no_grad():
self.latents = vae.encode(images[:,:,:,:3]).movedim(-1,1) * 0.13025
def __len__(self):
return len(self.latents)
def get_batch(self, batch_size):
idx = torch.randint(0, len(self.latents), (batch_size,))
latents = self.latents[idx]
prompts = [random.choice(self.templates).format(self.placeholder_token) for _ in range(batch_size)]
tokens_list = []
for prompt in prompts:
tokenized = self.tokenizer.tokenize_with_weights(prompt, return_word_ids=True)
# Based on your earlier output, tokenized["g"] is structured like:
# [[<tensor_or_list>, { ... }]]
# Extract the first element of the inner list:
extracted = tokenized["g"][0][0]
# If extracted is a tensor, convert its dtype to torch.long
if isinstance(extracted, torch.Tensor):
tokens = extracted.to(torch.long)
else:
# Otherwise, convert extracted to a tensor of type long
tokens = torch.tensor(extracted, dtype=torch.long)
tokens_list.append(tokens)
tokens_batch = torch.stack(tokens_list, dim=0)
return tokens_batch, latents
class ConceptEraserNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"clip": ("CLIP",),
"vae": ("VAE",),
"images": ("IMAGE",),
"concept": ("STRING", {"default": "fox"}),
"concept_type": (["object", "style"], {"default": "object"}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "normal"}),
"lr": ("FLOAT", {"default": 1e-5, "min": 1e-6, "max": 1e-3}),
"steps": ("INT", {"default": 100, "min": 10, "max": 500}),
"batch_size": ("INT", {"default": 2, "min": 1, "max": 8}),
}
}
RETURN_TYPES = ("MODEL", "CLIP")
FUNCTION = "erase"
CATEGORY = "model/advanced"
def freeze_text_encoder(self, clip):
# For SDXL, freeze both text encoders except the final normalization/layer layers
for enc in [clip.cond_stage_model.clip_l, clip.cond_stage_model.clip_g]:
for param in enc.parameters():
param.requires_grad = False
for name, module in enc.named_modules():
if "final_layer_norm" in name or "layer_23" in name:
for param in module.parameters():
param.requires_grad = True
return clip
def erase(self, model, clip, vae, images, concept, concept_type, scheduler, lr, steps, batch_size):
device = comfy.model_management.get_torch_device()
dtype = torch.float32 if device.type == 'mps' else comfy.model_management.text_encoder_dtype()
print(f"Using device: {device}, dtype: {dtype}")
print(f"Starting concept erasure for: {concept}")
# Prepare dataset with SDXL-compatible processing
dataset = SDXLAblatingDataset(
images=images,
clip=clip, # Pass the full CLIP object
placeholder_token=concept,
vae=vae,
concept_type=concept_type,
device=device,
batch_size=batch_size
)
# Clone and prepare models
clip_clone = self.freeze_text_encoder(clip.clone())
model_clone = model.clone()
optimizer = torch.optim.AdamW(
list(filter(lambda p: p.requires_grad, clip_clone.cond_stage_model.parameters())),
lr=lr,
weight_decay=1e-4
)
# Get model sampling parameters
model_sampling = model.get_model_object("model_sampling")
sigmas = comfy.samplers.calculate_sigmas(
model_sampling,
scheduler,
1000 # Total timesteps
)
# Training loop
pbar = comfy.utils.ProgressBar(steps)
for step in range(steps):
try:
# Get batch data
tokens, clean_latents = dataset.get_batch(batch_size)
noise = torch.randn_like(clean_latents).to(device)
clean_latents = clean_latents.to(device)
# Sample random timesteps
bsz = clean_latents.shape[0]
timesteps = torch.randint(1, 1000, (bsz,), device=device)
# Add noise to latents
batch_sigmas = sigmas[timesteps.cpu()].view(bsz, 1, 1, 1).to(device)
noisy_latents = clean_latents + noise * batch_sigmas
# Encode text embeddings in batch
tokens_batch = tokens
text_conds = clip_clone.encode_from_tokens(tokens_batch).to(device)
# Forward pass through UNet
with torch.cuda.amp.autocast(dtype=dtype):
timestep_embed = model_sampling.timestep(timesteps)
pred = model_clone.model.apply_model(
noisy_latents,
timesteps,
{"c_crossattn": [text_conds], "timestep_embed": timestep_embed}
)
# Negative MSE loss for concept erasure
loss = -torch.nn.functional.mse_loss(pred, noise)
# Optimization step
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(list(filter(lambda p: p.requires_grad, clip_clone.cond_stage_model.parameters())), 1.0)
optimizer.step()
pbar.update_absolute(step + 1)
print(f"Step {step+1}/{steps}, Loss: {loss.item():.6f}")
except Exception as e:
print(f"Error during step {step+1}: {str(e)}")
import traceback
traceback.print_exc()
continue
print(f"Concept erasure complete for: {concept}")
return (ModelPatcher(model_clone.model, load_device=device, offload_device=comfy.model_management.unet_offload_device()),
clip_clone)
# Register the nodes with ComfyUI
NODE_CLASS_MAPPINGS = {
"ConceptEraserNode": ConceptEraserNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ConceptEraserNode": "Concept Eraser"
}