3030from dataclasses import dataclass
3131from pathlib import Path
3232from threading import Lock
33- from typing import Any
33+ from typing import TYPE_CHECKING , Any
3434
3535from ..config import data_dir
3636from ..device .policy import Quantization
3737from ..errors import ComponentError
3838
39+ if TYPE_CHECKING :
40+ from ..graph .loader_runners import LoraRef
41+
3942
4043@dataclass (frozen = True )
4144class ArchSpec :
@@ -260,13 +263,15 @@ def _quantize_in_place(model: Any, quant: Quantization) -> None:
260263# --- component loaders (cached in-process) ------------------------------------------------------
261264
262265# Keyed by (arch, kind, file, dtype, quant, device) so switching one file (e.g. a different VAE),
263- # the quantization, or the load device reuses the other already-loaded components. The run manager
264- # executes one run at a time; the lock guards each build.
265- _CACHE : dict [tuple [str , str , str , str , str , str ], Any ] = {}
266+ # the quantization, or the load device reuses the other already-loaded components. A fused diffusion
267+ # transformer appends its LoRA stack (see ``lora_cache_key``) to that base key, so the same file
268+ # with a different stack is a distinct entry. The run manager executes one run at a time; the lock
269+ # guards each build.
270+ _CACHE : dict [tuple [str , ...], Any ] = {}
266271_CACHE_LOCK = Lock ()
267272
268273
269- def _cached (key : tuple [str , str , str , str , str , str ], build : Callable [[], Any ]) -> Any :
274+ def _cached (key : tuple [str , ... ], build : Callable [[], Any ]) -> Any :
270275 with _CACHE_LOCK :
271276 hit = _CACHE .get (key )
272277 if hit is not None :
@@ -284,17 +289,36 @@ def _device_key(device: str | None) -> str:
284289 return device or "cpu"
285290
286291
287- def unload_components (keep_files : set [str ] | None = None ) -> None :
292+ def lora_cache_key (loras : tuple [LoraRef , ...]) -> tuple [str , ...]:
293+ """The cache-key suffix a LoRA stack contributes to a fused diffusion transformer. Order- and
294+ strength-sensitive (fusing is not commutative), keyed by file+strength; an empty stack adds
295+ nothing, so an un-LoRA'd load keys exactly as it did before LoRA existed."""
296+ return tuple (f"{ lora .file } @{ lora .strength } " for lora in loras )
297+
298+
299+ def unload_components (
300+ keep_files : set [str ] | None = None , keep_loras : tuple [str , ...] | None = None
301+ ) -> None :
288302 """Drop cached components whose source file is NOT in ``keep_files``, freeing their VRAM/RAM.
289303
290304 Called when switching checkpoints so a new model doesn't stack on top of the previous one (the
291305 cache never evicted before, so a second distinct model roughly doubled resident memory). Only
292306 drops references + empties the CUDA cache - it does not move weights to CPU RAM (that would just
293307 relocate the pressure on a RAM-tight box). The caller must drop any pipeline holding these
294- components first, or the references keep them alive."""
308+ components first, or the references keep them alive.
309+
310+ ``keep_loras`` (the LoRA-stack suffix being loaded, from ``lora_cache_key``) additionally evicts
311+ a kept file's diffusion transformer when it carries a *different* stack: fusing a LoRA into an
312+ already-resident checkpoint would otherwise keep the unfused transformer AND the fused one - two
313+ full-size models on the card. Left None, eviction is file-only (the pre-LoRA behaviour)."""
295314 keep = keep_files or set ()
296315 with _CACHE_LOCK :
297- stale = [k for k in _CACHE if k [2 ] not in keep ] # k = (arch, kind, file, dtype, quant, dev)
316+ stale = []
317+ for k in _CACHE : # k = (arch, kind, file, dtype, quant, dev, *lora_suffix)
318+ if k [2 ] not in keep :
319+ stale .append (k )
320+ elif keep_loras is not None and k [1 ] == "diffusion" and tuple (k [6 :]) != keep_loras :
321+ stale .append (k )
298322 for k in stale :
299323 comp = _CACHE .pop (k )
300324 del comp
@@ -307,14 +331,16 @@ def load_diffusion(
307331 dtype : Any ,
308332 quant : Quantization = Quantization .NONE ,
309333 device : str | None = None ,
334+ loras : tuple [LoraRef , ...] = (),
310335) -> Any :
311336 """The diffusion transformer from a single ``.safetensors``. diffusers converts the checkpoint
312337 keys; the config comes from the bundled assets, so nothing is fetched at load time. ``quant``
313338 (smart memory) quantizes the weights on load. ``device`` (e.g. ``"cuda:0"``) streams each tensor
314339 **straight to the GPU** from an mmap-backed checkpoint - the fp16 weights are never materialized
315340 as an anonymous CPU copy (the host-RAM spike that OOM-killed the server), and for the int8 path
316341 torchao quantizes on-device per tensor. ``None`` loads to CPU (the offload path, where
317- accelerate installs its hooks before placing)."""
342+ accelerate installs its hooks before placing). ``loras`` are fused into the weights in order
343+ (the stack is part of the cache key), so a fused transformer is itself the cached artifact."""
318344
319345 def build () -> Any :
320346 from diffusers import ZImageTransformer2DModel
@@ -333,12 +359,20 @@ def build() -> Any:
333359 # ``from_pretrained``), so the transformer would load at full size and the "int8" plan would
334360 # blow the VRAM budget (a T4 OOMs mid-load). Quantize it explicitly with torchao after the
335361 # load instead - the weights briefly sit full-size on the device, then halve in place.
362+ # Fuse LoRAs BEFORE quantizing: the fuse adds a full-precision delta into each weight, which
363+ # int8 can't accept in place, and int8-quantized weights aren't a plain tensor to add into.
364+ if loras :
365+ from .lora import fuse_loras
366+
367+ fuse_loras (model , loras )
336368 _quantize_in_place (model , quant )
337369 return model
338370
339- return _cached (
340- (arch , "diffusion" , file , _dtype_key (dtype ), quant .value , _device_key (device )), build
371+ key = (
372+ arch , "diffusion" , file , _dtype_key (dtype ), quant .value , _device_key (device ),
373+ * lora_cache_key (loras ),
341374 )
375+ return _cached (key , build )
342376
343377
344378def load_vae (arch : str , file : str , dtype : Any , device : str | None = None ) -> Any :
@@ -431,6 +465,7 @@ def assemble_zimage_pipeline(
431465 quant : Quantization = Quantization .NONE ,
432466 vae_dtype : Any = None ,
433467 device : str | None = None ,
468+ loras : tuple [LoraRef , ...] = (),
434469 cancel_check : Callable [[], None ] | None = None ,
435470) -> Any :
436471 """Build a Z-Image pipeline from three local single files. Components are cached individually,
@@ -448,7 +483,7 @@ def assemble_zimage_pipeline(
448483 from diffusers import ZImageImg2ImgPipeline , ZImagePipeline
449484
450485 arch = _ZIMAGE .key
451- transformer = load_diffusion (arch , diffusion_file , dtype , quant , device = device )
486+ transformer = load_diffusion (arch , diffusion_file , dtype , quant , device = device , loras = loras )
452487 _release_transient ()
453488 if cancel_check is not None :
454489 cancel_check ()
0 commit comments