Files
huchukatoandDevin 0c5cd8283c Fix device mismatch in infer_tiled: move inputs to CUDA before tiling
The frames arrive from generate_frames_rife on CPU, but the engine output
(tile_out) is on CUDA. The accumulator tensors (output, weight, window)
were created on frame_0.device (CPU), causing a RuntimeError when mixing
devices in the feathering blend. Now all tensors are explicitly moved to
the CUDA device at the start of infer_tiled.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-05 10:44:51 +02:00

291 lines
12 KiB
Python

# https://github.com/Fannovel16/ComfyUI-Frame-Interpolation/blob/main/vfi_utils.py
import os
import gc
import torch
import typing
import einops
from comfy.model_management import soft_empty_cache, get_torch_device
import numpy as np
from comfy.utils import ProgressBar
from colored import Fore, Back, Style
DEVICE = get_torch_device()
class InterpolationStateList():
"""Carries a list of frame indices and a flag indicating whether the list
is a skip-list (frames to skip) or a keep-list (frames to interpolate).
Ported from Fannovel16/ComfyUI-Frame-Interpolation.
"""
def __init__(self, frame_indices: typing.List[int], is_skip_list: bool):
self.frame_indices = frame_indices
self.is_skip_list = is_skip_list
def is_frame_skipped(self, frame_index):
is_frame_in_list = frame_index in self.frame_indices
return self.is_skip_list and is_frame_in_list or not self.is_skip_list and not is_frame_in_list
def load_file_from_github_release(model_type, ckpt_name):
error_strs = []
for i, base_model_download_url in enumerate(BASE_MODEL_DOWNLOAD_URLS):
try:
return load_file_from_url(base_model_download_url + ckpt_name, get_ckpt_container_path(model_type))
except Exception:
traceback_str = traceback.format_exc()
if i < len(BASE_MODEL_DOWNLOAD_URLS) - 1:
print("Failed! Trying another endpoint.")
error_strs.append(f"Error when downloading from: {base_model_download_url + ckpt_name}\n\n{traceback_str}")
error_str = '\n\n'.join(error_strs)
raise Exception(f"Tried all GitHub base urls to download {ckpt_name} but no suceess. Below is the error log:\n\n{error_str}")
def logger(msg):
print(f'{Style.reset}{Fore.cyan}⚡ [Rife Tensorrt] - {msg}{Style.reset}')
def preprocess_frames(frames):
return einops.rearrange(frames[..., :3], "n h w c -> n c h w")
def postprocess_frames(frames):
# Always return float32 — numpy and all downstream ComfyUI nodes require it.
# Engine output may be fp16 when built with precision="fp16".
return einops.rearrange(frames, "n c h w -> n h w c")[..., :3].to(torch.float32).cpu()
def _make_feather_window(tile_h, tile_w, overlap):
"""Build a 2D cosine-feathered weight window for blending tiles."""
def _ramp(size):
win = torch.ones(size)
ramp = min(overlap, size // 2)
if ramp > 0:
t = torch.linspace(0, 1, ramp)
fade = 0.5 * (1 - torch.cos(torch.pi * t))
win[:ramp] = fade
win[-ramp:] = fade.flip(0)
return win
win_h = _ramp(tile_h)
win_w = _ramp(tile_w)
return win_h.view(1, 1, -1, 1) * win_w.view(1, 1, 1, -1)
def _compute_tile_positions(total, tile_size, overlap):
"""Return list of start positions covering [0, total) with given tile size and overlap."""
if total <= tile_size:
return [0]
stride = tile_size - overlap
positions = list(range(0, total - tile_size + 1, stride))
# Ensure the last tile reaches the end
if positions[-1] + tile_size < total:
positions.append(total - tile_size)
# Deduplicate (can happen when total is only slightly larger than tile_size)
return sorted(set(positions))
def infer_tiled(frame_0, frame_1, timestep, infer_fn, min_hw, max_hw, overlap=128):
"""Run inference with automatic tiling/padding when the frame dimensions
fall outside the TensorRT engine's optimization profile.
Args:
frame_0, frame_1: [1, C, H, W] tensors (CPU or CUDA).
timestep: float.
infer_fn: callable(f0, f1, t) -> [1, C, H, W] (the raw engine.infer wrapper).
min_hw: (min_h, min_w) from the engine profile.
max_hw: (max_h, max_w) from the engine profile.
overlap: feathering overlap in pixels between adjacent tiles.
Returns:
[1, C, H, W] interpolated frame at the original resolution (on CPU,
ready for generate_frames_rife which does .detach().cpu()).
"""
# Move inputs to CUDA — the engine inference requires GPU tensors.
device = get_torch_device()
frame_0 = frame_0.to(device)
frame_1 = frame_1.to(device)
_, C, H, W = frame_0.shape
min_h, min_w = min_hw
max_h, max_w = max_hw
needs_tile = H > max_h or W > max_w
needs_pad = H < min_h or W < min_w
# Fast path: dimensions are within profile
if not needs_tile and not needs_pad:
return infer_fn(frame_0, frame_1, timestep)
# --- Padding (under-min) ---
pad_h_before = pad_h_after = pad_w_before = pad_w_after = 0
if needs_pad:
pad_h_before = max(0, (min_h - H + 1) // 2)
pad_h_after = max(0, min_h - H - pad_h_before)
pad_w_before = max(0, (min_w - W + 1) // 2)
pad_w_after = max(0, min_w - W - pad_w_before)
frame_0 = torch.nn.functional.pad(frame_0, (pad_w_before, pad_w_after, pad_h_before, pad_h_after), mode='replicate')
frame_1 = torch.nn.functional.pad(frame_1, (pad_w_before, pad_w_after, pad_h_before, pad_h_after), mode='replicate')
_, _, H_pad, W_pad = frame_0.shape
else:
H_pad, W_pad = H, W
# --- Tiling (over-max) ---
if H_pad > max_h or W_pad > max_w:
tile_h = min(max_h, H_pad)
tile_w = min(max_w, W_pad)
h_positions = _compute_tile_positions(H_pad, tile_h, overlap)
w_positions = _compute_tile_positions(W_pad, tile_w, overlap)
window = _make_feather_window(tile_h, tile_w, overlap).to(device)
output = torch.zeros(1, C, H_pad, W_pad, device=device, dtype=frame_0.dtype)
weight = torch.zeros(1, 1, H_pad, W_pad, device=device, dtype=frame_0.dtype)
for h0 in h_positions:
for w0 in w_positions:
h1, w1 = h0 + tile_h, w0 + tile_w
t0 = frame_0[:, :, h0:h1, w0:w1]
t1 = frame_1[:, :, h0:h1, w0:w1]
tile_out = infer_fn(t0, t1, timestep)
output[:, :, h0:h1, w0:w1] += tile_out * window
weight[:, :, h0:h1, w0:w1] += window
result = output / weight.clamp(min=1e-8)
else:
result = infer_fn(frame_0, frame_1, timestep)
# --- Crop back if we padded ---
if needs_pad:
result = result[:, :, pad_h_before:pad_h_before + H, pad_w_before:pad_w_before + W]
return result
def generate_frames_rife(
frames,
clear_cache_after_n_frames,
multiplier,
return_middle_frame_function,
interpolation_states: InterpolationStateList = None,
batch_size: int = 1,
batch_infer_function=None,
):
"""Interpolate frames.
Args:
frames: input tensor [N, C, H, W] (already preprocessed).
clear_cache_after_n_frames: clear CUDA cache every N interpolated frames.
multiplier: int (uniform) or list[int] (per-pair schedule). A list is
padded with 2 for any missing trailing pairs, matching Fannovel16.
return_middle_frame_function: callable(frame0, frame1, timestep) -> middle.
Used when batch_size == 1 (or when batch_infer_function is None).
interpolation_states: optional InterpolationStateList to skip specific pairs.
batch_size: number of (pair, timestep) tasks per GPU call. >1 enables
batched inference (requires batch_infer_function and an engine built
with max_batch >= batch_size).
batch_infer_function: optional callable(frame0_batch, frame1_batch, timestep_batch)
-> middle_batch. frame0_batch/frame1_batch are [B,C,H,W], timestep_batch
is [B]. Returns [B,C,H,W]. Required when batch_size > 1.
"""
n_pairs = len(frames) - 1
# Normalise multiplier to a per-pair list
if isinstance(multiplier, int):
multipliers = [int(multiplier)] * n_pairs
else:
multipliers = list(map(int, multiplier))
multipliers += [2] * (n_pairs - len(multipliers))
# Upper bound on output length (sum of per-pair multipliers + final frame)
total_out = sum(multipliers) + 1
output_frames = torch.zeros(total_out, *frames.shape[1:], device="cpu")
out_len = 0
number_of_frames_processed_since_last_cleared_cuda_cache = 0
# Decide whether to use batched inference
use_batch = batch_size > 1 and batch_infer_function is not None
if use_batch:
# Build a flat list of all (pair_idx, timestep) tasks, skipping excluded pairs.
# Each task produces exactly one intermediate frame.
tasks = []
for pair_idx in range(n_pairs):
if interpolation_states is not None and interpolation_states.is_frame_skipped(pair_idx):
continue
m = multipliers[pair_idx]
for step in range(1, m):
tasks.append((pair_idx, step / m))
# Storage for intermediate frames, keyed by pair index
results = {i: [] for i in range(n_pairs)}
pos = 0
while pos < len(tasks):
batch_tasks = tasks[pos : pos + batch_size]
B = len(batch_tasks)
frame0_list = [frames[pair_idx : pair_idx + 1] for pair_idx, _ in batch_tasks]
frame1_list = [frames[pair_idx + 1 : pair_idx + 2] for pair_idx, _ in batch_tasks]
timestep_list = [dt for _, dt in batch_tasks]
frame0_batch = torch.cat(frame0_list, dim=0)
frame1_batch = torch.cat(frame1_list, dim=0)
timestep_batch = torch.tensor(timestep_list, dtype=torch.float32)
middle_batch = batch_infer_function(frame0_batch, frame1_batch, timestep_batch).detach().cpu()
for idx, (pair_idx, _) in enumerate(batch_tasks):
results[pair_idx].append(middle_batch[idx : idx + 1])
number_of_frames_processed_since_last_cleared_cuda_cache += 1
if number_of_frames_processed_since_last_cleared_cuda_cache >= clear_cache_after_n_frames:
soft_empty_cache()
gc.collect()
number_of_frames_processed_since_last_cleared_cuda_cache = 0
logger("Clearing cache...")
pos += B
# Assemble output: each original frame followed by its interpolated frames
for pair_idx in range(n_pairs):
output_frames[out_len] = frames[pair_idx : pair_idx + 1]
out_len += 1
for mid in results[pair_idx]:
output_frames[out_len] = mid
out_len += 1
else:
# Sequential path (original behaviour, batch_size == 1)
for frame_itr in range(n_pairs):
frame_0 = frames[frame_itr:frame_itr+1]
frame_1 = frames[frame_itr+1:frame_itr+2]
output_frames[out_len] = frame_0
out_len += 1
if interpolation_states is not None and interpolation_states.is_frame_skipped(frame_itr):
continue
m = multipliers[frame_itr]
for middle_i in range(1, m):
timestep = middle_i / m
middle_frame = return_middle_frame_function(frame_0, frame_1, timestep).detach().cpu()
output_frames[out_len] = middle_frame
out_len += 1
number_of_frames_processed_since_last_cleared_cuda_cache += 1
if number_of_frames_processed_since_last_cleared_cuda_cache >= clear_cache_after_n_frames:
soft_empty_cache()
gc.collect()
number_of_frames_processed_since_last_cleared_cuda_cache = 0
logger("Clearing cache...")
# Append final frame
output_frames[out_len] = frames[-1:]
new_frames_count = sum(max(m - 1, 0) for m in multipliers)
logger(f"done! - {new_frames_count} new frames generated at resolution: {output_frames[0].shape}")
out_len += 1
soft_empty_cache()
gc.collect()
logger("Final clearing cache done ...")
res = output_frames[:out_len]
return res