Skip to content

Commit 912ab4a

Browse files
authored
Merge pull request numz#298 from AInVFX/main
v2.5.10: Fix determinism, BlockSwap caching, and model path resolution
2 parents 9f78b30 + 65dd29a commit 912ab4a

File tree

8 files changed

+142
-52
lines changed

8 files changed

+142
-52
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ We're actively working on improvements and new features. To stay informed:
3636

3737
## 🚀 Updates
3838

39+
**2025.11.13 - Version 2.5.10**
40+
41+
- **🎯 Fix: Deterministic generation** - Identical images with the same seed now produce identical results across different sessions and batch positions
42+
- **🔧 Fix: Model caching with BlockSwap** - Resolved issue where cached DiT models wouldn't properly reload when VAE caching state changed
43+
- **💾 Fix: Runner caching optimization** - Runner templates now correctly cache whenever both DiT and VAE are cached, regardless of caching order
44+
- **📁 Fix: Case-insensitive model paths** - Extra model paths in YAML config now work regardless of case (seedvr2, SEEDVR2, SeedVR2, etc.)
45+
- **🐛 Fix: High resolution tile debug crash** - Fixed "NoneType has no attribute log" error when using maximum resolution with VAE tiling
46+
- **📊 Fix: Temporal overlap logging** - Corrected frame count reporting when temporal overlap is automatically adjusted
47+
- **🔍 Feature: Enhanced model path debugging** - Added detailed logging to help troubleshoot model loading issues (visible in debug mode)
48+
3949
**2025.11.12 - Version 2.5.9**
4050

4151
- **🐛 Fix: Tile debug visualization crash** - Fixed OpenCV error when using VAE tile debug mode on certain systems.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "seedvr2_videoupscaler"
33
description = "SeedVR2 official ComfyUI integration: ByteDance-Seed's one-step diffusion-based video/image upscaling with memory-efficient inference"
4-
version = "2.5.9"
4+
version = "2.5.10"
55
authors = [
66
{name = "numz"},
77
{name = "adrientoupet"}

src/core/generation_phases.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,10 @@ def encode_all_batches(
268268
if step <= 0:
269269
step = batch_size
270270
temporal_overlap = 0
271+
debug.log(f"temporal_overlap >= batch_size, resetting to 0", level="WARNING", category="setup", force=True)
272+
273+
# Store actual temporal overlap used (may differ from parameter if reset)
274+
ctx['actual_temporal_overlap'] = temporal_overlap
271275

272276
# Calculate number of batches
273277
num_encode_batches = 0
@@ -306,6 +310,14 @@ def encode_all_batches(
306310
runner.vae, ctx['cache_context']['vae_model'], debug
307311
)
308312
ctx['cache_context']['vae_newly_cached'] = True
313+
314+
# If both models now cached, cache runner template
315+
dit_is_cached = ctx['cache_context']['cached_dit'] or ctx['cache_context']['dit_newly_cached']
316+
if dit_is_cached:
317+
ctx['cache_context']['global_cache'].set_runner(
318+
ctx['cache_context']['dit_id'], ctx['cache_context']['vae_id'],
319+
runner, debug
320+
)
309321

310322
# Set deterministic seed for VAE encoding (separate from diffusion noise)
311323
# Uses seed + 1,000,000 to avoid collision with upscaling batch seeds
@@ -620,6 +632,7 @@ def upscale_all_batches(
620632
runner.dit, ctx['cache_context']['dit_model'], debug
621633
)
622634
ctx['cache_context']['dit_newly_cached'] = True
635+
623636
# If both models now cached, cache runner template
624637
vae_is_cached = ctx['cache_context']['cached_vae'] or ctx['cache_context']['vae_newly_cached']
625638
if vae_is_cached:
@@ -628,11 +641,6 @@ def upscale_all_batches(
628641
runner, debug
629642
)
630643

631-
# Set base seed for DiT noise generation
632-
# Ensures deterministic noise across all batches in this upscaling phase
633-
set_seed(seed)
634-
debug.log(f"Using seed: {seed}", category="dit")
635-
636644
# Move DiT to GPU for upscaling (no-op if already there)
637645
manage_model_device(model=runner.dit, target_device=ctx['dit_device'],
638646
model_name="DiT", debug=debug, runner=runner)
@@ -646,6 +654,11 @@ def upscale_all_batches(
646654
check_interrupt(ctx)
647655

648656
debug.log(f"Upscaling batch {upscale_idx+1}/{num_valid_latents}", category="generation", force=True)
657+
# Reset seed for each batch to ensure identical RNG state
658+
# This ensures identical inputs produce identical outputs regardless of batch position
659+
set_seed(seed)
660+
debug.log(f"Using seed: {seed} for deterministic generation", category="dit")
661+
649662
debug.start_timer(f"upscale_batch_{upscale_idx+1}")
650663

651664
# Move to DiT device with correct dtype for upscaling (no-op if already there)
@@ -1332,16 +1345,19 @@ def postprocess_all_batches(
13321345
if total_padding_removed > 0:
13331346
adjustments.append(f"{total_padding_removed} padding")
13341347

1348+
# Use actual temporal overlap from encoding (may have been reset)
1349+
actual_overlap = ctx.get('actual_temporal_overlap', temporal_overlap)
1350+
13351351
# Calculate and include temporal overlap blending info
1336-
if temporal_overlap > 0:
1337-
frames_blended = (num_valid_samples - 1) * temporal_overlap
1352+
if actual_overlap > 0:
1353+
frames_blended = (num_valid_samples - 1) * actual_overlap
13381354
adjustments.append(f"{frames_blended} overlap")
13391355

13401356
if adjustments:
13411357
# Add back all removed/blended frames to get true computed count
13421358
total_computed = frames_before_removal + total_padding_removed
1343-
if temporal_overlap > 0:
1344-
total_computed += (num_valid_samples - 1) * temporal_overlap
1359+
if actual_overlap > 0:
1360+
total_computed += (num_valid_samples - 1) * actual_overlap
13451361
frame_info += f" ({total_computed} computed with {' + '.join(adjustments)} removed)"
13461362

13471363
debug.log(f"Final output assembled: {frame_info}, Resolution: {Wf}x{Hf}px, Channels: {channels_str}",

src/models/video_vae_v3/modules/attn_video_vae.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,7 +1308,8 @@ def tiled_encode(self, x: torch.Tensor, tile_size: Tuple[int, int] = (512, 512),
13081308
if H <= tile_h and W <= tile_w:
13091309
return self.slicing_encode(x)
13101310
else:
1311-
self.debug.log(f"Using VAE tiled encoding (Tile: {tile_size}, Overlap: {tile_overlap})", category="vae", force=True, indent_level=1)
1311+
if self.debug:
1312+
self.debug.log(f"Using VAE tiled encoding (Tile: {tile_size}, Overlap: {tile_overlap})", category="vae", force=True, indent_level=1)
13121313

13131314
# Spatial scale factor (output/latent)
13141315
scale_factor = self.spatial_downsample_factor
@@ -1481,7 +1482,8 @@ def tiled_decode(self, z: torch.Tensor, tile_size: Tuple[int, int] = (512, 512),
14811482
if H <= latent_tile_h and W <= latent_tile_w:
14821483
return self.slicing_decode(z)
14831484
else:
1484-
self.debug.log(f"Using VAE tiled decoding (Tile: {tile_size}, Overlap: {tile_overlap})", category="vae", force=True, indent_level=1)
1485+
if self.debug:
1486+
self.debug.log(f"Using VAE tiled decoding (Tile: {tile_size}, Overlap: {tile_overlap})", category="vae", force=True, indent_level=1)
14851487

14861488
latent_overlap_h = max(0, min((overlap_h // scale_factor), latent_tile_h - 1))
14871489
latent_overlap_w = max(0, min((overlap_w // scale_factor), latent_tile_w - 1))

src/optimization/blockswap.py

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def apply_block_swap_to_dit(
193193
runner._blockswap_active = True
194194

195195
# Store configuration for debugging and cleanup
196-
runner._block_swap_config = {
196+
model._block_swap_config = {
197197
"blocks_swapped": blocks_to_swap,
198198
"swap_io_components": swap_io_components,
199199
"total_blocks": total_blocks,
@@ -650,11 +650,11 @@ def _protect_model_from_move(
650650
651651
Wraps model.to() method to prevent other code from accidentally moving
652652
the entire model to GPU, which would defeat BlockSwap's memory savings.
653-
Allows movement only when explicitly bypassed via runner flag.
653+
Allows movement only when explicitly bypassed via model flag.
654654
655655
Args:
656656
model: DiT model to protect
657-
runner: VideoDiffusionInfer instance (stored as weak reference)
657+
runner: VideoDiffusionInfer instance (for active status check)
658658
debug: Debug instance for logging (required)
659659
"""
660660
if not hasattr(model, '_original_to'):
@@ -665,34 +665,46 @@ def _protect_model_from_move(
665665
# Define the protected method without closures
666666
def protected_model_to(self, device, *args, **kwargs):
667667
# Check if protection is temporarily bypassed for offloading
668+
# Flag is stored on model itself (not runner) to survive runner recreation
669+
if getattr(self, "_blockswap_bypass_protection", False):
670+
# Protection bypassed, allow movement
671+
if hasattr(self, '_original_to'):
672+
return self._original_to(device, *args, **kwargs)
673+
674+
# Get configured offload device directly from model
675+
blockswap_offload_device = "cpu" # default
676+
if hasattr(self, "_block_swap_config"):
677+
blockswap_offload_device = self._block_swap_config.get("offload_device", "cpu")
678+
679+
# Check if BlockSwap is currently active via runner weak reference
668680
runner_ref = getattr(self, '_blockswap_runner_ref', None)
681+
blockswap_is_active = False
669682
if runner_ref:
670683
runner_obj = runner_ref()
671-
if runner_obj and getattr(runner_obj, "_blockswap_bypass_protection", False):
672-
# Protection bypassed, allow movement
673-
if hasattr(self, '_original_to'):
674-
return self._original_to(device, *args, **kwargs)
684+
if runner_obj and hasattr(runner_obj, "_blockswap_active"):
685+
blockswap_is_active = runner_obj._blockswap_active
675686

676-
# Check blockswap status using weak reference
677-
# Get configured offload device from runner
678-
blockswap_offload_device = "cpu" # default
679-
if runner_ref:
680-
runner_obj = runner_ref()
681-
if runner_obj and hasattr(runner_obj, "_block_swap_config"):
682-
blockswap_offload_device = runner_obj._block_swap_config.get("offload_device", "cpu")
687+
# Block attempts to move model away from configured offload device when active
688+
if blockswap_is_active and str(device) != str(blockswap_offload_device):
689+
# Get debug instance from runner if available
690+
debug_instance = None
691+
if runner_ref:
692+
runner_obj = runner_ref()
693+
if runner_obj and hasattr(runner_obj, 'debug'):
694+
debug_instance = runner_obj.debug
683695

684-
# Block attempts to move model away from configured offload device
685-
if str(device) != str(blockswap_offload_device):
686-
if runner_obj and hasattr(runner_obj, "_blockswap_active") and runner_obj._blockswap_active:
687-
debug.log(f"Blocked attempt to move blockswapped model from {blockswap_offload_device} to {device}",
688-
level="WARNING", category="blockswap", force=True)
689-
return self
696+
if debug_instance:
697+
debug_instance.log(
698+
f"Blocked attempt to move BlockSwap model from {blockswap_offload_device} to {device}",
699+
level="WARNING", category="blockswap", force=True
700+
)
701+
return self
690702

691-
# Use original method stored as attribute
703+
# Allow movement (either bypass is enabled or target is offload device)
692704
if hasattr(self, '_original_to'):
693705
return self._original_to(device, *args, **kwargs)
694706
else:
695-
# This shouldn't happen, but fallback to super().to()
707+
# Fallback - shouldn't happen
696708
return super(type(self), self).to(device, *args, **kwargs)
697709

698710
# Bind as a method to the model instance
@@ -712,7 +724,13 @@ def set_blockswap_bypass(runner, bypass: bool, debug):
712724
if not hasattr(runner, "_blockswap_active") or not runner._blockswap_active:
713725
return
714726

715-
runner._blockswap_bypass_protection = bypass
727+
# Get the actual model (handle FP8CompatibleDiT wrapper)
728+
model = runner.dit
729+
if hasattr(model, "dit_model"):
730+
model = model.dit_model
731+
732+
# Store on model so it survives runner recreation during caching
733+
model._blockswap_bypass_protection = bypass
716734

717735
if bypass:
718736
debug.log("BlockSwap protection disabled to allow model DiT offloading", category="success")
@@ -741,11 +759,16 @@ def cleanup_blockswap(runner, keep_state_for_cache=False):
741759

742760
debug = runner.debug
743761

744-
# Check if there's any BlockSwap state to clean up
762+
# Get the actual model (handle FP8CompatibleDiT wrapper)
763+
model = runner.dit
764+
if hasattr(model, "dit_model"):
765+
model = model.dit_model
766+
767+
# Check if there's any BlockSwap state to clean up (check both runner and model)
745768
has_blockswap_state = (
746769
hasattr(runner, "_blockswap_active") or
747-
hasattr(runner, "_block_swap_config") or
748-
hasattr(runner, "_blockswap_bypass_protection")
770+
hasattr(model, "_block_swap_config") or
771+
hasattr(model, "_blockswap_bypass_protection")
749772
)
750773

751774
if not has_blockswap_state:
@@ -757,7 +780,7 @@ def cleanup_blockswap(runner, keep_state_for_cache=False):
757780
# Minimal cleanup for caching - just mark as inactive and allow offloading
758781
# Everything else stays intact for fast reactivation
759782
if hasattr(runner, "_blockswap_active") and runner._blockswap_active:
760-
if not getattr(runner, "_blockswap_bypass_protection", False):
783+
if not getattr(model, "_blockswap_bypass_protection", False):
761784
set_blockswap_bypass(runner=runner, bypass=True, debug=debug)
762785
runner._blockswap_active = False
763786
debug.log("BlockSwap deactivated for caching (configuration preserved)", category="success")
@@ -829,7 +852,7 @@ def cleanup_blockswap(runner, keep_state_for_cache=False):
829852

830853
# 5. Clean up BlockSwap-specific attributes
831854
for attr in ['_blockswap_runner_ref', 'blocks_to_swap', 'main_device',
832-
'offload_device', '_blockswap_configured']:
855+
'offload_device']:
833856
if hasattr(model, attr):
834857
delattr(model, attr)
835858

src/optimization/memory_manager.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ def _handle_blockswap_model_movement(runner: Any, model: torch.nn.Module,
762762
else:
763763
# Moving to GPU (reload)
764764
# Check if we're in bypass mode (coming from offload)
765-
if not getattr(runner, "_blockswap_bypass_protection", False):
765+
if not getattr(model, "_blockswap_bypass_protection", False):
766766
# Not in bypass mode, blocks are already configured
767767
if debug:
768768
debug.log(f"{model_name} with BlockSwap active - blocks already distributed across devices, skipping movement", category="general")
@@ -787,7 +787,7 @@ def _handle_blockswap_model_movement(runner: Any, model: torch.nn.Module,
787787
# Restore blocks to their configured devices
788788
if hasattr(model, "blocks") and hasattr(model, "blocks_to_swap"):
789789
# Use configured offload_device from BlockSwap config
790-
offload_device = runner._block_swap_config.get("offload_device")
790+
offload_device = model._block_swap_config.get("offload_device")
791791
if not offload_device:
792792
raise ValueError("BlockSwap config missing offload_device")
793793

@@ -801,7 +801,7 @@ def _handle_blockswap_model_movement(runner: Any, model: torch.nn.Module,
801801
block.to(offload_device)
802802

803803
# Handle I/O components
804-
if not runner._block_swap_config.get("swap_io_components", False):
804+
if not model._block_swap_config.get("swap_io_components", False):
805805
# I/O components should be on GPU if not offloaded
806806
for name, module in model.named_children():
807807
if name != "blocks":
@@ -814,10 +814,10 @@ def _handle_blockswap_model_movement(runner: Any, model: torch.nn.Module,
814814

815815
if debug:
816816
# Get actual configuration from runner
817-
if hasattr(runner, '_block_swap_config'):
818-
blocks_on_gpu = runner._block_swap_config.get('total_blocks', 32) - runner._block_swap_config.get('blocks_swapped', 16)
819-
total_blocks = runner._block_swap_config.get('total_blocks', 32)
820-
main_device = runner._block_swap_config.get('main_device', 'GPU')
817+
if hasattr(model, '_block_swap_config'):
818+
blocks_on_gpu = model._block_swap_config.get('total_blocks', 32) - model._block_swap_config.get('blocks_swapped', 16)
819+
total_blocks = model._block_swap_config.get('total_blocks', 32)
820+
main_device = model._block_swap_config.get('main_device', 'GPU')
821821
debug.log(f"BlockSwap blocks restored to configured devices ({blocks_on_gpu}/{total_blocks} blocks on {str(main_device).upper()})", category="success")
822822
else:
823823
debug.log("BlockSwap blocks restored to configured devices", category="success")

src/utils/constants.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
# Version information
7-
__version__ = "2.5.9"
7+
__version__ = "2.5.10"
88

99
import os
1010
import warnings
@@ -55,14 +55,33 @@ def get_base_cache_dir() -> str:
5555

5656

5757
def get_all_model_paths() -> list:
58-
"""Get all registered model paths including those from extra_model_paths.yaml"""
58+
"""Get all registered model paths including those from extra_model_paths.yaml (case-insensitive)"""
5959
try:
6060
import folder_paths
6161
# Ensure default path is registered first
6262
get_base_cache_dir()
63-
# Get all paths registered for seedvr2 model type
64-
paths = folder_paths.get_folder_paths(SEEDVR2_MODEL_TYPE)
65-
return paths if paths else [get_base_cache_dir()]
63+
64+
# Case-insensitive lookup: search through all registered folder types
65+
# This handles any case variation users might use in extra_model_paths.yaml
66+
all_paths = []
67+
target_lower = SEEDVR2_MODEL_TYPE.lower()
68+
69+
# folder_paths.folder_names_and_paths is the underlying dict: {type: ([paths], extensions)}
70+
if hasattr(folder_paths, 'folder_names_and_paths'):
71+
for folder_type, (paths, _) in folder_paths.folder_names_and_paths.items():
72+
if folder_type.lower() == target_lower:
73+
all_paths.extend(paths)
74+
75+
# Remove duplicates while preserving order (os.path.normpath handles Windows/Linux path differences)
76+
seen = set()
77+
unique_paths = []
78+
for path in all_paths:
79+
normalized = os.path.normpath(path.lower())
80+
if normalized not in seen:
81+
seen.add(normalized)
82+
unique_paths.append(path)
83+
84+
return unique_paths if unique_paths else [get_base_cache_dir()]
6685
except:
6786
return [get_base_cache_dir()]
6887

0 commit comments

Comments
 (0)