Files
CalamitousFelicitousness 259e15fafe feat(lora): per-layer select stack modes klora and estlora
Two-network subject+style sets select a winner per layer instead of
summing: scores are top-K magnitude sums (klora) or Frobenius energies
(estlora), and a timestep ramp shifts layers from the subject network
toward the style network across sampling, reduced to at most one
precomputed flip per layer per pass. On sub-8-bit SDNQ the pair rides
the side-channel as separate segments flipped in place; other layers
recompute the winner from the pristine backup, so select modes force
backup mode. Selection resets per pass from the callback setup and is
gated off under model compile. estlora's measured style-discrepancy
term is exposed as an option. Adds XYZ axes for the stack settings.
2026-08-28 13:09:25 +01:00

337 lines
18 KiB
Python

from __future__ import annotations
import re
import time
from typing import TYPE_CHECKING
import torch
from modules.lora import lora_common as l
from modules.lora import lora_stack
from modules import shared, devices, errors
from modules.logger import log
if TYPE_CHECKING:
from collections.abc import Callable
import diffusers.models.lora
re_network_name = re.compile(r"(.*)\s*\([0-9a-fA-F]+\)")
def network_backup_weights(self: torch.nn.Conv2d | torch.nn.Linear | torch.nn.GroupNorm | torch.nn.LayerNorm | diffusers.models.lora.LoRACompatibleLinear | diffusers.models.lora.LoRACompatibleConv, network_layer_name: str, wanted_names: tuple, fuse: bool):
backup_size = 0
if len(l.loaded_networks) > 0 and network_layer_name is not None and any([net.modules.get(network_layer_name, None) for net in l.loaded_networks]): # noqa: C419 # pylint: disable=R1729
t0 = time.time()
weights_backup = getattr(self, "network_weights_backup", None)
bias_backup = getattr(self, "network_bias_backup", None)
if weights_backup is not None or bias_backup is not None:
if (fuse and not isinstance(weights_backup, bool)) or (not fuse and isinstance(weights_backup, bool)): # invalidate so we can change direct/backup on-the-fly
weights_backup = None
bias_backup = None
self.network_weights_backup = weights_backup
self.network_bias_backup = bias_backup
if weights_backup is None and wanted_names != (): # pylint: disable=C1803
weight = getattr(self, 'weight', None)
self.network_weights_backup = None
if fuse:
self.network_weights_backup = True
else:
self.network_weights_backup = weight.clone().to(devices.cpu)
if hasattr(self, "sdnq_dequantizer"):
self.sdnq_dequantizer_backup = self.sdnq_dequantizer
self.sdnq_scale_backup = self.scale.clone().to(devices.cpu)
if self.zero_point is not None:
self.sdnq_zero_point_backup = self.zero_point.clone().to(devices.cpu)
else:
self.sdnq_zero_point_backup = None
if self.svd_up is not None:
self.sdnq_svd_up_backup = self.svd_up.clone().to(devices.cpu)
self.sdnq_svd_down_backup = self.svd_down.clone().to(devices.cpu)
else:
self.sdnq_svd_up_backup = None
self.sdnq_svd_down_backup = None
if bias_backup is None:
if getattr(self, 'bias', None) is not None:
if fuse:
self.network_bias_backup = True
else:
bias_backup = self.bias.clone()
bias_backup = bias_backup.to(devices.cpu)
if getattr(self, 'network_weights_backup', None) is not None:
backup_size += self.network_weights_backup.numel() * self.network_weights_backup.element_size() if isinstance(self.network_weights_backup, torch.Tensor) else 0
if getattr(self, 'network_bias_backup', None) is not None:
backup_size += self.network_bias_backup.numel() * self.network_bias_backup.element_size() if isinstance(self.network_bias_backup, torch.Tensor) else 0
l.timer.backup += time.time() - t0
return backup_size
def network_calc_weights(self: torch.nn.Conv2d | torch.nn.Linear | torch.nn.GroupNorm | torch.nn.LayerNorm | diffusers.models.lora.LoRACompatibleLinear | diffusers.models.lora.LoRACompatibleConv, network_layer_name: str, use_previous: bool = False, *, elimit: Callable[[], None] | None = None, per_net: bool = False):
if shared.opts.diffusers_offload_mode == "none":
try:
self.to(devices.device)
except Exception:
pass
batch_updown = None
batch_ex_bias = None
stack_deltas = None
if per_net or (lora_stack.mode() in lora_stack.DENSE_MODES and network_layer_name is not None and not network_layer_name.startswith('lora_te')):
stack_deltas = [] # collect per-net deltas; combined after the loop unless the caller wants them separate (bias deltas stay summed)
loaded = l.loaded_networks if not use_previous else l.previously_loaded_networks
for net in loaded:
module = net.modules.get(network_layer_name, None)
if module is None:
continue
try:
t0 = time.time()
if hasattr(self, "sdnq_dequantizer_backup"):
weight = self.sdnq_dequantizer_backup(
self.weight.to(devices.device),
self.sdnq_scale_backup.to(devices.device),
self.sdnq_zero_point_backup.to(devices.device) if self.sdnq_zero_point_backup is not None else None,
self.sdnq_svd_up_backup.to(devices.device) if self.sdnq_svd_up_backup is not None else None,
self.sdnq_svd_down_backup.to(devices.device) if self.sdnq_svd_down_backup is not None else None,
skip_quantized_matmul=self.sdnq_dequantizer_backup.use_quantized_matmul
)
elif hasattr(self, "sdnq_dequantizer"):
weight = self.sdnq_dequantizer(
self.weight.to(devices.device),
self.scale.to(devices.device),
self.zero_point.to(devices.device) if self.zero_point is not None else None,
self.svd_up.to(devices.device) if self.svd_up is not None else None,
self.svd_down.to(devices.device) if self.svd_down is not None else None,
skip_quantized_matmul=self.sdnq_dequantizer.use_quantized_matmul
)
else:
weight = self.weight.to(devices.device) # must perform calc on gpu due to performance
updown, ex_bias = module.calc_updown(weight)
weight = None
del weight
if updown is not None:
if stack_deltas is not None:
stack_deltas.append((net.name, updown.to(devices.device)))
elif batch_updown is not None:
batch_updown += updown.to(batch_updown.device)
else:
batch_updown = updown.to(devices.device)
if ex_bias is not None:
if batch_ex_bias is not None:
batch_ex_bias += ex_bias.to(batch_ex_bias.device)
else:
batch_ex_bias = ex_bias.to(devices.device)
l.timer.calc += time.time() - t0
if shared.opts.diffusers_offload_mode == "sequential":
t0 = time.time()
if batch_updown is not None:
batch_updown = batch_updown.to(devices.cpu)
if batch_ex_bias is not None:
batch_ex_bias = batch_ex_bias.to(devices.cpu)
t1 = time.time()
l.timer.move += t1 - t0
except RuntimeError as e:
l.extra_network_lora.errors[net.name] = l.extra_network_lora.errors.get(net.name, 0) + 1
module_name = net.modules.get(network_layer_name, None)
log.error(f'Network: type=LoRA name="{net.name}" module="{module_name}" layer="{network_layer_name}" apply weight: {e}')
if l.debug:
errors.display(e, 'LoRA')
raise RuntimeError('LoRA apply weight') from e
if elimit is not None:
elimit()
continue
if per_net:
return stack_deltas, batch_ex_bias
if stack_deltas is not None and stack_deltas:
if len(stack_deltas) >= 2:
t0 = time.time()
batch_updown = lora_stack.combine(stack_deltas, network_layer_name)
l.timer.calc += time.time() - t0
else:
batch_updown = stack_deltas[0][1]
if shared.opts.diffusers_offload_mode == "sequential":
batch_updown = batch_updown.to(devices.cpu)
return batch_updown, batch_ex_bias
def assign_weight(self: torch.nn.Module, new_weight: torch.Tensor, device: torch.device, bias: bool = False):
"""Install new values on a module, writing into the existing parameter when it can take them.
Replacing a parameter puts the tensor on a fresh allocation, and matmul kernel
selection is sensitive to operand placement, so a swap shifts otherwise
deterministic outputs bitwise; copying in place keeps the module on its
load-time allocation across apply and restore cycles.
"""
target = self.bias if bias else self.weight
new_weight = new_weight.to(device)
if isinstance(target, torch.nn.Parameter) and target.shape == new_weight.shape and target.dtype == new_weight.dtype and target.device == new_weight.device:
target.data.copy_(new_weight)
elif bias:
self.bias = torch.nn.Parameter(new_weight, requires_grad=False)
else:
self.weight = torch.nn.Parameter(new_weight, requires_grad=False)
def network_add_weights(self: torch.nn.Conv2d | torch.nn.Linear | torch.nn.GroupNorm | torch.nn.LayerNorm | diffusers.models.lora.LoRACompatibleLinear | diffusers.models.lora.LoRACompatibleConv, model_weights: torch.Tensor | None = None, lora_weights: torch.Tensor = None, deactivate: bool = False, device: torch.device = None, bias: bool = False) -> bool:
"""Add a delta onto the module's weight or bias; False when nothing was written."""
if lora_weights is None:
return False
if deactivate:
lora_weights *= -1
if model_weights is None: # weights are used if provided-from-backup else use the live tensor the delta targets
model_weights = self.bias if bias else self.weight
weight, new_weight = None, None
written = True
if not bias and hasattr(self, "sdnq_dequantizer"):
try:
from sdnq import SDNQConfig, sdnq_quantize_layer
if hasattr(self, "sdnq_dequantizer_backup"):
use_svd = bool(self.sdnq_svd_up_backup is not None)
dequantize_fp32 = bool(self.sdnq_scale_backup.dtype == torch.float32)
sdnq_dequantizer = self.sdnq_dequantizer_backup
dequant_weight = self.sdnq_dequantizer_backup(
model_weights.to(devices.device),
self.sdnq_scale_backup.to(devices.device),
self.sdnq_zero_point_backup.to(devices.device) if self.sdnq_zero_point_backup is not None else None,
self.sdnq_svd_up_backup.to(devices.device) if use_svd else None,
self.sdnq_svd_down_backup.to(devices.device) if use_svd else None,
skip_quantized_matmul=self.sdnq_dequantizer_backup.use_quantized_matmul,
dtype=torch.float32,
)
else:
use_svd = bool(self.svd_up is not None)
dequantize_fp32 = bool(self.scale.dtype == torch.float32)
sdnq_dequantizer = self.sdnq_dequantizer
dequant_weight = self.sdnq_dequantizer(
model_weights.to(devices.device),
self.scale.to(devices.device),
self.zero_point.to(devices.device) if self.zero_point is not None else None,
self.svd_up.to(devices.device) if use_svd else None,
self.svd_down.to(devices.device) if use_svd else None,
skip_quantized_matmul=self.sdnq_dequantizer.use_quantized_matmul,
dtype=torch.float32,
)
new_weight = dequant_weight.to(devices.device, dtype=torch.float32) + lora_weights.to(devices.device, dtype=torch.float32)
self.weight = torch.nn.Parameter(new_weight, requires_grad=False)
self.sdnq_dequantizer = self.scale = self.zero_point = self.svd_up = self.svd_down = None
self = sdnq_quantize_layer(
self,
SDNQConfig(
weights_dtype=sdnq_dequantizer.weights_dtype,
quantized_matmul_dtype=sdnq_dequantizer.quantized_matmul_dtype,
group_size=sdnq_dequantizer.group_size,
hadamard_group_size=sdnq_dequantizer.hadamard_group_size,
svd_rank=sdnq_dequantizer.svd_rank,
use_quantized_matmul=sdnq_dequantizer.use_quantized_matmul,
use_quantized_matmul_conv=sdnq_dequantizer.use_quantized_matmul,
use_svd=use_svd,
use_hadamard=sdnq_dequantizer.use_hadamard,
use_codebook=sdnq_dequantizer.use_codebook,
dequantize_fp32=dequantize_fp32,
svd_steps=shared.opts.sdnq_svd_steps,
codebook_steps=shared.opts.sdnq_codebook_steps,
quant_conv=True, # quant_conv is True if conv layers ends up here
non_blocking=False,
quantization_device=devices.device,
return_device=device,
),
torch_dtype=sdnq_dequantizer.result_dtype,
param_name=getattr(self, 'network_layer_name', None),
)[0].to(device)
weight = None
del dequant_weight
except Exception as e:
log.error(f'Network load: type=LoRA quant=sdnq cls={self.__class__.__name__} weight={self.weight} lora_weights={lora_weights} {e}')
written = False
else:
try:
new_weight = model_weights.to(devices.device) + lora_weights.to(devices.device)
except Exception as e:
log.warning(f'Network load: {e}')
if 'The size of tensor' in str(e):
target = 'bias' if bias else 'weight'
log.error(f'Network load: type=LoRA model={shared.sd_model.__class__.__name__} layer="{getattr(self, "network_layer_name", None)}" target={target} shape={tuple(model_weights.shape)} lora={tuple(lora_weights.shape)} incompatible lora shape')
new_weight = model_weights
written = False
else:
new_weight = model_weights + lora_weights # try without device cast
assign_weight(self, new_weight, device, bias=bias)
del model_weights, lora_weights, new_weight, weight # required to avoid memory leak
return written
def network_apply_direct(self: torch.nn.Conv2d | torch.nn.Linear | torch.nn.GroupNorm | torch.nn.LayerNorm | diffusers.models.lora.LoRACompatibleLinear | diffusers.models.lora.LoRACompatibleConv, updown: torch.Tensor, ex_bias: torch.Tensor, deactivate: bool = False, device: torch.device = devices.device) -> tuple[bool, bool]:
"""Add the deltas onto the live tensors; returns which of (weight, bias) was written."""
weights_backup = getattr(self, "network_weights_backup", False)
bias_backup = getattr(self, "network_bias_backup", False)
if not isinstance(weights_backup, bool): # remove previous backup if we switched settings
weights_backup = True
if not isinstance(bias_backup, bool):
bias_backup = True
if not weights_backup and not bias_backup:
return False, False
t0 = time.time()
weight_written, bias_written = False, False
if weights_backup:
if updown is not None and len(self.weight.shape) == 4 and self.weight.shape[1] == 9: # inpainting model so zero pad updown to make channel 4 to 9
updown = torch.nn.functional.pad(updown, (0, 0, 0, 0, 0, 5)) # pylint: disable=not-callable
if updown is not None:
weight_written = network_add_weights(self, lora_weights=updown, deactivate=deactivate, device=device, bias=False)
if bias_backup:
if ex_bias is not None:
bias_written = network_add_weights(self, lora_weights=ex_bias, deactivate=deactivate, device=device, bias=True)
if hasattr(self, "qweight") and hasattr(self, "freeze"):
self.freeze()
l.timer.apply += time.time() - t0
return weight_written, bias_written
def network_apply_weights(self: torch.nn.Conv2d | torch.nn.Linear | torch.nn.GroupNorm | torch.nn.LayerNorm | diffusers.models.lora.LoRACompatibleLinear | diffusers.models.lora.LoRACompatibleConv, updown: torch.Tensor, ex_bias: torch.Tensor, device: torch.device, deactivate: bool = False) -> tuple[bool, bool]:
"""Add the deltas onto the backup copies; returns which of (weight, bias) was written."""
weights_backup = getattr(self, "network_weights_backup", None)
bias_backup = getattr(self, "network_bias_backup", None)
if weights_backup is None and bias_backup is None:
return False, False
t0 = time.time()
weight_written, bias_written = False, False
if weights_backup is not None and not isinstance(weights_backup, bool):
if updown is not None and len(weights_backup.shape) == 4 and weights_backup.shape[1] == 9: # inpainting model. zero pad updown to make channel[1] 4 to 9
updown = torch.nn.functional.pad(updown, (0, 0, 0, 0, 0, 5)) # pylint: disable=not-callable
if updown is not None:
weight_written = network_add_weights(self, model_weights=weights_backup, lora_weights=updown, deactivate=deactivate, device=device, bias=False)
else:
assign_weight(self, weights_backup, device)
if hasattr(self, "sdnq_dequantizer_backup"):
self.sdnq_dequantizer = self.sdnq_dequantizer_backup
self.scale = torch.nn.Parameter(self.sdnq_scale_backup.to(device), requires_grad=False)
if self.sdnq_zero_point_backup is not None:
self.zero_point = torch.nn.Parameter(self.sdnq_zero_point_backup.to(device), requires_grad=False)
else:
self.zero_point = None
if self.sdnq_svd_up_backup is not None:
self.svd_up = torch.nn.Parameter(self.sdnq_svd_up_backup.to(device), requires_grad=False)
self.svd_down = torch.nn.Parameter(self.sdnq_svd_down_backup.to(device), requires_grad=False)
else:
self.svd_up, self.svd_down = None, None
# del self.sdnq_dequantizer_backup, self.sdnq_scale_backup, self.sdnq_zero_point_backup, self.sdnq_svd_up_backup, self.sdnq_svd_down_backup
if bias_backup is not None and not isinstance(bias_backup, bool):
if ex_bias is not None:
bias_written = network_add_weights(self, model_weights=bias_backup, lora_weights=ex_bias, deactivate=deactivate, device=device, bias=True)
else:
assign_weight(self, bias_backup, device, bias=True)
if hasattr(self, "qweight") and hasattr(self, "freeze"):
self.freeze()
l.timer.apply += time.time() - t0
return weight_written, bias_written