Compare commits

...

4 Commits

Author SHA1 Message Date
Dustin
db916abee3
Merge 0388ac4309 into 439bd807f8 2026-07-06 18:02:05 -04:00
comfyanonymous
439bd807f8
Skip unloading dynamic model patchers in current workflow. (#14799) 2026-07-06 14:35:12 -07:00
Dustin
0388ac4309
Defensively truncate/pad nested noise components to match latent
When noise.is_nested with a different number of components than
latent_image, truncate extras or pad missing components with
torch.zeros_like, mirroring the denoise_mask handling pattern below.

Addresses CodeRabbit nitpick on #13318.
2026-05-03 23:52:08 -04:00
Dustin
2beca418ad
Fix noise/latent tensor mismatch when latent is nested but noise is not
When using LTXAV (audio+video) workflows, latent_image is a NestedTensor
but noise may be a regular tensor. Calling unbind() on non-nested noise
splits along dim=0 (channels), producing a shape mismatch at noise_scaling.

Check whether noise is nested before unbinding. If not, pad with zero-noise
for additional components (e.g. audio), which is semantically correct since
those components don't need denoising in the video sampler.
2026-04-07 06:07:26 -04:00
3 changed files with 38 additions and 3 deletions

View File

@ -1270,8 +1270,19 @@ class CFGGuider:
return latent_image
if latent_image.is_nested:
latent_image, latent_shapes = comfy.utils.pack_latents(latent_image.unbind())
noise, _ = comfy.utils.pack_latents(noise.unbind())
li_tensors = latent_image.unbind()
if noise.is_nested:
# Truncate extra noise components, pad missing ones with zeros
n_tensors = list(noise.unbind()[:len(li_tensors)])
for i in range(len(n_tensors), len(li_tensors)):
n_tensors.append(torch.zeros_like(li_tensors[i]))
else:
# Noise only covers video -- pad remaining components (audio) with zeros
n_tensors = [noise]
for i in range(1, len(li_tensors)):
n_tensors.append(torch.zeros_like(li_tensors[i]))
latent_image, latent_shapes = comfy.utils.pack_latents(li_tensors)
noise, _ = comfy.utils.pack_latents(n_tensors)
else:
latent_shapes = [latent_image.shape]

View File

@ -468,6 +468,9 @@ class CLIP:
def decode(self, token_ids, skip_special_tokens=True):
return self.tokenizer.decode(token_ids, skip_special_tokens=skip_special_tokens)
def is_dynamic(self):
return self.patcher.is_dynamic()
class VAE:
def __init__(self, sd=None, device=None, config=None, dtype=None, metadata=None):
if 'decoder.up_blocks.0.resnets.0.norm1.weight' in sd.keys(): #diffusers format
@ -1251,6 +1254,8 @@ class VAE:
except:
return None
def is_dynamic(self):
return self.patcher.is_dynamic()
class StyleModel:
def __init__(self, model, device="cpu"):

View File

@ -503,6 +503,21 @@ RAM_CACHE_DEFAULT_RAM_USAGE = 0.05
RAM_CACHE_OLD_WORKFLOW_OOM_MULTIPLIER = 1.3
def all_outputs_dynamic(outputs):
if outputs is None:
return False
for output in outputs:
if isinstance(output, (list, tuple)):
if not all_outputs_dynamic(output):
return False
elif not hasattr(output, "is_dynamic") or not output.is_dynamic():
return False
return True
class RAMPressureCache(LRUCache):
def __init__(self, key_class, enable_providers=False):
@ -533,7 +548,11 @@ class RAMPressureCache(LRUCache):
for key, cache_entry in self.cache.items():
if not free_active and self.used_generation[key] == self.generation:
continue
oom_score = RAM_CACHE_OLD_WORKFLOW_OOM_MULTIPLIER ** (self.generation - self.used_generation[key])
if all_outputs_dynamic(cache_entry.outputs) and self.used_generation[key] == self.generation:
continue
oom_score = RAM_CACHE_OLD_WORKFLOW_OOM_MULTIPLIER ** (self.generation - self.used_generation[key])
ram_usage = RAM_CACHE_DEFAULT_RAM_USAGE
def scan_list_for_ram_usage(outputs):