Skip to content

Commit 668382d

Browse files
authored
Merge pull request #260 from AInVFX/main
v2.5.5: Fix RAM leak for long videos via on-demand reconstruction - to address #256
2 parents 9cea011 + b5c40fe commit 668382d

5 files changed

Lines changed: 151 additions & 67 deletions

File tree

README.md

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

3737
## 🚀 Updates
3838

39+
**2025.11.09 - Version 2.5.5**
40+
41+
- 💾 **Memory: Fixed RAM leak for long videos** - On-demand reconstruction with lightweight batch indices instead of storing full transformed videos, fixed release_tensor_memory to handle CPU/CUDA/MPS consistently, and refactored batch processing helpers
42+
3943
**2025.11.08 - Version 2.5.4**
4044

4145
- 🎨 **Fix: AdaIN color correction** - Replace `.view()` with `.reshape()` to handle non-contiguous tensors after spatial padding, resolving "view size is not compatible with input tensor's size and stride" error

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.4"
4+
version = "2.5.5"
55
authors = [
66
{name = "numz"},
77
{name = "adrientoupet"}

src/core/generation_phases.py

Lines changed: 142 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,108 @@
6666
)
6767

6868

69+
def _prepare_video_batch(
70+
images: torch.Tensor,
71+
start_idx: int,
72+
end_idx: int,
73+
uniform_padding: int = 0,
74+
debug: Optional['Debug'] = None,
75+
log_info: bool = False
76+
) -> torch.Tensor:
77+
"""
78+
Extract and prepare video batch with uniform padding and permutation.
79+
80+
Args:
81+
images: Source video frames [T, H, W, C]
82+
start_idx: Start frame index
83+
end_idx: End frame index (exclusive)
84+
uniform_padding: Number of frames to pad (0 = no padding)
85+
debug: Debug instance for optional logging
86+
log_info: If True, log padding operations (used during encoding only)
87+
88+
Returns:
89+
Prepared video in TCHW format
90+
"""
91+
# Extract frames (view/slice, not copy)
92+
video = images[start_idx:end_idx]
93+
94+
# Apply uniform padding if needed
95+
if uniform_padding > 0:
96+
if log_info and debug:
97+
current_frames = end_idx - start_idx
98+
debug.log(f"Sequence of {current_frames} frames", category="video", force=True, indent_level=1)
99+
debug.log(f"Padding batch: {uniform_padding} frame{'s' if uniform_padding != 1 else ''} added ({current_frames}{current_frames + uniform_padding}) for uniform batches",
100+
category="video", force=True, indent_level=1)
101+
video = pad_video_temporal(video, count=uniform_padding, temporal_dim=0, prepend=False, debug=None)
102+
103+
# Permute to TCHW format
104+
video = video.permute(0, 3, 1, 2)
105+
106+
return video
107+
108+
109+
def _apply_4n1_padding(video: torch.Tensor) -> torch.Tensor:
110+
"""
111+
Apply 4n+1 temporal padding constraint required by VAE.
112+
113+
Args:
114+
video: Video tensor in TCHW format
115+
116+
Returns:
117+
Padded video in TCHW format
118+
"""
119+
t = video.size(0)
120+
if t % 4 != 1:
121+
video = optimized_single_video_rearrange(video) # TCHW -> CTHW
122+
video = pad_video_temporal(video, temporal_dim=1, prepend=False, debug=None)
123+
video = optimized_single_video_rearrange(video) # CTHW -> TCHW
124+
return video
125+
126+
127+
def _reconstruct_and_transform_batch(
128+
ctx: Dict[str, Any],
129+
batch_idx: int,
130+
debug: Optional['Debug'] = None
131+
) -> torch.Tensor:
132+
"""
133+
Reconstruct and transform a video batch for color correction (Phase 4).
134+
135+
Args:
136+
ctx: Context with input_images, batch_metadata, video_transform
137+
batch_idx: Index of batch to reconstruct
138+
debug: Debug instance for logging
139+
140+
Returns:
141+
Transformed video in CTHW format, ready for color correction
142+
"""
143+
start_idx, end_idx, uniform_padding = ctx['batch_metadata'][batch_idx]
144+
145+
# Prepare video batch
146+
video = _prepare_video_batch(
147+
images=ctx['input_images'],
148+
start_idx=start_idx,
149+
end_idx=end_idx,
150+
uniform_padding=uniform_padding,
151+
debug=None,
152+
log_info=False
153+
)
154+
155+
# Apply 4n+1 padding using shared helper
156+
video = _apply_4n1_padding(video)
157+
158+
# Extract RGB and transform
159+
if ctx.get('is_rgba', False):
160+
rgb_video = video[:, :3, :, :]
161+
else:
162+
rgb_video = video
163+
164+
transformed_video = ctx['video_transform'](rgb_video)
165+
166+
del video
167+
168+
return transformed_video
169+
170+
69171
def encode_all_batches(
70172
runner: 'VideoDiffusionInfer',
71173
ctx: Dict[str, Any],
@@ -106,7 +208,7 @@ def encode_all_batches(
106208
107209
Returns:
108210
dict: Context containing:
109-
- all_transformed_videos: List of (video, original_length) tuples
211+
- batch_metadata: Lightweight indices for on-demand transform reconstruction
110212
- all_latents: List of encoded latents ready for upscaling
111213
- Other state for subsequent phases
112214
@@ -179,9 +281,7 @@ def encode_all_batches(
179281
ctx['all_latents'] = [None] * num_encode_batches
180282
ctx['all_ori_lengths'] = [None] * num_encode_batches
181283
if color_correction != "none":
182-
ctx['all_transformed_videos'] = [None] * num_encode_batches
183-
else:
184-
ctx['all_transformed_videos'] = None
284+
ctx['batch_metadata'] = [None] * num_encode_batches
185285

186286
encode_idx = 0
187287

@@ -246,23 +346,21 @@ def encode_all_batches(
246346
debug.log(f"Encoding batch {encode_idx+1}/{num_encode_batches}", category="vae", force=True)
247347
debug.start_timer(f"encode_batch_{encode_idx+1}")
248348

249-
# Save original length BEFORE any padding (critical for post-processing trimming)
349+
# Save original length before any padding
250350
ori_length = current_frames
251351

252-
# Process current batch
253-
video = images[start_idx:end_idx]
254-
255-
# Log uniform padding if applied
352+
# Prepare video batch with uniform padding
353+
video = _prepare_video_batch(
354+
images=images,
355+
start_idx=start_idx,
356+
end_idx=end_idx,
357+
uniform_padding=batch_size - current_frames if is_uniform_padding else 0,
358+
debug=debug,
359+
log_info=True
360+
)
256361
if is_uniform_padding:
257-
padding_for_uniform = batch_size - current_frames
258-
debug.log(f"Sequence of {current_frames} frames", category="video", force=True, indent_level=1)
259-
debug.log(f"Padding batch: {padding_for_uniform} frame{'s' if padding_for_uniform != 1 else ''} added ({current_frames}{batch_size}) for uniform batches",
260-
category="video", force=True, indent_level=1)
261-
video = pad_video_temporal(video, count=padding_for_uniform, temporal_dim=0, prepend=False, debug=None)
262362
current_frames = batch_size
263363

264-
# Permute and move to device
265-
video = video.permute(0, 3, 1, 2)
266364
video = manage_tensor(
267365
tensor=video,
268366
target_device=ctx['vae_device'],
@@ -280,29 +378,23 @@ def encode_all_batches(
280378
if not is_uniform_padding:
281379
debug.log(f"Sequence of {t} frames", category="video", force=True, indent_level=1)
282380

283-
# Apply 4n+1 padding if needed
381+
# Apply 4n+1 padding using shared helper
284382
if t % 4 != 1:
285383
target = ((t-1)//4+1)*4+1
286384
padding_frames = target - t
287385
debug.log(f"Padding batch: {padding_frames} frame{'s' if padding_frames != 1 else ''} added ({t}{target}) to meet 4n+1 constraint",
288386
category="video", force=True, indent_level=1)
289-
290-
# Pad video using reversed frames (TCHW format, need to convert to CTHW)
291-
video = optimized_single_video_rearrange(video) # TCHW -> CTHW
292-
video = pad_video_temporal(video, temporal_dim=1, prepend=False, debug=None)
293-
video = optimized_single_video_rearrange(video) # CTHW -> TCHW
387+
# Apply 4n+1 padding to match exact frame count from encoding
388+
video = _apply_4n1_padding(video)
294389

295-
# Extract RGB for transforms (view, not copy)
390+
# Apply transformations (matches reconstruction logic)
296391
if ctx.get('is_rgba', False):
297-
rgb_for_transform = video[:, :3, :, :]
298392
debug.log(f"Extracted Alpha channel for edge-guided upscaling", category="alpha", indent_level=1)
393+
rgb_video = video[:, :3, :, :]
299394
else:
300-
rgb_for_transform = video
301-
302-
# Apply transformations (to RGB from already-padded video)
303-
transformed_video = ctx['video_transform'](rgb_for_transform)
395+
rgb_video = video
304396

305-
del rgb_for_transform
397+
transformed_video = ctx['video_transform'](rgb_video)
306398

307399
# Apply input noise if requested (to reduce artifacts at high resolutions)
308400
if input_noise_scale > 0:
@@ -325,6 +417,10 @@ def encode_all_batches(
325417
# Store original length for proper trimming later
326418
ctx['all_ori_lengths'][encode_idx] = ori_length
327419

420+
# Store batch frame indices for on-demand reconstruction
421+
if color_correction != "none":
422+
ctx['batch_metadata'][encode_idx] = (start_idx, end_idx, batch_size - ori_length if is_uniform_padding else 0)
423+
328424
# Extract and store Alpha and RGB from padded original video (before encoding)
329425
if ctx.get('is_rgba', False):
330426
if 'all_alpha_channels' not in ctx:
@@ -375,26 +471,9 @@ def encode_all_batches(
375471

376472
# Encode to latents
377473
cond_latents = runner.vae_encode([transformed_video])
378-
379-
# Store transformed video for color correction after encoding
380-
if color_correction != "none":
381-
if ctx['tensor_offload_device'] is not None:
382-
# Move to offload device to free VRAM
383-
ctx['all_transformed_videos'][encode_idx] = manage_tensor(
384-
tensor=transformed_video,
385-
target_device=ctx['tensor_offload_device'],
386-
tensor_name=f"transformed_video_{encode_idx+1}",
387-
debug=debug,
388-
reason="storing input reference for color correction",
389-
indent_level=1
390-
)
391-
else:
392-
# No offload device - keep reference on VAE device
393-
ctx['all_transformed_videos'][encode_idx] = transformed_video
394-
395-
# Clean up transformed_video reference if not needed or already offloaded
396-
if color_correction == "none" or ctx['tensor_offload_device'] is not None:
397-
del transformed_video
474+
475+
# Don't store transformed_video - will reconstruct on-demand in Phase 4
476+
del transformed_video, rgb_video
398477

399478
# Convert from VAE dtype to compute dtype and offload to avoid VRAM accumulation
400479
if ctx['tensor_offload_device'] is not None and (cond_latents[0].is_cuda or cond_latents[0].is_mps):
@@ -1031,13 +1110,14 @@ def postprocess_all_batches(
10311110
video_idx = min(batch_idx, len(ctx['all_ori_lengths']) - 1)
10321111
ori_length = ctx['all_ori_lengths'][video_idx] if 'all_ori_lengths' in ctx else sample.shape[0]
10331112

1034-
# Retrieve transformed video early for consistent trimming
1113+
# Reconstruct transformed video on-demand for color correction
10351114
input_video = None
1036-
if color_correction != "none" and ctx.get('all_transformed_videos') is not None:
1037-
if video_idx < len(ctx['all_transformed_videos']) and ctx['all_transformed_videos'][video_idx] is not None:
1038-
transformed_video = ctx['all_transformed_videos'][video_idx]
1039-
# Convert transformed video from C T H W to T C H W format
1115+
if color_correction != "none" and ctx.get('batch_metadata') is not None:
1116+
if video_idx < len(ctx['batch_metadata']) and ctx['batch_metadata'][video_idx] is not None:
1117+
# Reconstruct transformation
1118+
transformed_video = _reconstruct_and_transform_batch(ctx, video_idx, debug)
10401119
input_video = optimized_single_video_rearrange(transformed_video)
1120+
del transformed_video
10411121

10421122
# Trim both sample and input_video to original length if necessary (handles temporal padding)
10431123
if ori_length < sample.shape[0]:
@@ -1112,9 +1192,8 @@ def postprocess_all_batches(
11121192

11131193
debug.end_timer(f"color_correction_{color_correction}", f"Color correction ({color_correction})")
11141194

1115-
# Free the transformed video
1116-
ctx['all_transformed_videos'][video_idx] = None
1117-
del input_video, transformed_video
1195+
# Free the reconstructed transformed video
1196+
del input_video
11181197

11191198
# Recombine with Alpha if it was present in input
11201199
if has_alpha and alpha_channel is not None:
@@ -1299,8 +1378,7 @@ def postprocess_all_batches(
12991378
del ctx['video_transform']
13001379

13011380
# 3. Clean up storage lists (all_latents, all_alpha_channels, etc.)
1302-
tensor_storage_keys = ['all_latents', 'all_transformed_videos',
1303-
'all_alpha_channels', 'all_input_rgb']
1381+
tensor_storage_keys = ['all_latents', 'all_alpha_channels', 'all_input_rgb']
13041382
for key in tensor_storage_keys:
13051383
if key in ctx and ctx[key]:
13061384
release_tensor_collection(ctx[key])
@@ -1311,6 +1389,11 @@ def postprocess_all_batches(
13111389
del ctx['all_ori_lengths']
13121390
if 'true_target_dims' in ctx:
13131391
del ctx['true_target_dims']
1392+
if 'batch_metadata' in ctx:
1393+
del ctx['batch_metadata']
1394+
if 'input_images' in ctx:
1395+
release_tensor_memory(ctx['input_images'])
1396+
del ctx['input_images']
13141397

13151398
debug.end_timer("phase4_postprocessing", "Phase 4: Post-processing complete", show_breakdown=True)
13161399
debug.log_memory_state("After phase 4 (Post-processing)", show_tensors=False)

src/core/generation_utils.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,11 @@ def _normalize_device(device_spec: Optional[Union[str, torch.device]]) -> torch.
375375
'interrupt_fn': interrupt_fn,
376376
'video_transform': None,
377377
'text_embeds': None,
378-
'all_transformed_videos': [],
379378
'all_latents': [],
380379
'all_upscaled_latents': [],
381380
'batch_samples': [],
382381
'final_video': None,
383382
'comfyui_available': comfyui_available,
384-
'interrupt_fn': interrupt_fn,
385383
}
386384

387385
if debug:

src/optimization/memory_manager.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -423,12 +423,11 @@ def clear_rope_lru_caches(model: Optional[torch.nn.Module], debug: Optional['Deb
423423

424424

425425
def release_tensor_memory(tensor: Optional[torch.Tensor]) -> None:
426-
"""Release tensor memory properly without CPU allocation"""
426+
"""Release tensor memory from any device (CPU/CUDA/MPS)"""
427427
if tensor is not None and torch.is_tensor(tensor):
428-
if tensor.is_cuda or tensor.is_mps:
429-
# Release GPU memory directly without CPU transfer
430-
if tensor.numel() > 0:
431-
tensor.data.set_()
428+
# Release storage for all devices (CPU, CUDA, MPS)
429+
if tensor.numel() > 0:
430+
tensor.data.set_()
432431
tensor.grad = None
433432

434433

0 commit comments

Comments
 (0)