2023-12-13 21:52:11 +01:00
import torch
from torch import einsum
2023-12-13 16:10:03 -05:00
import torch . nn . functional as F
import math
2025-09-27 00:13:52 +03:00
from typing_extensions import override
2023-12-13 16:10:03 -05:00
2023-12-13 21:52:11 +01:00
from einops import rearrange , repeat
2024-05-14 18:02:27 -04:00
from comfy . ldm . modules . attention import optimized_attention
2023-12-13 16:10:03 -05:00
import comfy . samplers
2025-09-27 00:13:52 +03:00
from comfy_api . latest import ComfyExtension , io
2023-12-13 21:52:11 +01:00
# from comfy/ldm/modules/attention.py
# but modified to return attention scores as well as output
2024-05-14 18:02:27 -04:00
def attention_basic_with_sim ( q , k , v , heads , mask = None , attn_precision = None ) :
2023-12-13 21:52:11 +01:00
b , _ , dim_head = q . shape
dim_head / / = heads
scale = dim_head * * - 0.5
h = heads
q , k , v = map (
lambda t : t . unsqueeze ( 3 )
. reshape ( b , - 1 , heads , dim_head )
. permute ( 0 , 2 , 1 , 3 )
. reshape ( b * heads , - 1 , dim_head )
. contiguous ( ) ,
( q , k , v ) ,
)
# force cast to fp32 to avoid overflowing
2024-05-14 18:02:27 -04:00
if attn_precision == torch . float32 :
2023-12-15 01:28:16 -05:00
sim = einsum ( ' b i d, b j d -> b i j ' , q . float ( ) , k . float ( ) ) * scale
2023-12-13 21:52:11 +01:00
else :
sim = einsum ( ' b i d, b j d -> b i j ' , q , k ) * scale
del q , k
if mask is not None :
mask = rearrange ( mask , ' b ... -> b (...) ' )
max_neg_value = - torch . finfo ( sim . dtype ) . max
mask = repeat ( mask , ' b j -> (b h) () j ' , h = h )
sim . masked_fill_ ( ~ mask , max_neg_value )
# attention, what we cannot get enough of
sim = sim . softmax ( dim = - 1 )
out = einsum ( ' b i j, b j d -> b i d ' , sim . to ( v . dtype ) , v )
out = (
out . unsqueeze ( 0 )
. reshape ( b , heads , - 1 , dim_head )
. permute ( 0 , 2 , 1 , 3 )
. reshape ( b , - 1 , heads * dim_head )
)
return ( out , sim )
2023-12-13 16:10:03 -05:00
def create_blur_map ( x0 , attn , sigma = 3.0 , threshold = 1.0 ) :
# reshape and GAP the attention map
_ , hw1 , hw2 = attn . shape
b , _ , lh , lw = x0 . shape
attn = attn . reshape ( b , - 1 , hw1 , hw2 )
# Global Average Pool
mask = attn . mean ( 1 , keepdim = False ) . sum ( 1 , keepdim = False ) > threshold
2024-11-08 23:16:29 +00:00
2024-11-10 00:10:45 -05:00
total = mask . shape [ - 1 ]
x = round ( math . sqrt ( ( lh / lw ) * total ) )
xx = None
for i in range ( 0 , math . floor ( math . sqrt ( total ) / 2 ) ) :
for j in [ ( x + i ) , max ( 1 , x - i ) ] :
if total % j == 0 :
xx = j
break
if xx is not None :
break
x = xx
y = total / / x
2023-12-13 16:10:03 -05:00
# Reshape
mask = (
2024-11-10 00:10:45 -05:00
mask . reshape ( b , x , y )
2023-12-13 16:10:03 -05:00
. unsqueeze ( 1 )
. type ( attn . dtype )
)
# Upsample
mask = F . interpolate ( mask , ( lh , lw ) )
blurred = gaussian_blur_2d ( x0 , kernel_size = 9 , sigma = sigma )
blurred = blurred * mask + x0 * ( 1 - mask )
return blurred
def gaussian_blur_2d ( img , kernel_size , sigma ) :
ksize_half = ( kernel_size - 1 ) * 0.5
x = torch . linspace ( - ksize_half , ksize_half , steps = kernel_size )
pdf = torch . exp ( - 0.5 * ( x / sigma ) . pow ( 2 ) )
x_kernel = pdf / pdf . sum ( )
x_kernel = x_kernel . to ( device = img . device , dtype = img . dtype )
kernel2d = torch . mm ( x_kernel [ : , None ] , x_kernel [ None , : ] )
kernel2d = kernel2d . expand ( img . shape [ - 3 ] , 1 , kernel2d . shape [ 0 ] , kernel2d . shape [ 1 ] )
padding = [ kernel_size / / 2 , kernel_size / / 2 , kernel_size / / 2 , kernel_size / / 2 ]
img = F . pad ( img , padding , mode = " reflect " )
img = F . conv2d ( img , kernel2d , groups = img . shape [ - 3 ] )
return img
2025-09-27 00:13:52 +03:00
class SelfAttentionGuidance ( io . ComfyNode ) :
2023-12-13 21:52:11 +01:00
@classmethod
2025-09-27 00:13:52 +03:00
def define_schema ( cls ) :
return io . Schema (
node_id = " SelfAttentionGuidance " ,
display_name = " Self-Attention Guidance " ,
category = " _for_testing " ,
2026-02-16 14:02:17 -08:00
description = " Applies Self-Attention Guidance (SAG) which uses attention maps to create adversarially blurred images and computes a guidance signal that enhances fine details. " ,
short_description = " Self-Attention Guidance for enhanced detail. " ,
2025-09-27 00:13:52 +03:00
inputs = [
io . Model . Input ( " model " ) ,
io . Float . Input ( " scale " , default = 0.5 , min = - 2.0 , max = 5.0 , step = 0.01 ) ,
io . Float . Input ( " blur_sigma " , default = 2.0 , min = 0.0 , max = 10.0 , step = 0.1 ) ,
] ,
outputs = [
io . Model . Output ( ) ,
] ,
is_experimental = True ,
)
2023-12-13 21:52:11 +01:00
2025-09-27 00:13:52 +03:00
@classmethod
def execute ( cls , model , scale , blur_sigma ) :
2023-12-13 21:52:11 +01:00
m = model . clone ( )
2023-12-13 16:10:03 -05:00
2023-12-13 21:52:11 +01:00
attn_scores = None
# TODO: make this work properly with chunked batches
# currently, we can only save the attn from one UNet call
def attn_and_record ( q , k , v , extra_options ) :
nonlocal attn_scores
# if uncond, save the attention scores
heads = extra_options [ " n_heads " ]
cond_or_uncond = extra_options [ " cond_or_uncond " ]
b = q . shape [ 0 ] / / len ( cond_or_uncond )
if 1 in cond_or_uncond :
uncond_index = cond_or_uncond . index ( 1 )
# do the entire attention operation, but save the attention scores to attn_scores
2024-05-14 18:02:27 -04:00
( out , sim ) = attention_basic_with_sim ( q , k , v , heads = heads , attn_precision = extra_options [ " attn_precision " ] )
2023-12-13 21:52:11 +01:00
# when using a higher batch size, I BELIEVE the result batch dimension is [uc1, ... ucn, c1, ... cn]
n_slices = heads * b
attn_scores = sim [ n_slices * uncond_index : n_slices * ( uncond_index + 1 ) ]
return out
else :
2024-05-14 18:02:27 -04:00
return optimized_attention ( q , k , v , heads = heads , attn_precision = extra_options [ " attn_precision " ] )
2023-12-13 21:52:11 +01:00
2023-12-13 16:10:03 -05:00
def post_cfg_function ( args ) :
nonlocal attn_scores
uncond_attn = attn_scores
sag_scale = scale
sag_sigma = blur_sigma
sag_threshold = 1.0
model = args [ " model " ]
uncond_pred = args [ " uncond_denoised " ]
uncond = args [ " uncond " ]
cfg_result = args [ " denoised " ]
sigma = args [ " sigma " ]
model_options = args [ " model_options " ]
x = args [ " input " ]
2024-01-10 04:08:43 -05:00
if min ( cfg_result . shape [ 2 : ] ) < = 4 : #skip when too small to add padding
return cfg_result
2023-12-13 16:10:03 -05:00
# create the adversarially blurred image
degraded = create_blur_map ( uncond_pred , uncond_attn , sag_sigma , sag_threshold )
degraded_noised = degraded + x - uncond_pred
# call into the UNet
2024-04-01 17:23:07 -04:00
( sag , ) = comfy . samplers . calc_cond_batch ( model , [ uncond ] , degraded_noised , sigma , model_options )
2023-12-13 16:10:03 -05:00
return cfg_result + ( degraded - sag ) * sag_scale
2023-12-18 17:03:32 -05:00
m . set_model_sampler_post_cfg_function ( post_cfg_function , disable_cfg1_optimization = True )
2023-12-13 16:10:03 -05:00
2023-12-13 21:52:11 +01:00
# from diffusers:
# unet.mid_block.attentions[0].transformer_blocks[0].attn1.patch
2023-12-13 16:10:03 -05:00
m . set_model_attn1_replace ( attn_and_record , " middle " , 0 , 0 )
2025-09-27 00:13:52 +03:00
return io . NodeOutput ( m )
class SagExtension ( ComfyExtension ) :
@override
async def get_node_list ( self ) - > list [ type [ io . ComfyNode ] ] :
return [
SelfAttentionGuidance ,
]
2023-12-13 21:52:11 +01:00
2023-12-13 16:10:03 -05:00
2025-09-27 00:13:52 +03:00
async def comfy_entrypoint ( ) - > SagExtension :
return SagExtension ( )