1073 lines
41 KiB
Python
1073 lines
41 KiB
Python
import os
|
|
import gc
|
|
import inspect
|
|
import json
|
|
import torch
|
|
from transformers import (
|
|
Qwen2_5_VLForConditionalGeneration,
|
|
Qwen3VLForConditionalGeneration,
|
|
AutoModelForCausalLM,
|
|
AutoTokenizer,
|
|
AutoProcessor,
|
|
BitsAndBytesConfig,
|
|
)
|
|
from qwen_vl_utils import process_vision_info
|
|
from PIL import Image
|
|
import numpy as np
|
|
import folder_paths
|
|
import subprocess
|
|
import uuid
|
|
import time # Import at module level for timing
|
|
|
|
try:
|
|
import comfy.model_management as comfy_mm
|
|
except ImportError: # ComfyUI runtime not available during development/tests
|
|
comfy_mm = None
|
|
def _check_quantization_support():
|
|
"""Check if quantization kernels are available and working."""
|
|
if not torch.cuda.is_available():
|
|
return False, "CUDA not available"
|
|
|
|
try:
|
|
import bitsandbytes as bnb
|
|
# Test if CUDA kernels are available
|
|
test_tensor = torch.randn(10, 10).cuda()
|
|
try:
|
|
# Try to use quantization kernel
|
|
from bitsandbytes.functional import quantize_4bit
|
|
quantize_4bit(test_tensor)
|
|
return True, "Quantization kernels available"
|
|
except Exception as e:
|
|
return False, f"Quantization kernels not working: {e}"
|
|
except ImportError:
|
|
return False, "bitsandbytes not installed"
|
|
|
|
|
|
def _get_gpu_info():
|
|
"""Get GPU compute capability and check Flash Attention 2 optimization status.
|
|
|
|
Returns info about whether FA2 is likely OPTIMAL for this GPU (hint, not blocker).
|
|
FA2 may still work on newer GPUs even if not fully optimized yet.
|
|
"""
|
|
if not torch.cuda.is_available():
|
|
return None, None, True # Assume compatible if we can't check
|
|
|
|
try:
|
|
device = torch.device("cuda:0")
|
|
props = torch.cuda.get_device_properties(device)
|
|
major, minor = props.major, props.minor
|
|
compute_cap = f"{major}.{minor}"
|
|
gpu_name = props.name
|
|
|
|
# FA2 is optimized for SM 80-90 (Ampere, Ada, Hopper)
|
|
# Newer architectures (SM 120+ Blackwell) may not have optimized kernels yet
|
|
# But this could change with future FA2 updates
|
|
sm_version = major * 10 + minor
|
|
fa2_optimized = 80 <= sm_version <= 90
|
|
|
|
return gpu_name, compute_cap, fa2_optimized
|
|
except Exception:
|
|
return None, None, True # Assume compatible if we can't check
|
|
|
|
|
|
# Get GPU info at module load time
|
|
_GPU_NAME, _GPU_COMPUTE_CAP, _FA2_OPTIMIZED = _get_gpu_info()
|
|
if _GPU_NAME:
|
|
print(f"[SCG_LocalVLM] GPU: {_GPU_NAME} (SM {_GPU_COMPUTE_CAP})")
|
|
|
|
|
|
|
|
# --- Custom Models Loading ---
|
|
CUSTOM_MODELS_FILE = os.path.join(os.path.dirname(__file__), "custom_models.json")
|
|
CUSTOM_VL_MODELS = {} # name -> {repo_id, model_class, ...}
|
|
CUSTOM_TEXT_MODELS = {} # name -> {repo_id, ...}
|
|
|
|
|
|
def _load_custom_models():
|
|
"""Load custom models from custom_models.json if it exists."""
|
|
global CUSTOM_VL_MODELS, CUSTOM_TEXT_MODELS
|
|
if not os.path.exists(CUSTOM_MODELS_FILE):
|
|
return
|
|
|
|
try:
|
|
with open(CUSTOM_MODELS_FILE, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
hf_models = data.get("hf_models", {})
|
|
for model_name, model_info in hf_models.items():
|
|
repo_id = model_info.get("repo_id", "")
|
|
if not repo_id:
|
|
continue
|
|
|
|
# Determine if VL (vision-language) or text-only based on name
|
|
is_vl = "-VL-" in model_name.upper() or "-VL-" in repo_id.upper()
|
|
|
|
# Determine model class from name pattern
|
|
model_class = model_info.get("model_class", None)
|
|
if model_class is None:
|
|
if "Qwen3" in model_name or "Qwen3" in repo_id:
|
|
model_class = "Qwen3"
|
|
else:
|
|
model_class = "Qwen2.5"
|
|
|
|
model_entry = {
|
|
"repo_id": repo_id,
|
|
"model_class": model_class,
|
|
"default": model_info.get("default", False),
|
|
"quantized": model_info.get("quantized", False),
|
|
"vram_requirement": model_info.get("vram_requirement", {}),
|
|
}
|
|
|
|
if is_vl:
|
|
CUSTOM_VL_MODELS[model_name] = model_entry
|
|
else:
|
|
CUSTOM_TEXT_MODELS[model_name] = model_entry
|
|
|
|
if CUSTOM_VL_MODELS or CUSTOM_TEXT_MODELS:
|
|
print(f"[SCG_LocalVLM] Loaded {len(CUSTOM_VL_MODELS)} custom VL models and {len(CUSTOM_TEXT_MODELS)} custom text models from custom_models.json")
|
|
except Exception as e:
|
|
print(f"[SCG_LocalVLM] Warning: Failed to load custom_models.json: {e}")
|
|
|
|
|
|
# Load custom models at startup
|
|
_load_custom_models()
|
|
|
|
|
|
def _get_model_repo_id(model_name, is_vl=True):
|
|
"""Get the HuggingFace repo ID for a model name."""
|
|
# Check custom models first
|
|
if is_vl and model_name in CUSTOM_VL_MODELS:
|
|
return CUSTOM_VL_MODELS[model_name]["repo_id"]
|
|
if not is_vl and model_name in CUSTOM_TEXT_MODELS:
|
|
return CUSTOM_TEXT_MODELS[model_name]["repo_id"]
|
|
|
|
# Built-in models
|
|
if model_name.startswith("Qwen"):
|
|
return f"qwen/{model_name}"
|
|
elif model_name == "SkyCaptioner-V1":
|
|
return f"Skywork/{model_name}"
|
|
else:
|
|
return f"qwen/{model_name}"
|
|
|
|
|
|
def _get_model_class(model_name, is_vl=True):
|
|
"""Determine which model class to use for loading."""
|
|
# Check custom models first
|
|
if is_vl and model_name in CUSTOM_VL_MODELS:
|
|
return CUSTOM_VL_MODELS[model_name].get("model_class", "Qwen3")
|
|
if not is_vl and model_name in CUSTOM_TEXT_MODELS:
|
|
return CUSTOM_TEXT_MODELS[model_name].get("model_class", "Qwen3")
|
|
|
|
# Built-in: detect from name
|
|
if model_name.startswith("Qwen3"):
|
|
return "Qwen3"
|
|
return "Qwen2.5"
|
|
|
|
|
|
# Build model lists including custom models
|
|
BUILTIN_VL_MODELS = [
|
|
"Qwen2.5-VL-3B-Instruct",
|
|
"Qwen2.5-VL-7B-Instruct",
|
|
"Qwen3-VL-2B-Thinking",
|
|
"Qwen3-VL-2B-Instruct",
|
|
"Qwen3-VL-4B-Thinking",
|
|
"Qwen3-VL-4B-Instruct",
|
|
"Qwen3-VL-8B-Thinking",
|
|
"Qwen3-VL-8B-Instruct",
|
|
"Qwen3-VL-32B-Thinking",
|
|
"Qwen3-VL-32B-Instruct",
|
|
"SkyCaptioner-V1",
|
|
]
|
|
|
|
BUILTIN_TEXT_MODELS = [
|
|
"Qwen2.5-3B-Instruct",
|
|
"Qwen2.5-7B-Instruct",
|
|
"Qwen2.5-14B-Instruct",
|
|
"Qwen2.5-32B-Instruct",
|
|
"Qwen3-4B-Thinking-2507",
|
|
"Qwen3-4B-Instruct-2507",
|
|
]
|
|
|
|
|
|
def _get_vl_model_list():
|
|
"""Get the full list of VL models including custom ones."""
|
|
return BUILTIN_VL_MODELS + list(CUSTOM_VL_MODELS.keys())
|
|
|
|
|
|
def _get_text_model_list():
|
|
"""Get the full list of text models including custom ones."""
|
|
return BUILTIN_TEXT_MODELS + list(CUSTOM_TEXT_MODELS.keys())
|
|
|
|
|
|
def _maybe_move_to_cpu(module):
|
|
if module is None:
|
|
return
|
|
try:
|
|
module.to("cpu")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _clear_cuda_memory():
|
|
gc.collect()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
torch.cuda.ipc_collect()
|
|
if comfy_mm is not None:
|
|
try:
|
|
soft_empty = getattr(comfy_mm, "soft_empty_cache", None)
|
|
if callable(soft_empty):
|
|
params = inspect.signature(soft_empty).parameters
|
|
if "force" in params:
|
|
soft_empty(force=True)
|
|
else:
|
|
soft_empty()
|
|
return
|
|
cleanup_models = getattr(comfy_mm, "cleanup_models", None)
|
|
if callable(cleanup_models):
|
|
cleanup_models()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def tensor_to_pil(image_tensor, batch_index=0) -> Image:
|
|
"""Convert ComfyUI tensor to PIL Image with minimal CPU transfer."""
|
|
# Extract single image from batch
|
|
image_tensor = image_tensor[batch_index]
|
|
|
|
# Perform scaling and clamping on GPU before CPU transfer
|
|
img_data = (image_tensor * 255.0).clamp(0, 255).byte()
|
|
|
|
# Single CPU transfer at the very end
|
|
img_np = img_data.cpu().numpy()
|
|
|
|
# Convert to PIL
|
|
img = Image.fromarray(img_np)
|
|
return img
|
|
|
|
|
|
class QwenVL:
|
|
"""
|
|
Qwen Vision-Language Model Node for ComfyUI
|
|
|
|
Supports Qwen2.5-VL and Qwen3-VL models with:
|
|
- Multi-image input (up to 4 images)
|
|
- Video input via FFmpeg processing
|
|
- Multiple quantization options (none, 4bit, 8bit)
|
|
- Advanced generation parameters
|
|
- Flash Attention 2 optimization
|
|
|
|
ComfyUI v2/v3 compatible.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.model_checkpoint = None
|
|
self.processor = None
|
|
self.model = None
|
|
self.device = (
|
|
torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
|
|
)
|
|
self.bf16_support = (
|
|
torch.cuda.is_available()
|
|
and torch.cuda.get_device_capability(self.device)[0] >= 8
|
|
)
|
|
|
|
def _unload_resources(self):
|
|
_maybe_move_to_cpu(self.model)
|
|
self.model = None
|
|
self.processor = None
|
|
_clear_cuda_memory()
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"system": (
|
|
"STRING",
|
|
{
|
|
"default": "You are a helpful assistant.",
|
|
"multiline": True,
|
|
},
|
|
),
|
|
"text": ("STRING", {"default": "", "multiline": True}),
|
|
"model": (
|
|
_get_vl_model_list(),
|
|
{"default": "Qwen3-VL-4B-Instruct"},
|
|
),
|
|
"quantization": (
|
|
["none", "4bit", "8bit"],
|
|
{"default": "none"},
|
|
),
|
|
"attention_mode": (
|
|
["auto", "flash_attention_2", "sdpa", "eager"],
|
|
{"default": "auto"},
|
|
),
|
|
"keep_model_loaded": ("BOOLEAN", {"default": False}),
|
|
"bypass": ("BOOLEAN", {"default": False}),
|
|
"do_sample": ("BOOLEAN", {"default": True}),
|
|
"temperature": (
|
|
"FLOAT",
|
|
{"default": 0.7, "min": 0, "max": 2, "step": 0.05},
|
|
),
|
|
"top_p": (
|
|
"FLOAT",
|
|
{"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.05},
|
|
),
|
|
"top_k": (
|
|
"INT",
|
|
{"default": 50, "min": 0, "max": 200, "step": 1},
|
|
),
|
|
"min_p": (
|
|
"FLOAT",
|
|
{"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05},
|
|
),
|
|
"repetition_penalty": (
|
|
"FLOAT",
|
|
{"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.05},
|
|
),
|
|
"max_new_tokens": (
|
|
"INT",
|
|
{"default": 512, "min": 128, "max": 8000, "step": 1},
|
|
),
|
|
"repetition_penalty": (
|
|
"FLOAT",
|
|
{"default": 1.1, "min": 1.0, "max": 2.0, "step": 0.05},
|
|
),
|
|
"seed": ("INT", {"default": -1}),
|
|
},
|
|
"optional": {
|
|
"image1": ("IMAGE",),
|
|
"image2": ("IMAGE",),
|
|
"image3": ("IMAGE",),
|
|
"image4": ("IMAGE",),
|
|
"video_path": ("STRING", {"default": ""}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("text",)
|
|
FUNCTION = "inference"
|
|
CATEGORY = "Comfyui_QwenVL"
|
|
|
|
def inference(
|
|
self,
|
|
system,
|
|
text,
|
|
model,
|
|
quantization,
|
|
attention_mode,
|
|
keep_model_loaded,
|
|
bypass,
|
|
do_sample,
|
|
temperature,
|
|
top_p,
|
|
top_k,
|
|
min_p,
|
|
repetition_penalty,
|
|
max_new_tokens,
|
|
repetition_penalty,
|
|
seed,
|
|
image1=None,
|
|
image2=None,
|
|
image3=None,
|
|
image4=None,
|
|
video_path=None,
|
|
):
|
|
# Bypass mode: pass text directly to output without model inference
|
|
if bypass:
|
|
return (text,)
|
|
|
|
if seed != -1:
|
|
torch.manual_seed(seed)
|
|
|
|
model_id = _get_model_repo_id(model, is_vl=True)
|
|
# put downloaded model to model/LLM dir
|
|
self.model_checkpoint = os.path.join(
|
|
folder_paths.models_dir, "LLM", os.path.basename(model_id)
|
|
)
|
|
|
|
if not os.path.exists(self.model_checkpoint):
|
|
from huggingface_hub import snapshot_download
|
|
|
|
snapshot_download(
|
|
repo_id=model_id,
|
|
local_dir=self.model_checkpoint,
|
|
)
|
|
|
|
if self.processor is None:
|
|
# Define min_pixels and max_pixels:
|
|
# Images will be resized to maintain their aspect ratio
|
|
# within the range of min_pixels and max_pixels.
|
|
min_pixels = 256*28*28
|
|
max_pixels = 1024*28*28
|
|
|
|
self.processor = AutoProcessor.from_pretrained(
|
|
self.model_checkpoint,
|
|
min_pixels=min_pixels,
|
|
max_pixels=max_pixels,
|
|
)
|
|
|
|
if self.model is None:
|
|
load_start = time.time()
|
|
|
|
# Check quantization support if using quantization
|
|
if quantization != "none":
|
|
quant_ok, quant_msg = _check_quantization_support()
|
|
if not quant_ok:
|
|
print(f"[SCG_LocalVLM] WARNING: {quant_msg}")
|
|
print("[SCG_LocalVLM] Falling back to unquantized loading")
|
|
quantization = "none"
|
|
else:
|
|
print(f"[SCG_LocalVLM] {quant_msg}")
|
|
|
|
# Determine compute dtype
|
|
compute_dtype = torch.bfloat16 if self.bf16_support else torch.float16
|
|
print(f"[SCG_LocalVLM] Using compute dtype: {compute_dtype}")
|
|
|
|
# Build load_kwargs - SIMPLIFIED for maximum performance
|
|
# For non-quantized: use native PyTorch loading (no device_map overhead)
|
|
# For quantized: need device_map for bitsandbytes
|
|
load_kwargs = {
|
|
"torch_dtype": compute_dtype,
|
|
}
|
|
|
|
if quantization == "4bit":
|
|
quantization_config = BitsAndBytesConfig(
|
|
load_in_4bit=True,
|
|
bnb_4bit_compute_dtype=compute_dtype,
|
|
bnb_4bit_quant_type="nf4",
|
|
bnb_4bit_use_double_quant=True,
|
|
)
|
|
load_kwargs["quantization_config"] = quantization_config
|
|
# Use explicit GPU placement to avoid Accelerate memory management overhead
|
|
load_kwargs["device_map"] = {"": 0} if torch.cuda.device_count() == 1 else "auto"
|
|
print(f"[SCG_LocalVLM] Loading with 4-bit quantization")
|
|
|
|
elif quantization == "8bit":
|
|
quantization_config = BitsAndBytesConfig(
|
|
load_in_8bit=True,
|
|
bnb_8bit_compute_dtype=compute_dtype,
|
|
)
|
|
load_kwargs["quantization_config"] = quantization_config
|
|
load_kwargs["device_map"] = {"": 0} if torch.cuda.device_count() == 1 else "auto"
|
|
print(f"[SCG_LocalVLM] Loading with 8-bit quantization")
|
|
|
|
else:
|
|
# Non-quantized: load directly to GPU for maximum speed
|
|
load_kwargs["device_map"] = {"": "cuda:0"}
|
|
print("[SCG_LocalVLM] Loading without quantization")
|
|
|
|
# Handle attention mode
|
|
if attention_mode == "flash_attention_2":
|
|
try:
|
|
import flash_attn
|
|
load_kwargs["attn_implementation"] = "flash_attention_2"
|
|
attention_used = "flash_attention_2"
|
|
if not _FA2_OPTIMIZED:
|
|
print(f"[SCG_LocalVLM] Using Flash Attention 2 (may not be optimal for {_GPU_NAME or 'this GPU'})")
|
|
else:
|
|
print("[SCG_LocalVLM] Using Flash Attention 2")
|
|
except ImportError:
|
|
print("[SCG_LocalVLM] flash-attn not installed, using SDPA instead")
|
|
load_kwargs["attn_implementation"] = "sdpa"
|
|
attention_used = "sdpa"
|
|
elif attention_mode == "sdpa":
|
|
load_kwargs["attn_implementation"] = "sdpa"
|
|
attention_used = "sdpa"
|
|
print("[SCG_LocalVLM] Using SDPA attention")
|
|
elif attention_mode == "eager":
|
|
load_kwargs["attn_implementation"] = "eager"
|
|
attention_used = "eager"
|
|
print("[SCG_LocalVLM] Using eager attention")
|
|
else:
|
|
# auto - use sdpa as default (best compatibility + good performance)
|
|
load_kwargs["attn_implementation"] = "sdpa"
|
|
attention_used = "sdpa"
|
|
print("[SCG_LocalVLM] Using SDPA attention (auto)")
|
|
|
|
# Load the model
|
|
model_class = _get_model_class(model, is_vl=True)
|
|
print(f"[SCG_LocalVLM] Loading {model_class} model from {self.model_checkpoint}")
|
|
|
|
try:
|
|
if model_class == "Qwen3":
|
|
self.model = Qwen3VLForConditionalGeneration.from_pretrained(
|
|
self.model_checkpoint,
|
|
**load_kwargs,
|
|
)
|
|
else:
|
|
self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
|
self.model_checkpoint,
|
|
**load_kwargs,
|
|
)
|
|
|
|
load_time = time.time() - load_start
|
|
|
|
# Set model to eval mode
|
|
self.model.eval()
|
|
|
|
# Print comprehensive model info
|
|
print(f"[SCG_LocalVLM] Model loaded successfully in {load_time:.2f}s")
|
|
print(f"[SCG_LocalVLM] Device: {self.model.device}")
|
|
print(f"[SCG_LocalVLM] Dtype: {self.model.dtype}")
|
|
print(f"[SCG_LocalVLM] Attention: {attention_used}")
|
|
|
|
# Verify actual attention implementation
|
|
if hasattr(self.model.config, '_attn_implementation'):
|
|
actual_attn = self.model.config._attn_implementation
|
|
print(f"[SCG_LocalVLM] Attention (verified): {actual_attn}")
|
|
|
|
# Show quantization info
|
|
if quantization != "none":
|
|
print(f"[SCG_LocalVLM] Quantization: {quantization}")
|
|
if hasattr(self.model, 'hf_device_map'):
|
|
print(f"[SCG_LocalVLM] Device map: {self.model.hf_device_map}")
|
|
|
|
# Check if model is actually quantized
|
|
is_quantized = False
|
|
for name, param in self.model.named_parameters():
|
|
if hasattr(param, 'quant_state'):
|
|
is_quantized = True
|
|
break
|
|
|
|
if is_quantized:
|
|
print(f"[SCG_LocalVLM] Quantization: VERIFIED (layers are quantized)")
|
|
else:
|
|
print(f"[SCG_LocalVLM] WARNING: Quantization requested but layers not quantized!")
|
|
|
|
except Exception as e:
|
|
print(f"[SCG_LocalVLM] Error loading model: {str(e)}")
|
|
import traceback
|
|
print(traceback.format_exc())
|
|
|
|
# Try fallback without attention implementation
|
|
if "attn_implementation" in load_kwargs:
|
|
print(f"[SCG_LocalVLM] Retrying without {attention_mode} attention...")
|
|
load_kwargs.pop("attn_implementation", None)
|
|
|
|
if model_class == "Qwen3":
|
|
self.model = Qwen3VLForConditionalGeneration.from_pretrained(
|
|
self.model_checkpoint,
|
|
**load_kwargs,
|
|
)
|
|
else:
|
|
self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
|
self.model_checkpoint,
|
|
**load_kwargs,
|
|
)
|
|
print("[SCG_LocalVLM] Model loaded with fallback attention")
|
|
else:
|
|
raise
|
|
|
|
processed_video_path = None
|
|
result = None
|
|
|
|
with torch.inference_mode():
|
|
# Build user content list - images first (in order), then text
|
|
user_content = []
|
|
|
|
# Process images in order: image1, image2, image3, image4
|
|
images = [image1, image2, image3, image4]
|
|
image_count = 0
|
|
for idx, img in enumerate(images, start=1):
|
|
if img is not None:
|
|
print(f"Processing image{idx}")
|
|
pil_image = tensor_to_pil(img)
|
|
user_content.append({
|
|
"type": "image",
|
|
"image": pil_image,
|
|
})
|
|
image_count += 1
|
|
|
|
if image_count > 0:
|
|
print(f"Total images added: {image_count}")
|
|
|
|
try:
|
|
# Handle video if provided (takes precedence positioning but images still included)
|
|
if video_path:
|
|
print("deal video_path", video_path)
|
|
unique_id = uuid.uuid4().hex # 生成唯一标识符
|
|
processed_video_path = f"/tmp/processed_video_{unique_id}.mp4" # 临时文件路径
|
|
ffmpeg_command = [
|
|
"ffmpeg",
|
|
"-i", video_path,
|
|
"-vf", "fps=1,scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease",
|
|
"-c:v", "libx264",
|
|
"-preset", "fast",
|
|
"-crf", "18",
|
|
processed_video_path
|
|
]
|
|
subprocess.run(ffmpeg_command, check=True)
|
|
|
|
user_content.append({
|
|
"type": "video",
|
|
"video": processed_video_path,
|
|
})
|
|
|
|
# Add text prompt at the end
|
|
user_content.append({"type": "text", "text": text})
|
|
|
|
messages = [
|
|
{"role": "system", "content": system},
|
|
{"role": "user", "content": user_content},
|
|
]
|
|
|
|
text = self.processor.apply_chat_template(
|
|
messages, tokenize=False, add_generation_prompt=True
|
|
)
|
|
print("deal messages", messages)
|
|
image_inputs, video_inputs = process_vision_info(messages)
|
|
|
|
# Process inputs on CPU first
|
|
inputs = self.processor(
|
|
text=[text],
|
|
images=image_inputs,
|
|
videos=video_inputs,
|
|
padding=True,
|
|
return_tensors="pt",
|
|
).to(self.model.device)
|
|
|
|
# Build generation kwargs with optimal settings
|
|
generation_kwargs = {
|
|
"max_new_tokens": max_new_tokens,
|
|
"do_sample": do_sample,
|
|
"use_cache": True, # CRITICAL: Enable KV cache for fast generation
|
|
"num_beams": 1, # Greedy/sampling is faster than beam search
|
|
}
|
|
|
|
# Add pad_token_id to prevent warnings
|
|
if hasattr(self.processor, 'tokenizer'):
|
|
if hasattr(self.processor.tokenizer, 'pad_token_id') and self.processor.tokenizer.pad_token_id is not None:
|
|
generation_kwargs["pad_token_id"] = self.processor.tokenizer.pad_token_id
|
|
elif hasattr(self.processor.tokenizer, 'eos_token_id') and self.processor.tokenizer.eos_token_id is not None:
|
|
generation_kwargs["pad_token_id"] = self.processor.tokenizer.eos_token_id
|
|
|
|
# Add sampling parameters only if do_sample is True
|
|
if do_sample:
|
|
if temperature > 0:
|
|
generation_kwargs["temperature"] = temperature
|
|
if top_p < 1.0:
|
|
generation_kwargs["top_p"] = top_p
|
|
if top_k > 0:
|
|
generation_kwargs["top_k"] = top_k
|
|
if min_p > 0:
|
|
generation_kwargs["min_p"] = min_p
|
|
|
|
# Always apply repetition penalty if not default
|
|
if repetition_penalty != 1.0:
|
|
generation_kwargs["repetition_penalty"] = repetition_penalty
|
|
|
|
gen_start = time.time()
|
|
generated_ids = self.model.generate(**inputs, **generation_kwargs)
|
|
gen_time = time.time() - gen_start
|
|
|
|
generated_ids_trimmed = [
|
|
out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
|
|
]
|
|
|
|
# Calculate tokens/sec
|
|
if len(generated_ids_trimmed) > 0:
|
|
tokens_generated = len(generated_ids_trimmed[0])
|
|
tokens_per_sec = tokens_generated / gen_time if gen_time > 0 else 0
|
|
print(f"[SCG_LocalVLM] Generation Performance:")
|
|
print(f"[SCG_LocalVLM] Tokens: {tokens_generated}")
|
|
print(f"[SCG_LocalVLM] Time: {gen_time:.2f}s")
|
|
print(f"[SCG_LocalVLM] Speed: {tokens_per_sec:.2f} tokens/sec")
|
|
|
|
# Add performance analysis
|
|
if quantization == "4bit" and tokens_per_sec < 10:
|
|
print(f"[SCG_LocalVLM] WARNING: 4bit performance unusually slow!")
|
|
print(f"[SCG_LocalVLM] Expected: 40-60 tok/s, Got: {tokens_per_sec:.2f} tok/s")
|
|
print(f"[SCG_LocalVLM] Possible issues:")
|
|
print(f"[SCG_LocalVLM] - Quantization kernels not properly compiled")
|
|
print(f"[SCG_LocalVLM] - CUDA/PyTorch version mismatch")
|
|
print(f"[SCG_LocalVLM] - Try: pip install bitsandbytes --upgrade")
|
|
|
|
result = self.processor.batch_decode(
|
|
generated_ids_trimmed,
|
|
skip_special_tokens=True,
|
|
clean_up_tokenization_spaces=False,
|
|
)
|
|
except torch.cuda.OutOfMemoryError as e:
|
|
_clear_cuda_memory()
|
|
return (
|
|
f"[SCG_LocalVLM] CUDA Out of Memory Error. "
|
|
f"Try: 1) Enable quantization (4bit/8bit), "
|
|
f"2) Reduce max_new_tokens, "
|
|
f"3) Use a smaller model. "
|
|
f"Error: {str(e)}",
|
|
)
|
|
except Exception as e:
|
|
import traceback
|
|
error_details = traceback.format_exc()
|
|
print(f"[SCG_LocalVLM] Error during inference:\n{error_details}")
|
|
return (f"[SCG_LocalVLM] Error during model inference: {str(e)}",)
|
|
finally:
|
|
if not keep_model_loaded:
|
|
self._unload_resources()
|
|
if processed_video_path:
|
|
try:
|
|
os.remove(processed_video_path)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
return result
|
|
|
|
|
|
class Qwen:
|
|
"""
|
|
Qwen Text-Only Language Model Node for ComfyUI
|
|
|
|
Supports Qwen2.5 and Qwen3 text generation models with:
|
|
- Multiple quantization options (none, 4bit, 8bit)
|
|
- Advanced generation parameters
|
|
- Flash Attention 2 optimization
|
|
- System and user prompt support
|
|
|
|
ComfyUI v2/v3 compatible.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.model_checkpoint = None
|
|
self.tokenizer = None
|
|
self.model = None
|
|
self.device = (
|
|
torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
|
|
)
|
|
self.bf16_support = (
|
|
torch.cuda.is_available()
|
|
and torch.cuda.get_device_capability(self.device)[0] >= 8
|
|
)
|
|
|
|
def _unload_resources(self):
|
|
_maybe_move_to_cpu(self.model)
|
|
self.model = None
|
|
self.tokenizer = None
|
|
_clear_cuda_memory()
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"system": (
|
|
"STRING",
|
|
{
|
|
"default": "You are a helpful assistant.",
|
|
"multiline": True,
|
|
},
|
|
),
|
|
"prompt": ("STRING", {"default": "", "multiline": True}),
|
|
"model": (
|
|
_get_text_model_list(),
|
|
{"default": "Qwen3-4B-Instruct-2507"},
|
|
),
|
|
"quantization": (
|
|
["none", "4bit", "8bit"],
|
|
{"default": "none"},
|
|
),
|
|
"attention_mode": (
|
|
["auto", "flash_attention_2", "sdpa", "eager"],
|
|
{"default": "auto"},
|
|
),
|
|
"keep_model_loaded": ("BOOLEAN", {"default": False}),
|
|
"bypass": ("BOOLEAN", {"default": False}),
|
|
"do_sample": ("BOOLEAN", {"default": True}),
|
|
"temperature": (
|
|
"FLOAT",
|
|
{"default": 0.7, "min": 0, "max": 2, "step": 0.05},
|
|
),
|
|
"top_p": (
|
|
"FLOAT",
|
|
{"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.05},
|
|
),
|
|
"top_k": (
|
|
"INT",
|
|
{"default": 50, "min": 0, "max": 200, "step": 1},
|
|
),
|
|
"min_p": (
|
|
"FLOAT",
|
|
{"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05},
|
|
),
|
|
"repetition_penalty": (
|
|
"FLOAT",
|
|
{"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.05},
|
|
),
|
|
"max_new_tokens": (
|
|
"INT",
|
|
{"default": 512, "min": 128, "max": 8000, "step": 1},
|
|
),
|
|
"seed": ("INT", {"default": -1}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("text",)
|
|
FUNCTION = "inference"
|
|
CATEGORY = "Comfyui_QwenVL"
|
|
|
|
def inference(
|
|
self,
|
|
system,
|
|
prompt,
|
|
model,
|
|
quantization,
|
|
attention_mode,
|
|
keep_model_loaded,
|
|
bypass,
|
|
do_sample,
|
|
temperature,
|
|
top_p,
|
|
top_k,
|
|
min_p,
|
|
repetition_penalty,
|
|
max_new_tokens,
|
|
repetition_penalty,
|
|
seed,
|
|
):
|
|
# Bypass mode: pass prompt directly to output without model inference
|
|
if bypass:
|
|
return (prompt,)
|
|
|
|
if not prompt.strip() and not system.strip():
|
|
return ("Error: Both system and prompt are empty.",)
|
|
|
|
if seed != -1:
|
|
torch.manual_seed(seed)
|
|
model_id = _get_model_repo_id(model, is_vl=False)
|
|
# put downloaded model to model/LLM dir
|
|
self.model_checkpoint = os.path.join(
|
|
folder_paths.models_dir, "LLM", os.path.basename(model_id)
|
|
)
|
|
|
|
if not os.path.exists(self.model_checkpoint):
|
|
from huggingface_hub import snapshot_download
|
|
|
|
snapshot_download(
|
|
repo_id=model_id,
|
|
local_dir=self.model_checkpoint,
|
|
)
|
|
|
|
if self.tokenizer is None:
|
|
self.tokenizer = AutoTokenizer.from_pretrained(self.model_checkpoint)
|
|
|
|
if self.model is None:
|
|
load_start = time.time()
|
|
|
|
# Check quantization support if using quantization
|
|
if quantization != "none":
|
|
quant_ok, quant_msg = _check_quantization_support()
|
|
if not quant_ok:
|
|
print(f"[SCG_LocalVLM] WARNING: {quant_msg}")
|
|
print("[SCG_LocalVLM] Falling back to unquantized loading")
|
|
quantization = "none"
|
|
else:
|
|
print(f"[SCG_LocalVLM] {quant_msg}")
|
|
|
|
# Determine compute dtype
|
|
compute_dtype = torch.bfloat16 if self.bf16_support else torch.float16
|
|
print(f"[SCG_LocalVLM] Using compute dtype: {compute_dtype}")
|
|
|
|
# Build load_kwargs - SIMPLIFIED for maximum performance
|
|
load_kwargs = {
|
|
"torch_dtype": compute_dtype,
|
|
}
|
|
|
|
if quantization == "4bit":
|
|
quantization_config = BitsAndBytesConfig(
|
|
load_in_4bit=True,
|
|
bnb_4bit_compute_dtype=compute_dtype,
|
|
bnb_4bit_quant_type="nf4",
|
|
bnb_4bit_use_double_quant=True,
|
|
)
|
|
load_kwargs["quantization_config"] = quantization_config
|
|
load_kwargs["device_map"] = "auto"
|
|
print(f"[SCG_LocalVLM] Loading with 4-bit quantization")
|
|
|
|
elif quantization == "8bit":
|
|
quantization_config = BitsAndBytesConfig(
|
|
load_in_8bit=True,
|
|
bnb_8bit_compute_dtype=compute_dtype,
|
|
)
|
|
load_kwargs["quantization_config"] = quantization_config
|
|
load_kwargs["device_map"] = "auto"
|
|
print(f"[SCG_LocalVLM] Loading with 8-bit quantization")
|
|
|
|
else:
|
|
# Non-quantized: NO device_map - use native PyTorch loading
|
|
print("[SCG_LocalVLM] Loading without quantization (native PyTorch)")
|
|
|
|
# Handle attention mode - simplified
|
|
if attention_mode == "flash_attention_2" and _FA2_COMPATIBLE:
|
|
try:
|
|
import flash_attn
|
|
load_kwargs["attn_implementation"] = "flash_attention_2"
|
|
attention_used = "flash_attention_2"
|
|
except ImportError:
|
|
load_kwargs["attn_implementation"] = "sdpa"
|
|
attention_used = "sdpa"
|
|
elif attention_mode == "sdpa" or (attention_mode == "flash_attention_2" and not _FA2_COMPATIBLE):
|
|
load_kwargs["attn_implementation"] = "sdpa"
|
|
attention_used = "sdpa"
|
|
elif attention_mode == "eager":
|
|
load_kwargs["attn_implementation"] = "eager"
|
|
attention_used = "eager"
|
|
else:
|
|
load_kwargs["attn_implementation"] = "sdpa"
|
|
attention_used = "sdpa"
|
|
|
|
print(f"[SCG_LocalVLM] Using {attention_used} attention")
|
|
|
|
# Load the model
|
|
print(f"[SCG_LocalVLM] Loading model from {self.model_checkpoint}")
|
|
|
|
try:
|
|
self.model = AutoModelForCausalLM.from_pretrained(
|
|
self.model_checkpoint,
|
|
**load_kwargs,
|
|
)
|
|
|
|
load_time = time.time() - load_start
|
|
|
|
# For non-quantized: move to GPU
|
|
if quantization == "none":
|
|
self.model = self.model.cuda()
|
|
|
|
self.model.eval()
|
|
|
|
# Print comprehensive model info
|
|
print(f"[SCG_LocalVLM] Model loaded successfully in {load_time:.2f}s")
|
|
print(f"[SCG_LocalVLM] Device: {self.model.device}")
|
|
print(f"[SCG_LocalVLM] Dtype: {self.model.dtype}")
|
|
print(f"[SCG_LocalVLM] Attention: {attention_used}")
|
|
|
|
# Verify actual attention implementation
|
|
if hasattr(self.model.config, '_attn_implementation'):
|
|
actual_attn = self.model.config._attn_implementation
|
|
print(f"[SCG_LocalVLM] Attention (verified): {actual_attn}")
|
|
|
|
# Show quantization info
|
|
if quantization != "none":
|
|
print(f"[SCG_LocalVLM] Quantization: {quantization}")
|
|
if hasattr(self.model, 'hf_device_map'):
|
|
print(f"[SCG_LocalVLM] Device map: {self.model.hf_device_map}")
|
|
|
|
# Check if model is actually quantized
|
|
is_quantized = False
|
|
for name, param in self.model.named_parameters():
|
|
if hasattr(param, 'quant_state'):
|
|
is_quantized = True
|
|
break
|
|
|
|
if is_quantized:
|
|
print(f"[SCG_LocalVLM] Quantization: VERIFIED (layers are quantized)")
|
|
else:
|
|
print(f"[SCG_LocalVLM] WARNING: Quantization requested but layers not quantized!")
|
|
|
|
except Exception as e:
|
|
print(f"[SCG_LocalVLM] Error loading model: {str(e)}")
|
|
import traceback
|
|
print(traceback.format_exc())
|
|
|
|
# Try fallback without attention implementation
|
|
if "attn_implementation" in load_kwargs:
|
|
print(f"[SCG_LocalVLM] Retrying without {attention_mode} attention...")
|
|
load_kwargs.pop("attn_implementation", None)
|
|
|
|
self.model = AutoModelForCausalLM.from_pretrained(
|
|
self.model_checkpoint,
|
|
**load_kwargs,
|
|
)
|
|
print("[SCG_LocalVLM] Model loaded with fallback attention")
|
|
else:
|
|
raise
|
|
|
|
result = None
|
|
with torch.inference_mode():
|
|
messages = [
|
|
{"role": "system", "content": system},
|
|
{"role": "user", "content": prompt},
|
|
]
|
|
|
|
try:
|
|
text = self.tokenizer.apply_chat_template(
|
|
messages, tokenize=False, add_generation_prompt=True
|
|
)
|
|
|
|
inputs = self.tokenizer([text], return_tensors="pt").to(self.model.device)
|
|
|
|
# Build generation kwargs with proper parameter handling
|
|
generation_kwargs = {
|
|
"max_new_tokens": max_new_tokens,
|
|
"do_sample": do_sample,
|
|
"use_cache": True, # CRITICAL: Enable KV cache
|
|
}
|
|
|
|
# Add pad_token_id to prevent warnings and improve batching
|
|
if hasattr(self.tokenizer, 'pad_token_id') and self.tokenizer.pad_token_id is not None:
|
|
generation_kwargs["pad_token_id"] = self.tokenizer.pad_token_id
|
|
elif hasattr(self.tokenizer, 'eos_token_id') and self.tokenizer.eos_token_id is not None:
|
|
generation_kwargs["pad_token_id"] = self.tokenizer.eos_token_id
|
|
|
|
# Add sampling parameters only if do_sample is True
|
|
if do_sample:
|
|
if temperature > 0:
|
|
generation_kwargs["temperature"] = temperature
|
|
if top_p < 1.0:
|
|
generation_kwargs["top_p"] = top_p
|
|
if top_k > 0:
|
|
generation_kwargs["top_k"] = top_k
|
|
if min_p > 0:
|
|
generation_kwargs["min_p"] = min_p
|
|
|
|
# Always apply repetition penalty if not default
|
|
if repetition_penalty != 1.0:
|
|
generation_kwargs["repetition_penalty"] = repetition_penalty
|
|
|
|
# Start timing
|
|
import time
|
|
gen_start = time.time()
|
|
|
|
generated_ids = self.model.generate(**inputs, **generation_kwargs)
|
|
|
|
# Calculate performance
|
|
gen_time = time.time() - gen_start
|
|
|
|
generated_ids_trimmed = [
|
|
out_ids[len(in_ids) :]
|
|
for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
|
|
]
|
|
|
|
# Calculate tokens/sec
|
|
if len(generated_ids_trimmed) > 0:
|
|
tokens_generated = len(generated_ids_trimmed[0])
|
|
tokens_per_sec = tokens_generated / gen_time if gen_time > 0 else 0
|
|
print(f"[SCG_LocalVLM] Generation Performance:")
|
|
print(f"[SCG_LocalVLM] Tokens: {tokens_generated}")
|
|
print(f"[SCG_LocalVLM] Time: {gen_time:.2f}s")
|
|
print(f"[SCG_LocalVLM] Speed: {tokens_per_sec:.2f} tokens/sec")
|
|
|
|
result = self.tokenizer.batch_decode(
|
|
generated_ids_trimmed,
|
|
skip_special_tokens=True,
|
|
clean_up_tokenization_spaces=False,
|
|
)
|
|
except torch.cuda.OutOfMemoryError as e:
|
|
_clear_cuda_memory()
|
|
return (
|
|
f"[SCG_LocalVLM] CUDA Out of Memory Error. "
|
|
f"Try: 1) Enable quantization (4bit/8bit), "
|
|
f"2) Reduce max_new_tokens, "
|
|
f"3) Use a smaller model. "
|
|
f"Error: {str(e)}",
|
|
)
|
|
except Exception as e:
|
|
import traceback
|
|
error_details = traceback.format_exc()
|
|
print(f"[SCG_LocalVLM] Error during inference:\n{error_details}")
|
|
return (f"[SCG_LocalVLM] Error during model inference: {str(e)}",)
|
|
finally:
|
|
if not keep_model_loaded:
|
|
self._unload_resources()
|
|
|
|
return result
|