pearsonkyle/SDXL-Model-Merger
2
1"""Pipeline management for SDXL Model Merger."""2 3import torch4from diffusers import (5 StableDiffusionXLPipeline,6 AutoencoderKL,7 DPMSolverSDEScheduler,8)9 10from . import config11from .config import device, dtype, CACHE_DIR, device_description, is_running_on_spaces, set_download_cancelled12from .downloader import get_safe_filename_from_url, download_file_with_progress13from .gpu_decorator import GPU14 15 16@GPU(duration=300)17def _load_and_setup_pipeline(checkpoint_path, vae_path, lora_paths_and_strengths, load_kwargs):18 """GPU-decorated helper that performs all GPU-intensive pipeline setup."""19 _pipe = StableDiffusionXLPipeline.from_single_file(20 str(checkpoint_path),21 **load_kwargs,22 )23 print(" ✅ Text encoders loaded")24 25 # Move to device (unless using device_map='auto' which handles this automatically)26 if not is_running_on_spaces() or device != "cpu":27 print(f" ⚙️ Moving pipeline to device: {device_description}...")28 _pipe = _pipe.to(device=device, dtype=dtype)29 30 # Load custom VAE if provided31 if vae_path is not None:32 print(" ⚙️ Loading VAE weights...")33 vae = AutoencoderKL.from_single_file(34 str(vae_path),35 torch_dtype=dtype,36 )37 print(" ⚙️ Setting custom VAE...")38 _pipe.vae = vae.to(device=device, dtype=torch.float32)39 40 # Load and fuse each LoRA41 if lora_paths_and_strengths:42 # Ensure pipeline is on device for LoRA fusion43 _pipe = _pipe.to(device=device, dtype=dtype)44 45 for i, (lora_path, strength) in enumerate(lora_paths_and_strengths):46 adapter_name = f"lora_{i}"47 print(f" ⚙️ Loading LoRA {i+1}/{len(lora_paths_and_strengths)}...")48 _pipe.load_lora_weights(str(lora_path), adapter_name=adapter_name)49 print(f" ⚙️ Fusing LoRA {i+1} with strength={strength}...")50 _pipe.fuse_lora(adapter_names=[adapter_name], lora_scale=strength)51 _pipe.unload_lora_weights()52 else:53 # Move pipeline to device even without LoRAs54 _pipe = _pipe.to(device=device, dtype=dtype)55 56 # Set scheduler57 print(" ⚙️ Configuring scheduler...")58 _pipe.scheduler = DPMSolverSDEScheduler.from_config(59 _pipe.scheduler.config,60 algorithm_type="sde-dpmsolver++",61 use_karras_sigmas=False,62 )63 64 # Keep VAE in float32 to prevent colorful static output65 _pipe.vae.to(dtype=torch.float32)66 67 return _pipe68 69 70def load_pipeline(71 checkpoint_url: str,72 vae_url: str,73 lora_urls_str: str,74 lora_strengths_str: str,75 progress=None76) -> tuple[str, str]:77 """78 Load SDXL pipeline with checkpoint, VAE, and LoRAs.79 80 Args:81 checkpoint_url: URL to base model .safetensors file82 vae_url: Optional URL to VAE .safetensors file83 lora_urls_str: Newline-separated URLs for LoRA models84 lora_strengths_str: Comma-separated strength values for each LoRA85 progress: Optional gr.Progress() object for UI updates86 87 Yields:88 Tuple of (status_message, progress_text) at each loading stage.89 90 Returns:91 Final yielded tuple of (final_status_message, progress_text)92 """93 # Clear any previously loaded pipeline so the UI reflects loading state94 config.set_pipe(None)95 96 try:97 set_download_cancelled(False)98 99 print("=" * 60)100 print("🔄 Loading SDXL Pipeline...")101 print("=" * 60)102 103 checkpoint_filename = get_safe_filename_from_url(checkpoint_url, type_prefix="model")104 checkpoint_path = CACHE_DIR / checkpoint_filename105 106 # Check if checkpoint is already cached107 checkpoint_cached = checkpoint_path.exists() and checkpoint_path.stat().st_size > 0108 109 # Validate cache file before using it110 if checkpoint_cached:111 is_valid, msg = config.validate_cache_file(checkpoint_path)112 if not is_valid:113 print(f" ⚠️ Cache invalid: {msg}")114 checkpoint_path.unlink(missing_ok=True)115 checkpoint_cached = False116 117 # VAE: Use suffix="_vae" and default to "vae.safetensors" for proper caching/dropdown matching118 vae_filename = get_safe_filename_from_url(vae_url, default_name="vae.safetensors", suffix="_vae") if vae_url.strip() else None119 vae_path = CACHE_DIR / vae_filename if vae_filename else None120 vae_cached = vae_url.strip() and vae_path and vae_path.exists() and vae_path.stat().st_size > 0121 122 # Validate VAE cache file before using it123 if vae_cached:124 is_valid, msg = config.validate_cache_file(vae_path)125 if not is_valid:126 print(f" ⚠️ VAE Cache invalid: {msg}")127 vae_path.unlink(missing_ok=True)128 vae_cached = False129 130 # Download checkpoint (skips if already cached)131 if progress:132 progress(0.1, desc="Downloading base model..." if not checkpoint_cached else "Loading base model...")133 134 if not checkpoint_cached:135 status_msg = f"📥 Downloading {checkpoint_path.name}..."136 print(f" 📥 Downloading: {checkpoint_path.name}")137 else:138 status_msg = f"✅ Using cached {checkpoint_path.name}"139 print(f" ✅ Using cached: {checkpoint_path.name}")140 141 yield status_msg, "Starting download..."142 143 if not checkpoint_cached:144 download_file_with_progress(checkpoint_url, checkpoint_path)145 146 # Download VAE if provided (loading happens in _load_and_setup_pipeline)147 if vae_url and vae_url.strip():148 if vae_path:149 status_msg = f"📥 Downloading {vae_path.name}..." if not vae_cached else f"✅ Using cached {vae_path.name}"150 print(f" 📥 VAE: {vae_path.name}" if not vae_cached else f" ✅ VAE (cached): {vae_path.name}")151 152 if progress:153 progress(0.2, desc="Downloading VAE..." if not vae_cached else "Loading VAE...")154 155 yield status_msg, f"Downloading VAE: {vae_path.name}" if not vae_cached else f"Using cached VAE: {vae_path.name}"156 157 if not vae_cached:158 download_file_with_progress(vae_url, vae_path)159 160 # For CPU/low-memory environments on Spaces, use device_map for better RAM management161 load_kwargs = {162 "torch_dtype": dtype,163 "use_safetensors": True,164 }165 166 if is_running_on_spaces() and device == "cpu":167 print(" ℹ️ CPU mode detected: enabling device_map='auto' for better RAM management")168 load_kwargs["device_map"] = "auto"169 170 # Parse LoRA URLs & ensure strengths list matches171 lora_urls = [u.strip() for u in lora_urls_str.split("\n") if u.strip()]172 strengths_raw = [s.strip() for s in lora_strengths_str.split(",")]173 strengths = []174 for i, url in enumerate(lora_urls):175 try:176 val = float(strengths_raw[i]) if i < len(strengths_raw) else 1.0177 strengths.append(val)178 except ValueError:179 strengths.append(1.0)180 181 # Download LoRAs (CPU-bound downloads, before GPU work)182 lora_paths_and_strengths = []183 if lora_urls:184 for i, (lora_url, strength) in enumerate(zip(lora_urls, strengths)):185 lora_filename = get_safe_filename_from_url(lora_url, suffix="_lora")186 lora_path = CACHE_DIR / lora_filename187 lora_cached = lora_path.exists() and lora_path.stat().st_size > 0188 189 # Validate LoRA cache file before using it190 if lora_cached:191 is_valid, msg = config.validate_cache_file(lora_path)192 if not is_valid:193 print(f" ⚠️ LoRA Cache invalid: {msg}")194 lora_path.unlink(missing_ok=True)195 lora_cached = False196 197 if not lora_cached:198 print(f" 📥 LoRA {i+1}/{len(lora_urls)}: Downloading {lora_path.name}...")199 status_msg = f"📥 Downloading LoRA {i+1}/{len(lora_urls)}: {lora_path.name}..."200 else:201 print(f" ✅ LoRA {i+1}/{len(lora_urls)}: Using cached {lora_path.name}")202 status_msg = f"✅ Using cached LoRA {i+1}/{len(lora_urls)}: {lora_path.name}"203 204 yield (205 status_msg,206 f"Downloading LoRA {i+1}/{len(lora_urls)} ({lora_path.name})..." if not lora_cached207 else f"Using cached LoRA {i+1}/{len(lora_urls)} ({lora_path.name})"208 )209 210 if not lora_cached:211 download_file_with_progress(lora_url, lora_path)212 213 lora_paths_and_strengths.append((lora_path, strength))214 215 # All downloads complete — now do GPU-intensive setup in one decorated call216 yield "⚙️ Loading SDXL pipeline...", "Loading model weights into memory..."217 218 if progress:219 progress(0.5, desc="Loading pipeline...")220 221 _pipe = _load_and_setup_pipeline(222 checkpoint_path, vae_path, lora_paths_and_strengths, load_kwargs223 )224 225 if progress:226 progress(0.95, desc="Finalizing...")227 228 # ✅ Only publish the pipeline globally AFTER all steps succeed229 config.set_pipe(_pipe)230 231 print(" ✅ Pipeline ready!")232 yield "✅ Pipeline ready!", f"Ready! Loaded {len(lora_urls)} LoRA(s)"233 234 except KeyboardInterrupt:235 set_download_cancelled(False)236 config.set_pipe(None)237 print("\n⚠️ Download cancelled by user")238 return ("⚠️ Download cancelled by user", "Cancelled")239 except Exception as e:240 import traceback241 config.set_pipe(None)242 error_msg = f"❌ Error loading pipeline: {str(e)}"243 print(f"\n{error_msg}")244 print(traceback.format_exc())245 return (error_msg, f"Error: {str(e)}")246 247 248def cancel_download():249 """Set the global cancellation flag to stop any ongoing downloads."""250 set_download_cancelled(True)251 252 253def get_pipeline() -> StableDiffusionXLPipeline | None:254 """Get the currently loaded pipeline."""255 return config.get_pipe()256 