fix(lora): harden the sdnq factor path

- restore stashed svd factors onto the layer's current device; the
  stash tuple does not follow module device moves, so an offload
  between apply and remove left restored factors on a stale device
- recheck factor shapes for layers already in factor mode, so a
  malformed stacked network downgrades to the legacy path instead of
  raising in the concat
- clear the fallback log at activate entry so a raise mid-pass cannot
  leak stale entries into the next report
- pin both behaviors in the suite and state the compute-dtype fidelity
  floor in the module docstring
This commit is contained in:
CalamitousFelicitousness
2026-07-17 11:48:03 +01:00
parent 0b606b8b2a
commit b5c58151be
3 changed files with 65 additions and 3 deletions
+12 -3
View File
@@ -16,7 +16,10 @@ extra columns of ``svd_up`` and rows of ``svd_down``; because the Hadamard
rotation is block-diagonal, symmetric and self-inverse, storing ``A·H`` for
the down factor makes the round trip exact: ``(B @ (A·H)) · H = B @ A``.
Quantized weights are never touched, so apply and remove are exact and no
weight backup is needed.
weight backup is needed. The side-channel storage is lossless; realized
fidelity floors at the compute dtype, because the dequantizer materializes
``base + factors`` in the result dtype and a delta below its ULP of the
base rounds exactly as it would on an unquantized model of that dtype.
Only additive low-rank modules qualify (plain LoRA: no DoRA, no CP ``mid``,
no LyCORIS dense-bias, no ``diff_b``). Layers with any non-factorable
@@ -33,7 +36,7 @@ from modules.logger import log
fallback_layers: list[str] = []
def get_module_factors(module, device, dtype):
def get_module_factors(module, device, dtype, original_shape=None):
"""Return ``(up_eff, down)`` reproducing ``calc_updown`` exactly, or None.
``updown = up @ down * calc_scale() * multiplier()`` for a plain linear
@@ -50,6 +53,8 @@ def get_module_factors(module, device, dtype):
down = module.down_model.weight
if up.ndim != 2 or down.ndim != 2:
return None
if original_shape is not None and (up.shape[0] != original_shape[0] or down.shape[1] != original_shape[-1]):
return None # factor_candidate skips shape checks for layers already in factor mode; recheck here so a malformed stack falls back instead of raising in cat
dyn_dim = module.network.dyn_dim
if dyn_dim is not None and up.shape[1] != dyn_dim:
up = up[:, :dyn_dim]
@@ -96,6 +101,10 @@ def remove_factors(self):
if stash is None:
return False
svd_up, svd_down = stash
device = self.scale.device # the stash tuple does not follow module device moves; restore onto wherever the layer lives now
if svd_up is not None and svd_up.device != device:
svd_up = torch.nn.Parameter(svd_up.to(device=device), requires_grad=False)
svd_down = torch.nn.Parameter(svd_down.to(device=device), requires_grad=False)
self.svd_up = svd_up
self.svd_down = svd_down
del self.sdnq_lora_svd_stash
@@ -125,7 +134,7 @@ def apply_factors(self, network_layer_name, wanted_names, use_previous=False):
module = net.modules.get(network_layer_name, None)
if module is None:
continue
factors = get_module_factors(module, devices.device, dtype)
factors = get_module_factors(module, devices.device, dtype, original_shape=deq.original_shape)
if factors is None:
return None
up_eff, down = factors
+1
View File
@@ -90,6 +90,7 @@ def network_activate(include=None, exclude=None):
with devices.inference_context(), pbar:
wanted_names = tuple((x.name, x.te_multiplier, x.unet_multiplier, x.dyn_dim) for x in l.loaded_networks) if len(l.loaded_networks) > 0 else ()
applied_layers.clear()
lora_sdnq.fallback_layers.clear() # a raise mid-pass leaves stale entries behind
backup_size = 0
for component in modules.keys():
component_wanted = wanted_names if component in components else ()