1010import comfy .ops
1111import comfy .quant_ops
1212import comfy .rmsnorm
13- from comfy .ldm .modules .attention import optimized_attention
13+ from comfy .ldm .modules .attention import COMFY_KITCHEN_INT8_ATTENTION_IS_AVAILABLE , optimized_attention
1414
1515ops = comfy .ops .disable_weight_init
1616
3838
3939# 3D causal CNN encoder
4040
41+ def _kitchen_ndhwc (x ):
42+ # NDHWC when the kitchen pad kernel can feed it and the conv consumes it natively:
43+ # cuDNN does, MIOpen converts back and loses more than the fused pad saves
44+ ck = getattr (comfy .quant_ops , "ck" , None )
45+ return (ck is not None and hasattr (ck , "group_norm_silu_pad3d" ) and torch .version .hip is None
46+ and x .is_cuda and x .dtype in (torch .float16 , torch .bfloat16 ))
47+
48+
49+ def _fused_norm_pad (x , norm , spatial_pad , front ):
50+ # per-frame GroupNorm + SiLU + padding in one kitchen pass, NDHWC out
51+ if not _kitchen_ndhwc (x ) or x .shape [1 ] % 8 :
52+ return None
53+ ck = comfy .quant_ops .ck
54+ if norm is None :
55+ return ck .group_norm_silu_pad3d (x , None , None , 1 , 0.0 , (* spatial_pad , front ), silu = False )
56+ # cast_bias_weight: works for offloaded (dynamic VRAM) layers
57+ weight , bias , offload_stream = comfy .ops .cast_bias_weight (norm , x , offloadable = True )
58+ try :
59+ return ck .group_norm_silu_pad3d (x , weight , bias , norm .num_groups , norm .eps , (* spatial_pad , front ), silu = True )
60+ finally :
61+ comfy .ops .uncast_bias_weight (norm , weight , bias , offload_stream )
62+
63+
64+ def _fp16_accum_conv (conv , x , weight , bias , residual ):
65+ # kitchen fp16-accumulate conv, same opt-in as the fp16 GEMMs
66+ ck = comfy .quant_ops .ck
67+ if not hasattr (ck , "fp16_conv3d" ) or not comfy .ops ._fp16_linear_wanted (x ):
68+ return None
69+ return ck .fp16_conv3d (x , weight , bias , residual , conv .stride )
70+
71+
4172class CausalConv3d (ops .Conv3d ):
4273 # Reflect spatial padding, causal (zeros, front-only) temporal padding.
4374 def __init__ (self , in_channels , out_channels , kernel_size , stride = 1 , padding = 0 ):
4475 super ().__init__ (in_channels , out_channels , kernel_size = kernel_size , stride = stride )
4576 self .causal_padding = (padding ,) * 3 if isinstance (padding , int ) else tuple (padding )
4677
47- def forward (self , x ):
48- if sum (self .causal_padding ) == 0 :
49- return super ().forward (x )
50-
51- x = F .pad (x , (self .causal_padding [2 ], self .causal_padding [2 ], self .causal_padding [1 ], self .causal_padding [1 ], 0 , 0 ), mode = "reflect" )
52- if x .shape [2 ] == 1 :
78+ def forward (self , x , pre_norm = None , spatial_pad = None , residual = None ):
79+ # pre_norm, spatial_pad and residual fold into the kitchen pad pass and conv epilogue
80+ pad_t , pad_h , pad_w = self .causal_padding
81+ if spatial_pad is None :
82+ spatial_pad = (pad_w , pad_w , pad_h , pad_h )
83+ front = 0 if x .shape [2 ] == 1 else pad_t * 2
84+
85+ ndhwc = _kitchen_ndhwc (x )
86+ if ndhwc and self .weight .is_cuda and not self .weight .is_contiguous (memory_format = torch .channels_last_3d ):
87+ self .weight .data = self .weight .data .contiguous (memory_format = torch .channels_last_3d )
88+
89+ if pre_norm is not None or front or any (spatial_pad ): # the 1x1 shortcut has nothing to fold
90+ fused = _fused_norm_pad (x , pre_norm , spatial_pad , front )
91+ if fused is not None :
92+ x = fused
93+ else :
94+ if pre_norm is not None :
95+ x = F .silu (pre_norm (x ), inplace = True )
96+ if any (spatial_pad ):
97+ x = F .pad (x , (* spatial_pad , 0 , 0 ), mode = "reflect" )
98+ if front :
99+ x = F .pad (x , (0 , 0 , 0 , 0 , front , 0 ), mode = "constant" )
100+
101+ if x .shape [2 ] == 1 and pad_t :
53102 # single frame: the causal front padding is all zeros truncate the temporal taps instead of convolving zero frames
54- return super ().forward (x , autopad = "causal_zero" )
55- x = F .pad (x , (0 , 0 , 0 , 0 , self .causal_padding [0 ] * 2 , 0 ), mode = "constant" )
56- return super ().forward (x )
103+ out = super ().forward (x , autopad = "causal_zero" )
104+ elif ndhwc :
105+ # cast_bias_weight handles offloaded layers; channels_last keeps cuDNN's output NDHWC
106+ weight , bias , offload_stream = comfy .ops .cast_bias_weight (self , x , offloadable = True )
107+ try :
108+ weight_cl = weight .contiguous (memory_format = torch .channels_last_3d )
109+ out = _fp16_accum_conv (self , x , weight_cl , bias , residual )
110+ if out is not None :
111+ return out
112+ out = F .conv3d (x , weight_cl , bias , self .stride )
113+ finally :
114+ comfy .ops .uncast_bias_weight (self , weight , bias , offload_stream )
115+ else :
116+ out = super ().forward (x )
117+ if residual is not None :
118+ out += residual
119+ return out
57120
58121
59122class TemporalIsolatedGroupNorm (ops .GroupNorm ):
@@ -85,7 +148,7 @@ def __init__(self, in_channels, out_channels, time_stride=1, space_stride=2):
85148
86149 def forward (self , x ):
87150 if self .space_stride == 2 :
88- x = F . pad (x , (0 , 1 , 0 , 1 , 0 , 0 ), mode = "reflect" )
151+ return self . conv (x , spatial_pad = (0 , 1 , 0 , 1 ) )
89152 return self .conv (x )
90153
91154
@@ -104,11 +167,10 @@ def __init__(self, in_channels, out_channels=None):
104167 self .nin_shortcut = CausalConv3d (in_channels , out_channels , kernel_size = 1 )
105168
106169 def forward (self , x ):
107- h = self .conv1 (F .silu (self .norm1 (x ), inplace = True ))
108- h = self .conv2 (F .silu (self .norm2 (h ), inplace = True ))
170+ h = self .conv1 (x , pre_norm = self .norm1 )
109171 if self .in_channels != self .out_channels :
110172 x = self .nin_shortcut (x )
111- return h . add_ ( x )
173+ return self . conv2 ( h , pre_norm = self . norm2 , residual = x )
112174
113175
114176class EncoderFCN3D (nn .Module ):
@@ -160,8 +222,7 @@ def forward(self, x):
160222 h = self .down [i_level ].block [i_block ](h )
161223 if hasattr (self .down [i_level ], "downsample" ):
162224 h = self .down [i_level ].downsample (h )
163- h = F .silu (self .norm_out (h ))
164- return self .conv_out (h )
225+ return self .conv_out (h , pre_norm = self .norm_out )
165226
166227
167228# ViT3D decoder
@@ -206,9 +267,11 @@ def __init__(self, dim, mult=4, bias=True, operations=ops):
206267 self .w1 = operations .Linear (dim , inner_dim * 2 , bias = bias )
207268 self .w2 = operations .Linear (inner_dim , dim , bias = bias )
208269
209- def forward (self , x ):
210- gate , x = self .w1 (x ).chunk (2 , dim = - 1 )
211- return self .w2 (F .silu (gate ).mul_ (x ))
270+ def forward (self , x , pre_norm , residual , residual_scale ):
271+ # norm, gated silu and residual addcmul fold into the INT8 kernels
272+ h = comfy .ops .linear_input_act (self .w1 , x , "rms_norm" , pre_norm .weight , pre_norm .eps )
273+ return comfy .ops .linear_input_act (
274+ self .w2 , h , "swiglu" , residual = residual , residual_scale = residual_scale )
212275
213276
214277class Attention (nn .Module ):
@@ -219,27 +282,39 @@ def __init__(self, heads, dim_head, bias=True, eps=1e-5, operations=ops):
219282 inner_dim = dim_head * heads
220283 self .norm_q = ops .RMSNorm (dim_head , eps = eps , elementwise_affine = False )
221284 self .norm_k = ops .RMSNorm (dim_head , eps = eps , elementwise_affine = False )
285+ # unit scale for the fused rms+rope kernel; the qk norms are not affine
286+ self .register_buffer ("qk_norm_scale" , torch .ones (dim_head ), persistent = False )
222287 self .to_qkv = operations .Linear (inner_dim , inner_dim * 3 , bias = bias )
223288 self .to_out = operations .Linear (inner_dim , inner_dim , bias = bias )
224289
225- def forward (self , x , rotary_pos_emb = None ):
290+ def forward (self , x , rotary_pos_emb , pre_norm , residual , residual_scale ):
226291 batch_size , seq_len , _ = x .shape
227292
228- qkv = self .to_qkv ( x )
293+ qkv = comfy . ops . linear_input_act ( self .to_qkv , x , "rms_norm" , pre_norm . weight , pre_norm . eps )
229294 qkv = qkv .view (batch_size , seq_len , - 1 , 3 * self .dim_head )
230295 query , key , value = torch .chunk (qkv , 3 , dim = - 1 )
231296
232- query = comfy .rmsnorm .rms_norm (query , self .norm_q .weight , self .norm_q .eps )
233- key = comfy .rmsnorm .rms_norm (key , self .norm_k .weight , self .norm_k .eps )
234-
235297 if rotary_pos_emb is not None :
236- rot = rotary_pos_emb .shape [- 3 ] * 2
237- query [..., :rot ], key [..., :rot ] = comfy .quant_ops .ck .apply_rope_split_half (
238- query [..., :rot ], key [..., :rot ], rotary_pos_emb )
239-
240- out = optimized_attention (query .transpose (1 , 2 ), key .transpose (1 , 2 ), value .transpose (1 , 2 ),
241- self .heads , skip_reshape = True ).nan_to_num_ (0.0 )
242- return self .to_out (out )
298+ # fused per-head RMSNorm + partial split-half rope, in place when autograd is off
299+ rms_rope = (comfy .quant_ops .ck .rms_rope_split_half if torch .is_grad_enabled ()
300+ else comfy .quant_ops .ck .rms_rope_split_half_ )
301+ query , key = rms_rope (
302+ query , key , rotary_pos_emb , self .qk_norm_scale ,
303+ epsilon = self .norm_q .eps , rot_dim = rotary_pos_emb .shape [- 3 ] * 2 )
304+ else :
305+ query = comfy .rmsnorm .rms_norm (query , self .norm_q .weight , self .norm_q .eps )
306+ key = comfy .rmsnorm .rms_norm (key , self .norm_k .weight , self .norm_k .eps )
307+
308+ query , key , value = (t .transpose (1 , 2 ) for t in (query , key , value ))
309+ # an int8 decoder already accepts int8 numerics; fp16 keeps exact flash
310+ if isinstance (self .to_qkv .weight , comfy .ops .QuantizedTensor ) and COMFY_KITCHEN_INT8_ATTENTION_IS_AVAILABLE :
311+ out = comfy .quant_ops .ck .int8_attention (query , key , value )
312+ out = out .transpose (1 , 2 ).reshape (batch_size , seq_len , - 1 )
313+ else :
314+ out = optimized_attention (query , key , value , self .heads , skip_reshape = True )
315+ return comfy .ops .linear_input_act (
316+ self .to_out , torch .nan_to_num (out ), None ,
317+ residual = residual , residual_scale = residual_scale )
243318
244319
245320class TransformerBlock (nn .Module ):
@@ -253,9 +328,17 @@ def __init__(self, heads, dim_head, bias=True, eps=1e-5, operations=ops):
253328 self .ff = FeedForward (dim = dim , bias = bias , operations = operations )
254329 self .scale2 = nn .Parameter (torch .empty (dim ))
255330
331+ def _residual_scale (self , scale , x ):
332+ # cast_to_input always copies, defeating the kernels' identity-keyed caches
333+ if scale .dtype == x .dtype and scale .device == x .device :
334+ return scale
335+ return comfy .ops .cast_to_input (scale , x )
336+
256337 def forward (self , x , rotary_pos_emb = None ):
257- x = x .addcmul_ (self .attn (comfy .rmsnorm .rms_norm (x , self .norm1 .weight , self .norm1 .eps ), rotary_pos_emb ), comfy .ops .cast_to_input (self .scale1 , x ))
258- return x .addcmul_ (self .ff (comfy .rmsnorm .rms_norm (x , self .norm2 .weight , self .norm2 .eps )), comfy .ops .cast_to_input (self .scale2 , x ))
338+ x = self .attn (x , rotary_pos_emb , pre_norm = self .norm1 ,
339+ residual = x , residual_scale = self ._residual_scale (self .scale1 , x ))
340+ return self .ff (x , pre_norm = self .norm2 ,
341+ residual = x , residual_scale = self ._residual_scale (self .scale2 , x ))
259342
260343
261344class ViT3DDecoder (nn .Module ):
@@ -502,6 +585,17 @@ def tiled_encode(self, x):
502585 result_rows .append (torch .cat (result_row , dim = - 1 ))
503586 return torch .cat (result_rows , dim = - 2 )
504587
588+ def _decode_tile_row (self , z_row , x_idx , x_len ):
589+ # a few tiles per decoder call (~7%); each extra tile holds ~100 MB of activations
590+ free = comfy .model_management .get_free_memory (z_row .device )
591+ batch = int (max (1 , min (4 , free // (128 * 2 ** 20 * z_row .shape [0 ]))))
592+ slices = [z_row [..., j_pos // self .vae_ratio :(j_pos + j_len ) // self .vae_ratio ] for j_pos , j_len in zip (x_idx , x_len )]
593+ tiles = []
594+ for k in range (0 , len (slices ), batch ):
595+ group = slices [k :k + batch ]
596+ tiles .extend (self ._decode_pixels (torch .cat (group )).chunk (len (group )))
597+ return tiles
598+
505599 def tiled_decode (self , z ):
506600 height , width = z .shape [- 2 ] * self .vae_ratio , z .shape [- 1 ] * self .vae_ratio
507601 y_idx , y_len , y_overlap = self .split_tiles (height )
@@ -513,12 +607,11 @@ def tiled_decode(self, z):
513607 out_y = 0
514608 for i , (i_pos , i_len ) in enumerate (zip (y_idx , y_len )):
515609 zi , zl = i_pos // self .vae_ratio , i_len // self .vae_ratio
610+ tiles = self ._decode_tile_row (z [..., zi :zi + zl , :], x_idx , x_len )
516611 new_tails = []
517612 left_tail = None
518613 out_x = 0
519- for j , (j_pos , j_len ) in enumerate (zip (x_idx , x_len )):
520- zj , zw = j_pos // self .vae_ratio , j_len // self .vae_ratio
521- tile = self ._decode_pixels (z [..., zi :zi + zl , zj :zj + zw ])
614+ for j , tile in enumerate (tiles ):
522615 if i < len (y_idx ) - 1 :
523616 new_tails .append (tile [..., - y_overlap [i ]:, :].clone ())
524617 next_left_tail = tile [..., :, - x_overlap [j ]:].clone () if j < len (x_idx ) - 1 else None
0 commit comments