The download_file() function now validates: - HTTP status code (fails on non-200) - File size (removes files <1KB as likely error pages) - ONNX header (detects HTML pages masquerading as .onnx files) The ONNX download in AutoLoadRifeTensorrtModel now tries multiple HuggingFace sources before giving up, with clear error message pointing users to manual export_onnx.py as fallback. This prevents the cryptic "factory function returned nullptr" error when TensorRT attempts to parse a corrupted/empty/HTML file as ONNX. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
745 lines
34 KiB
Python
745 lines
34 KiB
Python
import torch
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import re
|
|
from pathlib import Path
|
|
from comfy.model_management import get_torch_device
|
|
from .vfi_utilities import preprocess_frames, postprocess_frames, generate_frames_rife, logger, InterpolationStateList, infer_tiled
|
|
from .trt_utilities import Engine
|
|
from .utilities import download_file, ColoredLogger
|
|
import folder_paths
|
|
import time
|
|
|
|
def detect_cloud_environment():
|
|
"""Detect if running in cloud environment with CUDA 12 where CUDA graph may have issues"""
|
|
# Check for common cloud environment indicators
|
|
cloud_indicators = [
|
|
'RUNPOD', 'COLAB', 'KAGGLE', 'PAPERSPACE', 'GOOGLE_COLAB',
|
|
'JUPYTER_RUNTIME', 'DOCKER', 'CONTAINER'
|
|
]
|
|
|
|
# Check environment variables
|
|
for indicator in cloud_indicators:
|
|
if indicator in os.environ:
|
|
print(f"🌩️ Detected cloud environment: {indicator}")
|
|
|
|
# Check CUDA version - only disable CUDA graph for CUDA 12
|
|
try:
|
|
import torch
|
|
cuda_version = torch.version.cuda
|
|
if cuda_version and cuda_version.startswith('12'):
|
|
print(f"🌩️ CUDA {cuda_version} detected in cloud environment - disabling CUDA graph for stability")
|
|
return True
|
|
else:
|
|
print(f"✅ CUDA {cuda_version} detected - keeping CUDA graph enabled")
|
|
return False
|
|
except:
|
|
print("⚠️ Could not detect CUDA version, disabling CUDA graph for safety")
|
|
return True
|
|
|
|
# Check if running in Docker/container
|
|
if os.path.exists('/.dockerenv'):
|
|
print("🌩️ Detected Docker environment")
|
|
try:
|
|
import torch
|
|
cuda_version = torch.version.cuda
|
|
if cuda_version and cuda_version.startswith('12'):
|
|
print(f"🌩️ CUDA {cuda_version} in Docker - disabling CUDA graph")
|
|
return True
|
|
else:
|
|
print(f"✅ CUDA {cuda_version} in Docker - keeping CUDA graph enabled")
|
|
return False
|
|
except:
|
|
print("⚠️ Could not detect CUDA version in Docker, disabling CUDA graph")
|
|
return True
|
|
|
|
# Check for common cloud provider files
|
|
cloud_files = [
|
|
'/run/cloud-init/result.json',
|
|
'/var/lib/cloud/data/result.json'
|
|
]
|
|
|
|
for file_path in cloud_files:
|
|
if os.path.exists(file_path):
|
|
print("🌩️ Detected cloud environment via cloud-init")
|
|
try:
|
|
import torch
|
|
cuda_version = torch.version.cuda
|
|
if cuda_version and cuda_version.startswith('12'):
|
|
print(f"🌩️ CUDA {cuda_version} in cloud - disabling CUDA graph")
|
|
return True
|
|
else:
|
|
print(f"✅ CUDA {cuda_version} in cloud - keeping CUDA graph enabled")
|
|
return False
|
|
except:
|
|
print("⚠️ Could not detect CUDA version, disabling CUDA graph for safety")
|
|
return True
|
|
|
|
return False
|
|
|
|
# Auto-detect CUDA and install appropriate TensorRT packages
|
|
def _auto_install_tensorrt():
|
|
"""Auto-detect CUDA version and install the matching TensorRT wheels.
|
|
|
|
The NVIDIA CUDA Toolkit must already be installed on the system.
|
|
This function installs only the TensorRT packages via pip.
|
|
A marker file prevents repeated install attempts on every ComfyUI startup.
|
|
"""
|
|
disable_auto_install = os.environ.get("DISABLE_TENSORRT_AUTO_INSTALL", "false").lower() == "true"
|
|
if disable_auto_install:
|
|
print("[ComfyUI-RIFE-TensorRT] Auto-installation disabled via DISABLE_TENSORRT_AUTO_INSTALL")
|
|
return True
|
|
|
|
node_dir = Path(__file__).resolve().parent
|
|
installed_marker = node_dir / ".tensorrt_auto_installed"
|
|
failed_marker = node_dir / ".tensorrt_auto_install_failed"
|
|
|
|
# Skip if we already installed successfully in a previous run AND tensorrt is importable.
|
|
if installed_marker.exists():
|
|
try:
|
|
import tensorrt
|
|
return True
|
|
except ImportError:
|
|
print("[ComfyUI-RIFE-TensorRT] Install marker exists but tensorrt is not importable; retrying.")
|
|
try:
|
|
installed_marker.unlink()
|
|
except Exception:
|
|
pass
|
|
|
|
# Avoid retrying too often after a failure (do not block every startup).
|
|
if failed_marker.exists():
|
|
try:
|
|
last_fail = failed_marker.stat().st_mtime
|
|
if time.time() - last_fail < 3600:
|
|
print("[ComfyUI-RIFE-TensorRT] Recent failed install attempt; skipping auto-install.")
|
|
print("To retry now, delete the .tensorrt_auto_install_failed marker file or wait 1 hour.")
|
|
return False
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
# Check if TensorRT is already installed.
|
|
try:
|
|
import tensorrt
|
|
print(f"[ComfyUI-RIFE-TensorRT] TensorRT already installed (version: {tensorrt.__version__})")
|
|
installed_marker.touch()
|
|
return True
|
|
except ImportError:
|
|
print("[ComfyUI-RIFE-TensorRT] TensorRT not found, detecting CUDA version...")
|
|
|
|
# Detect CUDA version
|
|
cuda_version = None
|
|
|
|
# Try PyTorch CUDA version first (most reliable on cloud platforms)
|
|
try:
|
|
if hasattr(torch, 'version') and hasattr(torch.version, 'cuda'):
|
|
pytorch_cuda = torch.version.cuda
|
|
if pytorch_cuda:
|
|
cuda_version = pytorch_cuda
|
|
print(f"[ComfyUI-RIFE-TensorRT] Detected CUDA version from PyTorch: {cuda_version}")
|
|
except Exception as e:
|
|
print(f"[ComfyUI-RIFE-TensorRT] Could not detect CUDA from PyTorch: {e}")
|
|
|
|
# Try nvcc command as fallback
|
|
if not cuda_version:
|
|
try:
|
|
result = subprocess.run("nvcc --version", shell=True, capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
match = re.search(r"release (\d+\.\d+)", result.stdout)
|
|
if match:
|
|
cuda_version = match.group(1)
|
|
print(f"[ComfyUI-RIFE-TensorRT] Detected CUDA version from nvcc: {cuda_version}")
|
|
except Exception:
|
|
pass
|
|
|
|
# Try CUDA_PATH
|
|
if not cuda_version and os.environ.get("CUDA_PATH"):
|
|
nvcc_path = os.path.join(os.environ["CUDA_PATH"], "bin", "nvcc")
|
|
if os.path.exists(nvcc_path):
|
|
try:
|
|
result = subprocess.run(f'"{nvcc_path}" --version', shell=True, capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
match = re.search(r"release (\d+\.\d+)", result.stdout)
|
|
if match:
|
|
cuda_version = match.group(1)
|
|
print(f"[ComfyUI-RIFE-TensorRT] Detected CUDA via CUDA_PATH: {cuda_version}")
|
|
except Exception:
|
|
pass
|
|
|
|
# Try CUDA_HOME
|
|
if not cuda_version and os.environ.get("CUDA_HOME"):
|
|
nvcc_path = os.path.join(os.environ["CUDA_HOME"], "bin", "nvcc")
|
|
if os.path.exists(nvcc_path):
|
|
try:
|
|
result = subprocess.run(f'"{nvcc_path}" --version', shell=True, capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
match = re.search(r"release (\d+\.\d+)", result.stdout)
|
|
if match:
|
|
cuda_version = match.group(1)
|
|
print(f"[ComfyUI-RIFE-TensorRT] Detected CUDA via CUDA_HOME: {cuda_version}")
|
|
except Exception:
|
|
pass
|
|
|
|
if not cuda_version:
|
|
print("[ComfyUI-RIFE-TensorRT] WARNING: Could not detect CUDA version automatically.")
|
|
print("The NVIDIA CUDA Toolkit must be installed before TensorRT can work.")
|
|
print("Please run 'python install.py' manually after installing CUDA.")
|
|
failed_marker.touch()
|
|
return False
|
|
|
|
major_version = int(cuda_version.split('.')[0])
|
|
|
|
if major_version == 13:
|
|
print("[ComfyUI-RIFE-TensorRT] Installing CUDA 13 TensorRT packages (RTX 50 series)")
|
|
req_file = "requirements_cu13.txt"
|
|
elif major_version == 12:
|
|
print("[ComfyUI-RIFE-TensorRT] Installing CUDA 12 TensorRT packages (RTX 30/40 series)")
|
|
req_file = "requirements_cu12.txt"
|
|
else:
|
|
print(f"[ComfyUI-RIFE-TensorRT] Unsupported CUDA version: {cuda_version}")
|
|
failed_marker.touch()
|
|
return False
|
|
|
|
req_path = node_dir / req_file
|
|
if not req_path.exists():
|
|
print(f"[ComfyUI-RIFE-TensorRT] Missing requirements file: {req_path}")
|
|
failed_marker.touch()
|
|
return False
|
|
|
|
# Install base dependencies first, then the CUDA-specific TensorRT wheels.
|
|
for req_name in ["requirements.txt", req_file]:
|
|
req_file_path = node_dir / req_name
|
|
if not req_file_path.exists():
|
|
continue
|
|
print(f"[ComfyUI-RIFE-TensorRT] Installing from {req_name}...")
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", "--no-input", "--prefer-binary", "-r", str(req_file_path)],
|
|
capture_output=True
|
|
)
|
|
if result.returncode != 0:
|
|
print(f"[ComfyUI-RIFE-TensorRT] Failed to install {req_name}")
|
|
print(result.stderr.decode(errors="replace"))
|
|
failed_marker.touch()
|
|
return False
|
|
# Show pip stdout so the user can see progress / warnings.
|
|
stdout = result.stdout.decode(errors="replace").strip()
|
|
if stdout:
|
|
print(stdout)
|
|
|
|
installed_marker.touch()
|
|
print("[ComfyUI-RIFE-TensorRT] TensorRT installation completed successfully!")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"[ComfyUI-RIFE-TensorRT] Auto-installation failed: {e}")
|
|
print("Please run 'python install.py' manually to install TensorRT")
|
|
try:
|
|
failed_marker.touch()
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
# Run auto-install on module import
|
|
_auto_install_tensorrt()
|
|
|
|
# Auto-detect CUDA toolkit and add DLL path before importing polygraphy
|
|
def _setup_cuda_dll_path():
|
|
"""Auto-detect CUDA toolkit and add cudart64 DLL path on Windows."""
|
|
if not sys.platform.startswith("win"):
|
|
return
|
|
|
|
cuda_root = None
|
|
|
|
# Check for CUDA_PATH or CUDA_HOME environment variables
|
|
cuda_root = os.environ.get("CUDA_PATH") or os.environ.get("CUDA_HOME")
|
|
|
|
if not cuda_root:
|
|
# Try default Windows install location
|
|
program_files = os.environ.get("PROGRAMFILES")
|
|
if program_files:
|
|
cuda_base = Path(program_files) / "NVIDIA GPU Computing Toolkit" / "CUDA"
|
|
if cuda_base.exists():
|
|
# Find highest version directory
|
|
versions = sorted([d for d in cuda_base.iterdir() if d.is_dir()], reverse=True)
|
|
if versions:
|
|
cuda_root = str(versions[0])
|
|
|
|
if cuda_root:
|
|
cuda_path = Path(cuda_root)
|
|
# CUDA 13.0+ puts cudart64 in bin/x64 subdirectory
|
|
cuda_bin_x64 = cuda_path / "bin" / "x64"
|
|
if cuda_bin_x64.exists() and any(cuda_bin_x64.glob("cudart64*.dll")):
|
|
os.add_dll_directory(str(cuda_bin_x64))
|
|
return
|
|
# Fallback to regular bin directory for older CUDA versions
|
|
cuda_bin = cuda_path / "bin"
|
|
if cuda_bin.exists() and any(cuda_bin.glob("cudart64*.dll")):
|
|
os.add_dll_directory(str(cuda_bin))
|
|
return
|
|
|
|
# CUDA toolkit not found - print warning with download link
|
|
print("[ComfyUI-Rife-TensorRT] WARNING: CUDA toolkit not found.")
|
|
print(" Set CUDA_PATH environment variable or install CUDA toolkit.")
|
|
print(" Download: https://developer.nvidia.com/cuda-13-0-2-download-archive")
|
|
|
|
_setup_cuda_dll_path()
|
|
|
|
from polygraphy import cuda
|
|
import comfy.model_management as mm
|
|
import tensorrt
|
|
import json
|
|
|
|
ENGINE_DIR = os.path.join(folder_paths.models_dir, "tensorrt", "rife")
|
|
|
|
# Default resolution profiles (fallback if config is missing)
|
|
DEFAULT_RESOLUTION_PROFILES = {
|
|
"small": {"min": 384, "opt": 720, "max": 1080},
|
|
"medium": {"min": 672, "opt": 1080, "max": 1312}
|
|
}
|
|
|
|
# Logger for this module
|
|
rife_logger = ColoredLogger("ComfyUI-Rife-Tensorrt")
|
|
|
|
# Function to load configuration
|
|
def load_node_config(config_filename="load_rife_config.json"):
|
|
"""Loads node configuration from a JSON file."""
|
|
current_dir = os.path.dirname(__file__)
|
|
config_path = os.path.join(current_dir, config_filename)
|
|
|
|
default_config = {
|
|
"model": {
|
|
"options": ["rife49_ensemble_True_scale_1_sim"],
|
|
"default": "rife49_ensemble_True_scale_1_sim",
|
|
"tooltip": "Default model (fallback from code)"
|
|
},
|
|
"precision": {
|
|
"options": ["fp16", "fp32"],
|
|
"default": "fp16",
|
|
"tooltip": "Default precision (fallback from code)"
|
|
}
|
|
}
|
|
|
|
try:
|
|
with open(config_path, 'r') as f:
|
|
config = json.load(f)
|
|
rife_logger.info(f"Successfully loaded configuration from {config_filename}")
|
|
return config
|
|
except FileNotFoundError:
|
|
rife_logger.warning(f"Configuration file '{config_path}' not found. Using default fallback configuration.")
|
|
return default_config
|
|
except json.JSONDecodeError:
|
|
rife_logger.error(f"Error decoding JSON from '{config_path}'. Using default fallback configuration.")
|
|
return default_config
|
|
except Exception as e:
|
|
rife_logger.error(f"An unexpected error occurred while loading '{config_path}': {e}. Using default fallback.")
|
|
return default_config
|
|
|
|
# Load the configuration once when the module is imported
|
|
LOAD_RIFE_NODE_CONFIG = load_node_config()
|
|
|
|
|
|
class CustomResolutionConfig:
|
|
"""Node to configure custom resolution dimensions for TensorRT engine building."""
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"min_dim": ("INT", {"default": 384, "min": 64, "max": 4096, "step": 8, "tooltip": "Minimum resolution dimension"}),
|
|
"opt_dim": ("INT", {"default": 720, "min": 64, "max": 4096, "step": 8, "tooltip": "Optimal resolution dimension (most common)"}),
|
|
"max_dim": ("INT", {"default": 1312, "min": 64, "max": 4096, "step": 8, "tooltip": "Maximum resolution dimension"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("RIFE_RESOLUTION_CONFIG",)
|
|
RETURN_NAMES = ("resolution_config",)
|
|
FUNCTION = "configure"
|
|
CATEGORY = "⚡️ TensorRT/RIFE"
|
|
DESCRIPTION = "Configure custom resolution dimensions for RIFE TensorRT engine."
|
|
|
|
def configure(self, min_dim, opt_dim, max_dim):
|
|
config = {
|
|
"min": min_dim,
|
|
"opt": opt_dim,
|
|
"max": max_dim,
|
|
}
|
|
return (config,)
|
|
|
|
|
|
class AutoLoadRifeTensorrtModel:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
# Use the pre-loaded configuration
|
|
model_config = LOAD_RIFE_NODE_CONFIG.get("model", {})
|
|
precision_config = LOAD_RIFE_NODE_CONFIG.get("precision", {})
|
|
|
|
# Provide sensible defaults if keys are missing in the config
|
|
model_options = model_config.get("options", ["rife49_ensemble_True_scale_1_sim"])
|
|
model_default = model_config.get("default", "rife49_ensemble_True_scale_1_sim")
|
|
model_tooltip = model_config.get("tooltip", "Select a RIFE model.")
|
|
|
|
precision_options = precision_config.get("options", ["fp16", "fp32"])
|
|
precision_default = precision_config.get("default", "fp16")
|
|
precision_tooltip = precision_config.get("tooltip", "Select precision.")
|
|
|
|
# Resolution profile configuration
|
|
profile_config = LOAD_RIFE_NODE_CONFIG.get("resolution_profile", {})
|
|
profile_options = profile_config.get("options", ["small", "medium"])
|
|
# Ensure 'custom' is always available
|
|
if "custom" not in profile_options:
|
|
profile_options = profile_options + ["custom"]
|
|
profile_default = profile_config.get("default", "small")
|
|
profile_tooltip = profile_config.get("tooltip", "Resolution range for TensorRT engine. Use 'custom' with the INT inputs below.")
|
|
|
|
return {
|
|
"required": {
|
|
"model": (model_options, {"default": model_default, "tooltip": model_tooltip}),
|
|
"precision": (precision_options, {"default": precision_default, "tooltip": precision_tooltip}),
|
|
"resolution_profile": (profile_options, {"default": profile_default, "tooltip": profile_tooltip}),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 16, "step": 1,
|
|
"tooltip": "Max batch size for the engine profile. "
|
|
"Higher values allow batched inference (multiple frame pairs per GPU call) "
|
|
"for better throughput, but use more VRAM and require a rebuild. "
|
|
"Set to 1 for the original behaviour."}),
|
|
},
|
|
"optional": {
|
|
"custom_config": ("RIFE_RESOLUTION_CONFIG", {"tooltip": "Custom resolution config (used when profile='custom')"}),
|
|
}
|
|
}
|
|
|
|
RETURN_NAMES = ("rife_trt_model",)
|
|
RETURN_TYPES = ("RIFE_TRT_MODEL",)
|
|
CATEGORY = "⚡️ TensorRT/RIFE"
|
|
DESCRIPTION = "Load RIFE tensorrt models, they will be built automatically if not found."
|
|
FUNCTION = "load_rife_tensorrt_model"
|
|
|
|
def load_rife_tensorrt_model(self, model, precision, resolution_profile, batch_size=1, custom_config=None):
|
|
tensorrt_models_dir = os.path.join(folder_paths.models_dir, "tensorrt", "rife")
|
|
onnx_models_dir = os.path.join(folder_paths.models_dir, "onnx")
|
|
|
|
os.makedirs(tensorrt_models_dir, exist_ok=True)
|
|
os.makedirs(onnx_models_dir, exist_ok=True)
|
|
|
|
onnx_model_path = os.path.join(onnx_models_dir, f"{model}.onnx")
|
|
|
|
# Get resolution dimensions based on profile
|
|
if resolution_profile == "custom":
|
|
if custom_config is None:
|
|
rife_logger.warning("Custom profile selected but no custom_config provided. Using defaults that cover both small and medium ranges.")
|
|
dim_min, dim_opt, dim_max = 384, 720, 1312
|
|
else:
|
|
dim_min = custom_config.get("min", 384)
|
|
dim_opt = custom_config.get("opt", 720)
|
|
dim_max = custom_config.get("max", 1312)
|
|
# Use dimensions in profile name for custom engines
|
|
profile_name = f"custom_{dim_min}_{dim_opt}_{dim_max}"
|
|
else:
|
|
profiles = LOAD_RIFE_NODE_CONFIG.get("resolution_profiles", DEFAULT_RESOLUTION_PROFILES)
|
|
profile = profiles.get(resolution_profile, DEFAULT_RESOLUTION_PROFILES["small"])
|
|
dim_min = profile.get("min", 384)
|
|
dim_opt = profile.get("opt", 720)
|
|
dim_max = profile.get("max", 1080)
|
|
profile_name = resolution_profile
|
|
rife_logger.info(f"Using resolution profile '{profile_name}': min={dim_min}, opt={dim_opt}, max={dim_max}")
|
|
|
|
# Build tensorrt model path with detailed naming (includes profile)
|
|
engine_channel = 3
|
|
engine_min_batch = 1
|
|
engine_opt_batch = max(1, batch_size)
|
|
engine_max_batch = max(1, batch_size)
|
|
engine_min_h, engine_opt_h, engine_max_h = dim_min, dim_opt, dim_max
|
|
engine_min_w, engine_opt_w, engine_max_w = dim_min, dim_opt, dim_max
|
|
tensorrt_model_path = os.path.join(tensorrt_models_dir, f"{model}_{precision}_{profile_name}_{engine_min_batch}x{engine_channel}x{engine_min_h}x{engine_min_w}_{engine_opt_batch}x{engine_channel}x{engine_opt_h}x{engine_opt_w}_{engine_max_batch}x{engine_channel}x{engine_max_h}x{engine_max_w}_{tensorrt.__version__}.trt")
|
|
|
|
if not os.path.exists(tensorrt_model_path):
|
|
if not os.path.exists(onnx_model_path):
|
|
# Try multiple download sources in case the primary one fails
|
|
onnx_download_urls = [
|
|
f"https://huggingface.co/yuvraj108c/rife-onnx/resolve/main/{model}.onnx",
|
|
f"https://huggingface.co/marduk191/rife/resolve/main/{model}.onnx",
|
|
]
|
|
download_success = False
|
|
for url in onnx_download_urls:
|
|
try:
|
|
rife_logger.info(f"Downloading {url}")
|
|
download_file(url=url, save_path=onnx_model_path)
|
|
download_success = True
|
|
break
|
|
except Exception as e:
|
|
rife_logger.warning(f"Download failed from {url}: {e}")
|
|
continue
|
|
if not download_success:
|
|
raise RuntimeError(
|
|
f"Failed to download ONNX model '{model}.onnx' from all sources. "
|
|
f"You can manually export it using export_onnx.py and place it in: {onnx_model_path}"
|
|
)
|
|
else:
|
|
rife_logger.info(f"ONNX model found at: {onnx_model_path}")
|
|
|
|
rife_logger.info(f"Building TensorRT engine for {onnx_model_path}: {tensorrt_model_path}")
|
|
mm.soft_empty_cache()
|
|
s = time.time()
|
|
engine = Engine(tensorrt_model_path)
|
|
engine.build(
|
|
onnx_path=onnx_model_path,
|
|
fp16=True if precision == "fp16" else False,
|
|
input_profile=[
|
|
{
|
|
"img0": [(engine_min_batch, engine_channel, engine_min_h, engine_min_w), (engine_opt_batch, engine_channel, engine_opt_h, engine_opt_w), (engine_max_batch, engine_channel, engine_max_h, engine_max_w)],
|
|
"img1": [(engine_min_batch, engine_channel, engine_min_h, engine_min_w), (engine_opt_batch, engine_channel, engine_opt_h, engine_opt_w), (engine_max_batch, engine_channel, engine_max_h, engine_max_w)],
|
|
}
|
|
],
|
|
)
|
|
e = time.time()
|
|
rife_logger.info(f"Time taken to build: {(e-s)} seconds")
|
|
|
|
rife_logger.info(f"Loading TensorRT engine: {tensorrt_model_path}")
|
|
mm.soft_empty_cache()
|
|
engine = Engine(tensorrt_model_path)
|
|
engine.load()
|
|
|
|
return (engine,)
|
|
|
|
|
|
class MakeInterpolationStateList:
|
|
"""Build an InterpolationStateList to skip or keep specific frame pairs.
|
|
Ported from Fannovel16/ComfyUI-Frame-Interpolation.
|
|
"""
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"frame_indices": ("STRING", {"multiline": True, "default": "1,2,3",
|
|
"tooltip": "Comma-separated list of frame-pair indices (0-based). "
|
|
"Pair i is formed by frame i and frame i+1."}),
|
|
"is_skip_list": ("BOOLEAN", {"default": True,
|
|
"tooltip": "True = skip interpolation for the listed pairs. "
|
|
"False = interpolate ONLY the listed pairs."}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("INTERPOLATION_STATES",)
|
|
FUNCTION = "create_options"
|
|
CATEGORY = "⚡️ TensorRT/RIFE"
|
|
DESCRIPTION = "Build a skip/keep list controlling which frame pairs get interpolated by RIFE."
|
|
|
|
def create_options(self, frame_indices: str, is_skip_list: bool):
|
|
frame_indices_list = [int(item.strip()) for item in frame_indices.split(',') if item.strip() != '']
|
|
interpolation_state_list = InterpolationStateList(
|
|
frame_indices=frame_indices_list,
|
|
is_skip_list=is_skip_list,
|
|
)
|
|
return (interpolation_state_list,)
|
|
|
|
|
|
class AutoRifeTensorrt:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"frames": ("IMAGE", {"tooltip": "Input frames for video frame interpolation"}),
|
|
"rife_trt_model": ("RIFE_TRT_MODEL", {"tooltip": "Tensorrt model built and loaded"}),
|
|
"clear_cache_after_n_frames": ("INT", {"default": 100, "min": 1, "max": 1000, "tooltip": "Clear CUDA cache after processing this many frames"}),
|
|
"multiplier": ("INT", {"default": 2, "min": 1, "tooltip": "Frame interpolation multiplier (uniform). Ignored if multiplier_list is connected."}),
|
|
"keep_model_loaded": ("BOOLEAN", {"default": False, "tooltip": "Keep model loaded in memory after processing"}),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 16,
|
|
"tooltip": "Number of interpolation tasks per GPU call. "
|
|
"Higher values improve throughput but use more VRAM. "
|
|
"Must be <= the batch_size used when building the engine. "
|
|
"Set to 1 for the safest behaviour."}),
|
|
},
|
|
"optional": {
|
|
"multiplier_list": ("STRING", {"multiline": True, "default": "",
|
|
"tooltip": "Per-pair multiplier schedule, comma-separated (e.g. '2,2,4,4,2'). "
|
|
"When connected, overrides 'multiplier'. Missing trailing pairs default to 2."}),
|
|
"interpolation_states": ("INTERPOLATION_STATES", {"tooltip": "Optional skip/keep list from MakeInterpolationStateList"}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", )
|
|
FUNCTION = "vfi"
|
|
CATEGORY = "⚡️ TensorRT/RIFE"
|
|
OUTPUT_NODE=True
|
|
|
|
def vfi(
|
|
self,
|
|
frames,
|
|
rife_trt_model,
|
|
clear_cache_after_n_frames=100,
|
|
multiplier=2,
|
|
keep_model_loaded=False,
|
|
batch_size=1,
|
|
multiplier_list="",
|
|
interpolation_states=None,
|
|
):
|
|
B, H, W, C = frames.shape
|
|
shape_dict = {
|
|
"img0": {"shape": (1, 3, H, W)},
|
|
"img1": {"shape": (1, 3, H, W)},
|
|
"output": {"shape": (1, 3, H, W)},
|
|
}
|
|
|
|
cudaStream = cuda.Stream()
|
|
|
|
# Auto-detect CUDA graph usage based on CUDA version
|
|
use_cuda_graph = False
|
|
try:
|
|
import torch
|
|
cuda_version = torch.version.cuda
|
|
if cuda_version and cuda_version.startswith('13'):
|
|
use_cuda_graph = True
|
|
print(f"✅ CUDA {cuda_version} detected - enabling CUDA graph for better performance")
|
|
else:
|
|
use_cuda_graph = False
|
|
print(f"⚠️ CUDA {cuda_version} detected - disabling CUDA graph for stability")
|
|
except:
|
|
use_cuda_graph = False
|
|
print("⚠️ Could not detect CUDA version - disabling CUDA graph for safety")
|
|
|
|
# Use the provided model directly
|
|
engine = rife_trt_model
|
|
logger(f"Using loaded TensorRT engine")
|
|
|
|
# Activate engine and retrieve profile bounds
|
|
engine.activate()
|
|
bounds = engine.get_input_profile_bounds()
|
|
|
|
# Determine if tiling/padding is needed
|
|
needs_tiling = False
|
|
min_hw = (1, 1)
|
|
max_hw = (4096, 4096)
|
|
if bounds and 'img0' in bounds:
|
|
min_shape = bounds['img0'][0]
|
|
max_shape = bounds['img0'][2]
|
|
min_hw = (min_shape[2], min_shape[3])
|
|
max_hw = (max_shape[2], max_shape[3])
|
|
needs_tiling = H > max_hw[0] or W > max_hw[1] or H < min_hw[0] or W < min_hw[1]
|
|
if needs_tiling:
|
|
logger(f"Frame {H}x{W} outside profile range "
|
|
f"[{min_hw[0]}-{max_hw[0]}]x[{min_hw[1]}-{max_hw[1]}] — enabling tiling/padding")
|
|
|
|
frames = preprocess_frames(frames)
|
|
|
|
if needs_tiling:
|
|
# Disable CUDA graph when tiling (buffers are reallocated per tile)
|
|
use_cuda_graph = False
|
|
logger("Tiling mode: CUDA graph disabled")
|
|
|
|
def _raw_infer(f0, f1, t):
|
|
"""Per-tile inference: reallocate buffers for the tile shape."""
|
|
_, _, th, tw = f0.shape
|
|
tile_shape_dict = {
|
|
"img0": {"shape": (1, 3, th, tw)},
|
|
"img1": {"shape": (1, 3, th, tw)},
|
|
"output": {"shape": (1, 3, th, tw)},
|
|
}
|
|
engine.allocate_buffers(shape_dict=tile_shape_dict)
|
|
timestep_t = torch.tensor([t], dtype=torch.float32).to(get_torch_device())
|
|
output = engine.infer({"img0": f0, "img1": f1, "timestep": timestep_t}, cudaStream, use_cuda_graph)
|
|
return output['output']
|
|
|
|
def return_middle_frame(frame_0, frame_1, timestep):
|
|
return infer_tiled(frame_0, frame_1, timestep, _raw_infer, min_hw, max_hw)
|
|
|
|
# Batch inference is not compatible with tiling (each tile has different shape)
|
|
batch_infer_fn = None
|
|
effective_batch_size = 1
|
|
else:
|
|
# Fast path: single allocation, original behaviour
|
|
engine.allocate_buffers(shape_dict=shape_dict)
|
|
|
|
def return_middle_frame(frame_0, frame_1, timestep):
|
|
timestep_t = torch.tensor([timestep], dtype=torch.float32).to(get_torch_device())
|
|
output = engine.infer({"img0": frame_0, "img1": frame_1, "timestep": timestep_t}, cudaStream, use_cuda_graph)
|
|
result = output['output']
|
|
return result
|
|
|
|
# Batch inference: allocate buffers for the requested batch size
|
|
if batch_size > 1:
|
|
batch_shape_dict = {
|
|
"img0": {"shape": (batch_size, 3, H, W)},
|
|
"img1": {"shape": (batch_size, 3, H, W)},
|
|
"output": {"shape": (batch_size, 3, H, W)},
|
|
}
|
|
# Check if the engine profile supports this batch size
|
|
engine_max_batch = max_hw[0] # placeholder, real check below
|
|
if bounds and 'img0' in bounds:
|
|
engine_max_batch = bounds['img0'][2][0] # max batch dim
|
|
if engine_max_batch >= batch_size:
|
|
logger(f"Batch inference enabled: batch_size={batch_size}")
|
|
# Pre-allocate buffers for the full batch
|
|
engine.allocate_buffers(shape_dict=batch_shape_dict)
|
|
|
|
def batch_infer_fn(frame0_batch, frame1_batch, timestep_batch):
|
|
B_actual = frame0_batch.shape[0]
|
|
# If the actual batch is smaller than the allocated batch,
|
|
# we still pass the full buffer but only use the first B_actual rows.
|
|
# TensorRT requires the input shape to match what was set in allocate_buffers.
|
|
# So we pad to the full batch size if needed.
|
|
if B_actual < batch_size:
|
|
pad_shape = (batch_size - B_actual,) + frame0_batch.shape[1:]
|
|
pad0 = torch.zeros(pad_shape, dtype=frame0_batch.dtype, device=frame0_batch.device)
|
|
pad1 = torch.zeros(pad_shape, dtype=frame1_batch.dtype, device=frame1_batch.device)
|
|
frame0_batch = torch.cat([frame0_batch, pad0], dim=0)
|
|
frame1_batch = torch.cat([frame1_batch, pad1], dim=0)
|
|
timestep_batch = torch.cat([timestep_batch, torch.zeros(batch_size - B_actual, dtype=timestep_batch.dtype, device=timestep_batch.device)])
|
|
|
|
timestep_t = timestep_batch.to(get_torch_device())
|
|
output = engine.infer({"img0": frame0_batch, "img1": frame1_batch, "timestep": timestep_t}, cudaStream, use_cuda_graph)
|
|
return output['output'][:B_actual]
|
|
|
|
effective_batch_size = batch_size
|
|
else:
|
|
logger(f"⚠️ Engine max_batch={engine_max_batch} < requested batch_size={batch_size}. "
|
|
"Falling back to sequential inference. Rebuild engine with batch_size >= "
|
|
f"{batch_size} to enable batched inference.")
|
|
batch_infer_fn = None
|
|
effective_batch_size = 1
|
|
else:
|
|
batch_infer_fn = None
|
|
effective_batch_size = 1
|
|
|
|
# Resolve multiplier: per-pair schedule (list) overrides uniform int
|
|
effective_multiplier = multiplier
|
|
if multiplier_list and multiplier_list.strip():
|
|
try:
|
|
effective_multiplier = [int(x.strip()) for x in multiplier_list.split(',') if x.strip() != '']
|
|
logger(f"Using per-pair multiplier schedule: {effective_multiplier}")
|
|
except ValueError as e:
|
|
logger(f"Invalid multiplier_list '{multiplier_list}': {e}. Falling back to uniform multiplier={multiplier}")
|
|
effective_multiplier = multiplier
|
|
|
|
result = generate_frames_rife(
|
|
frames,
|
|
clear_cache_after_n_frames,
|
|
effective_multiplier,
|
|
return_middle_frame,
|
|
interpolation_states=interpolation_states,
|
|
batch_size=effective_batch_size,
|
|
batch_infer_function=batch_infer_fn,
|
|
)
|
|
out = postprocess_frames(result)
|
|
|
|
if not keep_model_loaded:
|
|
engine.reset()
|
|
|
|
return (out,)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"AutoRifeTensorrt": AutoRifeTensorrt,
|
|
"AutoLoadRifeTensorrtModel": AutoLoadRifeTensorrtModel,
|
|
"CustomResolutionConfig": CustomResolutionConfig,
|
|
"MakeInterpolationStateList": MakeInterpolationStateList,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"AutoRifeTensorrt": "Auto RIFE TensorRT",
|
|
"AutoLoadRifeTensorrtModel": "(Down)load RIFE TensorRT Model",
|
|
"CustomResolutionConfig": "RIFE Custom Resolution Config",
|
|
"MakeInterpolationStateList": "RIFE Interpolation State List",
|
|
}
|
|
|
|
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
|
|