Files
ModelTC-ComfyUI-Lightx2vWra…/nodes.py
T

427 lines
16 KiB
Python

"""Modular ComfyUI nodes for LightX2V without presets."""
import asyncio
import gc
import logging
import os
import tempfile
from typing import Any, Dict
import numpy as np
import torch
from comfy.utils import ProgressBar
from PIL import Image
from .bridge import ModularConfigManager, get_available_attn_ops, get_available_quant_ops
from .lightx2v.lightx2v.infer import init_runner
class LightX2VInferenceConfig:
"""Basic inference configuration node."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_cls": (["wan2.1", "hunyuan"], {"default": "wan2.1", "tooltip": "模型类型"}),
"model_path": ("STRING", {"default": "", "tooltip": "模型路径"}),
"task": (["t2v", "i2v"], {"default": "t2v", "tooltip": "任务类型:文本到视频或图像到视频"}),
"infer_steps": ("INT", {"default": 40, "min": 1, "max": 100, "tooltip": "推理步数"}),
"seed": ("INT", {"default": 42, "min": -1, "max": 2**32 - 1, "tooltip": "随机种子,-1为随机"}),
"cfg_scale": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 10.0, "step": 0.1, "tooltip": "CFG引导强度"}),
"sample_shift": ("INT", {"default": 5, "min": 0, "max": 10, "tooltip": "采样偏移"}),
"height": ("INT", {"default": 480, "min": 64, "max": 2048, "step": 8, "tooltip": "视频高度"}),
"width": ("INT", {"default": 832, "min": 64, "max": 2048, "step": 8, "tooltip": "视频宽度"}),
"video_length": ("INT", {"default": 81, "min": 16, "max": 120, "tooltip": "视频帧数"}),
"fps": ("INT", {"default": 16, "min": 8, "max": 30, "tooltip": "每秒帧数"}),
}
}
RETURN_TYPES = ("INFERENCE_CONFIG",)
RETURN_NAMES = ("inference_config",)
FUNCTION = "create_config"
CATEGORY = "LightX2V/Config"
def create_config(self, model_cls, model_path, task, infer_steps, seed, cfg_scale, sample_shift, height, width, video_length, fps):
"""Create basic inference configuration."""
config = {
"model_cls": model_cls,
"model_path": model_path,
"task": task,
"infer_steps": infer_steps,
"seed": seed if seed != -1 else np.random.randint(0, 2**32 - 1),
"cfg_scale": cfg_scale,
"sample_shift": sample_shift,
"height": height,
"width": width,
"video_length": video_length,
"fps": fps,
}
return (config,)
class LightX2VTeaCache:
"""TeaCache configuration node."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"enable": ("BOOLEAN", {"default": False, "tooltip": "启用TeaCache特征缓存"}),
"threshold": (
"FLOAT",
{"default": 0.26, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "缓存阈值,越低加速越多:0.1约2倍加速,0.2约3倍加速"},
),
"cache_key_steps_only": ("BOOLEAN", {"default": False, "tooltip": "只缓存关键步骤以平衡质量和速度"}),
}
}
RETURN_TYPES = ("TEACACHE_CONFIG",)
RETURN_NAMES = ("teacache_config",)
FUNCTION = "create_config"
CATEGORY = "LightX2V/Config"
def create_config(self, enable, threshold, cache_key_steps_only):
"""Create TeaCache configuration."""
config = {
"enable": enable,
"threshold": threshold,
"cache_key_steps_only": cache_key_steps_only,
}
return (config,)
class LightX2VQuantization:
"""Quantization configuration node."""
@classmethod
def INPUT_TYPES(cls):
# Get available quantization backends
available_ops = get_available_quant_ops()
quant_backends = []
for op_name, is_available in available_ops:
if is_available:
quant_backends.append(op_name)
# Always have at least one option
if not quant_backends:
quant_backends = ["none"]
return {
"required": {
"dit_precision": (["bf16", "int8", "fp8"], {"default": "bf16", "tooltip": "DIT模型量化精度"}),
"t5_precision": (["bf16", "int8", "fp8"], {"default": "bf16", "tooltip": "T5编码器量化精度"}),
"clip_precision": (["fp16", "int8", "fp8"], {"default": "fp16", "tooltip": "CLIP编码器量化精度"}),
"quant_backend": (quant_backends, {"default": quant_backends[0], "tooltip": "量化计算后端"}),
"sensitive_layers_precision": (["fp32", "bf16"], {"default": "fp32", "tooltip": "敏感层(归一化和嵌入层)精度"}),
}
}
RETURN_TYPES = ("QUANT_CONFIG",)
RETURN_NAMES = ("quantization_config",)
FUNCTION = "create_config"
CATEGORY = "LightX2V/Config"
def create_config(self, dit_precision, t5_precision, clip_precision, quant_backend, sensitive_layers_precision):
"""Create quantization configuration."""
config = {
"dit_precision": dit_precision,
"t5_precision": t5_precision,
"clip_precision": clip_precision,
"quant_backend": quant_backend,
"sensitive_layers_precision": sensitive_layers_precision,
}
return (config,)
class LightX2VMemoryOptimization:
"""Memory optimization configuration node."""
@classmethod
def INPUT_TYPES(cls):
# Get available attention types
available_attn = get_available_attn_ops()
attn_types = []
for op_name, is_available in available_attn:
if is_available:
attn_types.append(op_name)
# Always include fallback
if "torch_sdpa" not in attn_types:
attn_types.append("torch_sdpa")
return {
"required": {
"optimization_level": (
["none", "low", "medium", "high", "extreme"],
{"default": "none", "tooltip": "内存优化级别,越高越省内存但可能影响速度"},
),
"attention_type": (attn_types, {"default": attn_types[0], "tooltip": "注意力机制类型"}),
},
"optional": {
# GPU optimization
"enable_rotary_chunk": ("BOOLEAN", {"default": False, "tooltip": "启用旋转编码分块"}),
"rotary_chunk_size": ("INT", {"default": 100, "min": 100, "max": 10000, "step": 100}),
"clean_cuda_cache": ("BOOLEAN", {"default": False, "tooltip": "及时清理CUDA缓存"}),
# CPU offloading
"enable_cpu_offload": ("BOOLEAN", {"default": False, "tooltip": "启用CPU卸载"}),
"offload_granularity": (["block", "phase"], {"default": "phase", "tooltip": "卸载粒度"}),
"offload_ratio": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}),
# Module management
"lazy_load": ("BOOLEAN", {"default": False, "tooltip": "延迟加载模型"}),
"unload_after_inference": ("BOOLEAN", {"default": False, "tooltip": "推理后卸载模块"}),
},
}
RETURN_TYPES = ("MEMORY_CONFIG",)
RETURN_NAMES = ("memory_config",)
FUNCTION = "create_config"
CATEGORY = "LightX2V/Config"
def create_config(
self,
optimization_level,
attention_type,
enable_rotary_chunk=False,
rotary_chunk_size=100,
clean_cuda_cache=False,
enable_cpu_offload=False,
offload_granularity="phase",
offload_ratio=1.0,
lazy_load=False,
unload_after_inference=False,
):
"""Create memory optimization configuration."""
config = {
"optimization_level": optimization_level,
"attention_type": attention_type,
"enable_rotary_chunk": enable_rotary_chunk,
"rotary_chunk_size": rotary_chunk_size,
"clean_cuda_cache": clean_cuda_cache,
"enable_cpu_offload": enable_cpu_offload,
"offload_granularity": offload_granularity,
"offload_ratio": offload_ratio,
"lazy_load": lazy_load,
"unload_after_inference": unload_after_inference,
}
return (config,)
class LightX2VLightweightVAE:
"""Lightweight VAE configuration node."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"use_tiny_vae": ("BOOLEAN", {"default": False, "tooltip": "使用轻量级VAE加速解码"}),
"use_tiling_vae": ("BOOLEAN", {"default": False, "tooltip": "使用VAE分块推理减少显存"}),
}
}
RETURN_TYPES = ("VAE_CONFIG",)
RETURN_NAMES = ("vae_config",)
FUNCTION = "create_config"
CATEGORY = "LightX2V/Config"
def create_config(self, use_tiny_vae, use_tiling_vae):
"""Create VAE configuration."""
config = {
"use_tiny_vae": use_tiny_vae,
"use_tiling_vae": use_tiling_vae,
}
return (config,)
class LightX2VModularInference:
"""Modular inference node that combines all configurations."""
def __init__(self):
self.config_manager = ModularConfigManager()
self._current_runner = None
self._current_config_hash = None
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"inference_config": ("INFERENCE_CONFIG", {"tooltip": "基础推理配置"}),
"prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "生成提示词"}),
"negative_prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "负面提示词"}),
},
"optional": {
"image": ("IMAGE", {"tooltip": "i2v任务的输入图像"}),
"teacache_config": ("TEACACHE_CONFIG", {"tooltip": "TeaCache配置"}),
"quantization_config": ("QUANT_CONFIG", {"tooltip": "量化配置"}),
"memory_config": ("MEMORY_CONFIG", {"tooltip": "内存优化配置"}),
"vae_config": ("VAE_CONFIG", {"tooltip": "VAE配置"}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "generate"
CATEGORY = "LightX2V/Inference"
def _get_config_hash(self, configs: Dict[str, Any]) -> str:
"""Generate a hash for configuration to detect changes."""
import hashlib
import json
# Only hash model-related configs
relevant_configs = {
"model_cls": configs.get("inference", {}).get("model_cls"),
"model_path": configs.get("inference", {}).get("model_path"),
"quantization": configs.get("quantization"),
"memory_lazy_load": configs.get("memory", {}).get("lazy_load"),
}
config_str = json.dumps(relevant_configs, sort_keys=True)
return hashlib.md5(config_str.encode()).hexdigest()
def generate(
self,
inference_config,
prompt,
negative_prompt,
image=None,
teacache_config=None,
quantization_config=None,
memory_config=None,
vae_config=None,
**kwargs,
):
"""Generate video using modular configuration."""
# Set environment variables
os.environ["TOKENIZERS_PARALLELISM"] = "false"
if "DTYPE" not in os.environ:
os.environ["DTYPE"] = "BF16"
if "ENABLE_GRAPH_MODE" not in os.environ:
os.environ["ENABLE_GRAPH_MODE"] = "false"
if "ENABLE_PROFILING_DEBUG" not in os.environ:
os.environ["ENABLE_PROFILING_DEBUG"] = "true"
# Collect all configurations
configs = {
"inference": inference_config,
}
if teacache_config:
configs["teacache"] = teacache_config
if quantization_config:
configs["quantization"] = quantization_config
if memory_config:
configs["memory"] = memory_config
if vae_config:
configs["vae"] = vae_config
# Build final configuration
config = self.config_manager.build_final_config(configs)
# Add prompt and negative prompt
config.prompt = prompt
config.negative_prompt = negative_prompt
# Check if task requires image
if config.task == "i2v" and image is None:
raise ValueError("i2v task requires input image")
temp_files = []
try:
# Handle image input for i2v
if config.task == "i2v" and image is not None:
# Convert ComfyUI image to PIL
image_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
pil_image = Image.fromarray(image_np)
# Save to temporary file
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
pil_image.save(tmp.name)
config.image_path = tmp.name
temp_files.append(tmp.name)
# Check if we need to reinitialize runner
config_hash = self._get_config_hash(configs)
needs_reinit = (
self._current_runner is None or self._current_config_hash != config_hash or configs.get("memory", {}).get("lazy_load", False)
)
if needs_reinit:
# Clear old runner
if self._current_runner is not None:
del self._current_runner
torch.cuda.empty_cache()
gc.collect()
# Initialize new runner
self._current_runner = init_runner(config)
self._current_config_hash = config_hash
else:
# Update config for existing runner
self._current_runner.config = config
# Set up progress callback
total_steps = config.get("infer_steps", 40)
progress = ProgressBar(total_steps)
def update_progress(current_step, total):
progress.update_absolute(current_step)
self._current_runner.set_progress_callback(update_progress)
# Run inference
images = asyncio.run(self._current_runner.run_pipeline(save_video=False))
# Clean up if requested
if configs.get("memory", {}).get("unload_after_inference", False):
del self._current_runner
self._current_runner = None
self._current_config_hash = None
torch.cuda.empty_cache()
gc.collect()
# Convert output to ComfyUI format
images = (images + 1) / 2
images = images.squeeze(0).permute(1, 2, 3, 0).cpu()
images = torch.clamp(images, 0, 1)
return (images,)
except Exception as e:
logging.error(f"Error during inference: {e}")
raise
finally:
# Clean up temporary files
for temp_file in temp_files:
if os.path.exists(temp_file):
try:
os.unlink(temp_file)
except Exception:
pass
# Node mappings
NODE_CLASS_MAPPINGS = {
"LightX2VInferenceConfig": LightX2VInferenceConfig,
"LightX2VTeaCache": LightX2VTeaCache,
"LightX2VQuantization": LightX2VQuantization,
"LightX2VMemoryOptimization": LightX2VMemoryOptimization,
"LightX2VLightweightVAE": LightX2VLightweightVAE,
"LightX2VModularInference": LightX2VModularInference,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LightX2VInferenceConfig": "LightX2V 推理配置",
"LightX2VTeaCache": "LightX2V TeaCache缓存",
"LightX2VQuantization": "LightX2V 低精度量化",
"LightX2VMemoryOptimization": "LightX2V 内存优化",
"LightX2VLightweightVAE": "LightX2V 轻量VAE",
"LightX2VModularInference": "LightX2V 模块化推理",
}