## Problem PyTorch threads can only be set ONCE at startup, before any parallel work begins. Trying to set them during inference causes: RuntimeError: cannot set number of interop threads after parallel work has started ## Solution - Set threads at module load time (lines 63-75) - Default to 8 threads for optimal CPU parallelism - Can be overridden via PYTORCH_THREADS environment variable - Gracefully handle case where threads already set by ComfyUI ## Usage To use different thread count, set environment variable before starting ComfyUI: Windows: set PYTORCH_THREADS=12 Linux/Mac: export PYTORCH_THREADS=12 Then start ComfyUI. Removed dropdown parameter since it can't be changed dynamically.
1256 lines
52 KiB
Python
1256 lines
52 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
|
|
import transformers
|
|
|
|
try:
|
|
import comfy.model_management as comfy_mm
|
|
except ImportError: # ComfyUI runtime not available during development/tests
|
|
comfy_mm = None
|
|
|
|
# COMPATIBILITY PATCH for transformers 4.56.2
|
|
# Fixes slow generation (30% GPU util) in Qwen3-VL models
|
|
# This issue is fixed in transformers 4.57.1+, but we backport for compatibility
|
|
_TRANSFORMERS_VERSION = tuple(int(x) for x in transformers.__version__.split('.')[:3])
|
|
_NEEDS_PATCH = _TRANSFORMERS_VERSION < (4, 57, 1)
|
|
|
|
if _NEEDS_PATCH:
|
|
print(f"[SCG_LocalVLM] Applying compatibility patch for transformers {transformers.__version__}")
|
|
print(f"[SCG_LocalVLM] This fixes slow generation issue (will be 3-4x faster)")
|
|
|
|
# Monkey patch to optimize vision token handling in 4.56.2
|
|
_original_qwen3_prepare_inputs = Qwen3VLForConditionalGeneration.prepare_inputs_for_generation
|
|
|
|
def _patched_prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
|
|
"""
|
|
Patched version that prevents re-processing vision tokens on every step.
|
|
In 4.56.2, pixel_values were being passed through on every generation step,
|
|
causing the vision encoder to run repeatedly (30% GPU util bottleneck).
|
|
"""
|
|
# Call original method
|
|
model_inputs = _original_qwen3_prepare_inputs(self, input_ids, past_key_values, **kwargs)
|
|
|
|
# CRITICAL FIX: Remove pixel_values after first pass
|
|
# Vision encoder should only run once during prefill, not on every decode step
|
|
if past_key_values is not None and len(past_key_values) > 0:
|
|
# We have cached key-values, so vision has already been processed
|
|
# Remove vision-related inputs to prevent re-encoding
|
|
model_inputs.pop('pixel_values', None)
|
|
model_inputs.pop('image_grid_thw', None)
|
|
|
|
return model_inputs
|
|
|
|
# Apply the patch
|
|
Qwen3VLForConditionalGeneration.prepare_inputs_for_generation = _patched_prepare_inputs_for_generation
|
|
|
|
# PERFORMANCE: Set PyTorch thread count for optimal CPU parallelism
|
|
# Can be overridden via environment variable: PYTORCH_THREADS
|
|
# Low thread count causes CPU bottleneck -> low GPU utilization
|
|
_DEFAULT_THREADS = 8
|
|
_THREAD_COUNT = int(os.environ.get('PYTORCH_THREADS', _DEFAULT_THREADS))
|
|
try:
|
|
torch.set_num_threads(_THREAD_COUNT)
|
|
torch.set_num_interop_threads(_THREAD_COUNT)
|
|
print(f"[SCG_LocalVLM] PyTorch threads set to {_THREAD_COUNT} (override with PYTORCH_THREADS env var)")
|
|
except RuntimeError as e:
|
|
# Already set by ComfyUI or another module
|
|
print(f"[SCG_LocalVLM] Thread count already set: {e}")
|
|
print(f"[SCG_LocalVLM] Current threads: {torch.get_num_threads()}, interop: {torch.get_num_interop_threads()}")
|
|
|
|
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.current_image_resolution = None # Track current resolution setting
|
|
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
|
|
self.current_image_resolution = 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},
|
|
),
|
|
"seed": ("INT", {"default": -1}),
|
|
"image_resolution": (
|
|
["low (256x28x28)", "medium (512x28x28)", "high (768x28x28)", "ultra (1024x28x28)"],
|
|
{"default": "medium (512x28x28)"},
|
|
),
|
|
},
|
|
"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,
|
|
seed,
|
|
image_resolution,
|
|
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,
|
|
)
|
|
|
|
# Recreate processor if it doesn't exist OR if resolution setting changed
|
|
if self.processor is None or self.current_image_resolution != image_resolution:
|
|
# Define min_pixels and max_pixels based on image_resolution setting:
|
|
# Images will be resized to maintain their aspect ratio
|
|
# within the range of min_pixels and max_pixels.
|
|
# Lower resolution = faster processing, higher resolution = better quality
|
|
resolution_map = {
|
|
"low (256x28x28)": (128*28*28, 256*28*28), # 100K - 200K pixels (fastest)
|
|
"medium (512x28x28)": (256*28*28, 512*28*28), # 200K - 400K pixels (balanced)
|
|
"high (768x28x28)": (512*28*28, 768*28*28), # 400K - 600K pixels (quality)
|
|
"ultra (1024x28x28)": (768*28*28, 1024*28*28), # 600K - 800K pixels (original, slowest)
|
|
}
|
|
min_pixels, max_pixels = resolution_map.get(image_resolution, (256*28*28, 512*28*28))
|
|
|
|
if self.current_image_resolution != image_resolution:
|
|
print(f"[SCG_LocalVLM] Image resolution changed: {self.current_image_resolution} → {image_resolution}")
|
|
print(f"[SCG_LocalVLM] Image resolution: {image_resolution}")
|
|
print(f"[SCG_LocalVLM] min_pixels={min_pixels:,}, max_pixels={max_pixels:,}")
|
|
|
|
self.processor = AutoProcessor.from_pretrained(
|
|
self.model_checkpoint,
|
|
min_pixels=min_pixels,
|
|
max_pixels=max_pixels,
|
|
)
|
|
self.current_image_resolution = image_resolution
|
|
|
|
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()
|
|
|
|
# CRITICAL: Clear CUDA cache and reset internal state to avoid random slowness
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
torch.cuda.synchronize()
|
|
# Reset any accumulated gradients or state
|
|
for param in self.model.parameters():
|
|
param.grad = None
|
|
|
|
# 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}")
|
|
if actual_attn != attention_used:
|
|
print(f"[SCG_LocalVLM] WARNING: Requested {attention_used} but got {actual_attn}!")
|
|
|
|
# GPU memory diagnostics
|
|
if torch.cuda.is_available():
|
|
mem_allocated = torch.cuda.memory_allocated(0) / 1024**3
|
|
mem_reserved = torch.cuda.memory_reserved(0) / 1024**3
|
|
print(f"[SCG_LocalVLM] GPU Memory: {mem_allocated:.2f}GB allocated, {mem_reserved:.2f}GB reserved")
|
|
|
|
# Verify model is on GPU for non-quantized
|
|
if quantization == "none":
|
|
# Check first parameter device
|
|
first_param = next(self.model.parameters())
|
|
if not first_param.is_cuda:
|
|
print(f"[SCG_LocalVLM] WARNING: Model is on {first_param.device}, not CUDA!")
|
|
print(f"[SCG_LocalVLM] This will cause VERY slow inference!")
|
|
|
|
# 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
|
|
img_convert_start = time.time()
|
|
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
|
|
img_convert_time = time.time() - img_convert_start
|
|
|
|
if image_count > 0:
|
|
print(f"Total images added: {image_count}")
|
|
print(f"[SCG_LocalVLM] Image conversion time: {img_convert_time:.3f}s")
|
|
|
|
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)
|
|
|
|
# Time vision info processing
|
|
vision_start = time.time()
|
|
image_inputs, video_inputs = process_vision_info(messages)
|
|
vision_time = time.time() - vision_start
|
|
print(f"[SCG_LocalVLM] Vision info processing time: {vision_time:.3f}s")
|
|
|
|
# Time processor (image embedding generation) - this is often the slowest part
|
|
processor_start = time.time()
|
|
inputs = self.processor(
|
|
text=[text],
|
|
images=image_inputs,
|
|
videos=video_inputs,
|
|
padding=True,
|
|
return_tensors="pt",
|
|
).to(self.model.device)
|
|
processor_time = time.time() - processor_start
|
|
print(f"[SCG_LocalVLM] Processor (embedding) time: {processor_time:.3f}s")
|
|
|
|
# CRITICAL: Verify inputs are actually on GPU
|
|
print(f"[SCG_LocalVLM] Verifying input devices...")
|
|
print(f"[SCG_LocalVLM] input_ids device: {inputs.input_ids.device}")
|
|
if hasattr(inputs, 'pixel_values') and inputs.pixel_values is not None:
|
|
print(f"[SCG_LocalVLM] pixel_values device: {inputs.pixel_values.device}")
|
|
print(f"[SCG_LocalVLM] pixel_values shape: {inputs.pixel_values.shape}")
|
|
if hasattr(inputs, 'image_grid_thw') and inputs.image_grid_thw is not None:
|
|
print(f"[SCG_LocalVLM] image_grid_thw device: {inputs.image_grid_thw.device}")
|
|
|
|
# Show input context size (important for understanding generation speed)
|
|
input_token_count = inputs.input_ids.shape[1]
|
|
print(f"[SCG_LocalVLM] Input context: {input_token_count} tokens (text + vision)")
|
|
|
|
# Timing check - see if there's a delay before generation
|
|
print(f"[SCG_LocalVLM] Building generation kwargs...")
|
|
kwargs_start = time.time()
|
|
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
|
|
|
|
kwargs_time = time.time() - kwargs_start
|
|
print(f"[SCG_LocalVLM] Generation kwargs built in {kwargs_time:.3f}s")
|
|
|
|
# Check GPU memory before generation
|
|
if torch.cuda.is_available():
|
|
torch.cuda.synchronize()
|
|
gpu_mem_before = torch.cuda.memory_allocated(0) / 1024**3
|
|
|
|
# CRITICAL: Force CUDA sync and clear cache before generation to avoid random slowness
|
|
if torch.cuda.is_available():
|
|
torch.cuda.synchronize()
|
|
torch.cuda.empty_cache()
|
|
|
|
print(f"[SCG_LocalVLM] Starting generation with {input_token_count} input tokens...")
|
|
gen_start = time.time()
|
|
generated_ids = self.model.generate(**inputs, **generation_kwargs)
|
|
gen_time = time.time() - gen_start
|
|
|
|
# Check GPU memory after generation
|
|
if torch.cuda.is_available():
|
|
torch.cuda.synchronize()
|
|
gpu_mem_after = torch.cuda.memory_allocated(0) / 1024**3
|
|
gpu_mem_peak = torch.cuda.max_memory_allocated(0) / 1024**3
|
|
print(f"[SCG_LocalVLM] GPU Memory during generation: {gpu_mem_before:.2f}GB → {gpu_mem_after:.2f}GB (peak: {gpu_mem_peak:.2f}GB)")
|
|
torch.cuda.reset_peak_memory_stats(0)
|
|
|
|
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")
|
|
|
|
# Performance benchmarks for RTX 5090 + Qwen3-VL-4B
|
|
# Based on actual testing - see context.md
|
|
expected_speeds = {
|
|
"none": {"min": 15.0, "typical": 16.5, "attn": "sdpa or flash_attention_2"},
|
|
"4bit": {"min": 10.0, "typical": 50.0, "attn": "eager (quantization doesn't work well with FA2)"},
|
|
"8bit": {"min": 8.0, "typical": 40.0, "attn": "eager"},
|
|
}
|
|
|
|
# Check if performance is below expected
|
|
if quantization in expected_speeds:
|
|
expected = expected_speeds[quantization]
|
|
if tokens_per_sec < expected["min"]:
|
|
print(f"[SCG_LocalVLM] ⚠️ WARNING: Very slow generation speed!")
|
|
print(f"[SCG_LocalVLM] Expected: {expected['typical']:.1f} tok/s")
|
|
print(f"[SCG_LocalVLM] Got: {tokens_per_sec:.2f} tok/s ({tokens_per_sec/expected['typical']*100:.1f}% of expected)")
|
|
print(f"[SCG_LocalVLM] Recommended attention: {expected['attn']}")
|
|
print(f"[SCG_LocalVLM] Possible causes:")
|
|
print(f"[SCG_LocalVLM] 1. Model loaded on CPU instead of GPU (check warnings above)")
|
|
print(f"[SCG_LocalVLM] 2. Wrong attention mode (try '{expected['attn']}')")
|
|
print(f"[SCG_LocalVLM] 3. Other processes using GPU (check nvidia-smi)")
|
|
if quantization == "none":
|
|
print(f"[SCG_LocalVLM] 4. Try 'sdpa' instead of 'flash_attention_2' (often faster)")
|
|
elif quantization == "4bit":
|
|
print(f"[SCG_LocalVLM] 4. Quantization kernels issue (pip install bitsandbytes --upgrade)")
|
|
|
|
# Show timing breakdown if images were processed
|
|
if image_count > 0:
|
|
total_prep_time = img_convert_time + vision_time + processor_time
|
|
print(f"[SCG_LocalVLM] Timing Breakdown:")
|
|
print(f"[SCG_LocalVLM] Image conversion: {img_convert_time:.3f}s")
|
|
print(f"[SCG_LocalVLM] Vision processing: {vision_time:.3f}s")
|
|
print(f"[SCG_LocalVLM] Image embedding: {processor_time:.3f}s")
|
|
print(f"[SCG_LocalVLM] Text generation: {gen_time:.3f}s")
|
|
print(f"[SCG_LocalVLM] Total prep time: {total_prep_time:.3f}s")
|
|
if processor_time > 1.0:
|
|
print(f"[SCG_LocalVLM] TIP: Image embedding is slow. Try:")
|
|
print(f"[SCG_LocalVLM] - Use 'low' or 'medium' image_resolution for faster processing")
|
|
print(f"[SCG_LocalVLM] - Current: {image_resolution}")
|
|
|
|
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,
|
|
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}")
|
|
if actual_attn != attention_used:
|
|
print(f"[SCG_LocalVLM] WARNING: Requested {attention_used} but got {actual_attn}!")
|
|
|
|
# GPU memory diagnostics
|
|
if torch.cuda.is_available():
|
|
mem_allocated = torch.cuda.memory_allocated(0) / 1024**3
|
|
mem_reserved = torch.cuda.memory_reserved(0) / 1024**3
|
|
print(f"[SCG_LocalVLM] GPU Memory: {mem_allocated:.2f}GB allocated, {mem_reserved:.2f}GB reserved")
|
|
|
|
# Verify model is on GPU for non-quantized
|
|
if quantization == "none":
|
|
# Check first parameter device
|
|
first_param = next(self.model.parameters())
|
|
if not first_param.is_cuda:
|
|
print(f"[SCG_LocalVLM] WARNING: Model is on {first_param.device}, not CUDA!")
|
|
print(f"[SCG_LocalVLM] This will cause VERY slow inference!")
|
|
|
|
# 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
|