Merge pull request #5066 from vladmandic/lora-exact

merge lora-exact into dev
This commit is contained in:
Vladimir Mandic
2026-08-26 15:59:00 +02:00
committed by GitHub
12 changed files with 2111 additions and 42 deletions
+483
View File
@@ -0,0 +1,483 @@
#!/usr/bin/env python
"""LoRA fidelity analyzer for quantized base models.
Measures, in weight space, how faithfully a LoRA lands on an SDNQ-quantized
model. Every targeted module is rebuilt with the loader's own module class and
its delta taken from the production ``calc_updown``, so all adapter families
(LoRA, LoKR, LoHA, OFT, full, IA3, GLoRA, norm, plus DoRA and bias variants)
are measured as they would actually apply:
- factor path (plain additive LoRA riding the svd side-channel): storage is
lossless; the reported figure is the delta realized through the result-dtype
materialize, the same bf16 rounding an unquantized model applies.
Eligibility is decided by the loader's own predicate.
- hosted path (non-factorable families on sub-8-bit formats): the seeded svd
truncation at ``--host-rank``, realized the same way.
- requantize path (all other fallbacks): retention ``rho`` of the intended
delta. On-grid rounding erases sub-step deltas down to a ``2/group_size``
floor, so low-bit formats (<=6 bits) typically show rho ~= 0.02-0.03.
- unquantized modules: the LoRA applies exactly regardless.
Reported fidelity is per-module ``applied_rho`` (the measured figure for
whichever path the loader would take), summarized as a median and an
energy-weighted mean over the file's modules; ``requant_rho`` always carries
the if-merged figure.
Works offline against a pre-quantized SDNQ repo (stored tensors + config) or
a bf16 repo with simulated quantization settings, so a combination can be
assessed before committing to a quantized checkpoint.
Examples:
python cli/lora-quant-fidelity.py --model vladmandic/Krea-2-Base-sdnq-hadamard-uint4 --arch krea2 --lora "~/models/Lora/Krea 2/krea2_turbo_distill_r256.safetensors"
python cli/lora-quant-fidelity.py --model CalamitousFelicitousness/Krea-2-Base-Diffusers --arch krea2 --dtype uint4 --lora lora.safetensors --json report.json
"""
import os
import sys
import json
import argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault('SD_INSTALL_QUIET', '1')
def parse_cli():
parser = argparse.ArgumentParser(description='lora-quant-fidelity')
parser.add_argument('--model', required=True, help='model dir, transformer dir, or org/name repo id')
parser.add_argument('--arch', default='generic', help='lora key resolver: a native arch (e.g. krea2, zimage, f2) or generic')
parser.add_argument('--lora', required=True, nargs='+', help='lora safetensors file(s)')
parser.add_argument('--dtype', default=None, help='simulate quantization of a bf16 repo at this sdnq dtype (e.g. uint4, int8); bf16 measures the unquantized reference')
parser.add_argument('--group', type=int, default=0, help='sdnq group_size for simulation')
parser.add_argument('--hadamard-group', type=int, default=256, help='sdnq hadamard group for simulation')
parser.add_argument('--sample', type=int, default=40, help='max modules analyzed per lora (evenly sampled)')
parser.add_argument('--full', action='store_true', help='analyze every matched module')
parser.add_argument('--json', default=None, help='write full report to this json file')
parser.add_argument('--host-rank', type=int, default=256, help='svd hosting cap for non-factorable modules on sub-8-bit formats, mirroring lora_sdnq_host_rank; 0 scores the requantize path instead')
parser.add_argument('--calib', default=None, help='activation statistics file (models/calibration/*.safetensors): hosting truncation is then channel-weighted as with lora_sdnq_host_calib, and hosted rho is measured in the activation-weighted norm (the output-error proxy)')
parser.add_argument('--fail-under', type=float, default=None, help='exit 2 when median applied fidelity of any lora is below this')
return parser.parse_args()
cli_args = parse_cli()
sys.argv = [sys.argv[0]] # sdnext arg parsing during imports must not see tool args (prefix matching eats --model/--lora)
import modules.cmd_args # pylint: disable=wrong-import-position
import installer # pylint: disable=wrong-import-position
modules.cmd_args.parse_args()
installer.add_args(modules.cmd_args.parser)
modules.cmd_args.parsed, _unknown = modules.cmd_args.parser.parse_known_args([])
import torch # pylint: disable=wrong-import-position
from safetensors import safe_open # pylint: disable=wrong-import-position
from rich import print as rprint # pylint: disable=wrong-import-position
from modules.lora import native_adapter, network, network_lora, network_lokr, network_hada, network_oft, network_full, network_ia3, network_glora, network_norm, lora_sdnq # pylint: disable=wrong-import-position
from modules.lora.lora_load import NATIVE_DISPATCH # pylint: disable=wrong-import-position
from sdnq.quantizer import sdnq_quantize_layer_weight # pylint: disable=wrong-import-position
from sdnq.quant_utils import rotate_hadamard # pylint: disable=wrong-import-position
MODEL_ROOTS = [
os.path.expanduser('~/database/models/huggingface'),
os.path.expanduser('~/database/models/Diffusers'),
]
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# every adapter family the native loader can build, with the module class that owns its
# apply-time math. deltas are taken from the production calc_updown so the tool cannot
# drift from the loader, and eligibility is decided by the production predicate itself.
FAMILY_SPECS = (
('lora', network_lora.NetworkModuleLora, native_adapter.LORA_SUFFIXES, native_adapter.LORA_MARKERS),
('lokr', network_lokr.NetworkModuleLokr, native_adapter.LOKR_SUFFIXES, native_adapter.LOKR_MARKERS),
('loha', network_hada.NetworkModuleHada, native_adapter.LOHA_SUFFIXES, native_adapter.LOHA_MARKERS),
('oft', network_oft.NetworkModuleOFT, native_adapter.OFT_SUFFIXES, native_adapter.OFT_MARKERS),
('full', network_full.NetworkModuleFull, native_adapter.FULL_SUFFIXES, native_adapter.FULL_MARKERS),
('ia3', network_ia3.NetworkModuleIa3, native_adapter.IA3_SUFFIXES, native_adapter.IA3_MARKERS),
('glora', network_glora.NetworkModuleGLora, native_adapter.GLORA_SUFFIXES, native_adapter.GLORA_MARKERS),
('norm', network_norm.NetworkModuleNorm, native_adapter.NORM_SUFFIXES, native_adapter.NORM_MARKERS),
)
class StubOnDisk:
def __init__(self, path):
self.filename = path
self.name = os.path.splitext(os.path.basename(path))[0]
self.shorthash = ''
self.sd_version = 'unknown'
def resolve_model_dir(spec):
"""Return the transformer directory for a local path or org/name repo id."""
candidates = [spec, os.path.join(spec, 'transformer')]
cache_name = 'models--' + spec.replace('/', '--')
for root in MODEL_ROOTS:
snap_root = os.path.join(root, cache_name, 'snapshots')
if os.path.isdir(snap_root):
for snap in sorted(os.listdir(snap_root), reverse=True):
candidates.append(os.path.join(snap_root, snap, 'transformer'))
candidates.append(os.path.join(snap_root, snap))
for c in candidates:
if os.path.isfile(os.path.join(c, 'config.json')):
return c
raise SystemExit(f'model not found: {spec}')
def resolve_arch(name):
"""Return the arch lora module for key resolution, or None for generic matching."""
if name == 'generic':
return None
path = NATIVE_DISPATCH.get({'flux2': 'f2', 'ernie': 'ernieimage'}.get(name, name))
if path is None:
raise SystemExit(f'unknown arch {name}; choices: {sorted(NATIVE_DISPATCH)} or generic')
import importlib
return importlib.import_module(path)
def map_lora_modules(lora_path, arch_mod):
"""Return {model_module_path: (family, weights)} across every adapter family, plus a census.
Grouping mirrors the native loader: a family is only considered when its
marker is present, and groups resolve to model paths through the arch's own
resolver. Fused-split chunks are counted but not analyzed (their apply-time
math is arch-owned).
"""
with safe_open(lora_path, framework='pt', device='cpu') as f:
state_dict = {k: f.get_tensor(k) for k in f.keys()}
prefixes = getattr(arch_mod, 'KNOWN_PREFIXES', native_adapter.KNOWN_PREFIXES_DEFAULT)
bare = getattr(arch_mod, 'BARE_DIFFUSERS_PREFIXES', ())
resolve = getattr(arch_mod, 'resolve_targets', None) or (lambda prefix, base: [(base, None)])
mapped, census, chunked = {}, {}, 0
for fam, _cls, suffixes, markers in FAMILY_SPECS:
if not native_adapter.has_marker(state_dict, markers):
continue
groups = native_adapter.group_by_suffixes(state_dict, suffixes, prefixes=prefixes, bare_diffusers_prefixes=bare)
if fam == 'lora':
groups = {k: w for k, w in groups.items() if 'lora_down.weight' in w and 'lora_up.weight' in w}
else:
groups = {k: w for k, w in groups.items() if native_adapter.has_marker({f'x.{s}': None for s in w}, markers)}
if not groups:
continue
census[fam] = len(groups)
for (prefix, base), w in groups.items():
for path, chunk in native_adapter.resolve_group_targets(resolve, prefix, base):
if chunk is not None:
chunked += 1
continue
mapped.setdefault(path, []).append((fam, w)) # a module can carry several families; the loader applies each
return mapped, census, chunked
def stamp_index(paths):
"""Map each module path to its stamped form, the way the loader matches.
The loader compares ``network_prefix + path.replace('.', '_')`` against each
module's stamped ``network_layer_name``, so kohya-style ``lora_unet_`` keys
(whose base arrives already underscored) resolve fine there. Matching on the
stamped form reproduces that and keeps dotted bases working unchanged.
"""
return {p.replace('.', '_'): p for p in paths}
def make_stub(shape, dtype=torch.bfloat16):
"""Minimal sd_module standing in for a bf16 repo weight: the module classes key off its type and shape."""
if len(shape) == 2:
return torch.nn.Linear(shape[1], shape[0], bias=False, dtype=dtype, device='meta')
return torch.nn.Conv2d(shape[1], shape[0], shape[2:], bias=False, dtype=dtype, device='meta')
def build_module(fam, path, w, net, sd_module):
"""Instantiate the family's production NetworkModule for one target."""
cls = next(c for f, c, _s, _m in FAMILY_SPECS if f == fam)
weights = network.NetworkWeights(network_key=path, sd_key=path, w=w, sd_module=sd_module)
return cls(net, weights)
def resolve_transformer_cls(arch, class_name):
"""Prefer an sdnext-owned transformer class over the upstream diffusers one.
Arches like krea2 keep checkpoint-style module names in their own class;
the diffusers class of the same name expects diffusers-style keys and
cannot load these state dicts.
"""
if arch and class_name:
try:
import importlib
pkg = importlib.import_module(f'pipelines.{ {"zimage": "z_image", "f2": "flux"}.get(arch, arch) }')
for attr in dir(pkg):
if attr.endswith('_SPEC'):
cls = getattr(getattr(pkg, attr), 'cls', None)
if cls is not None and cls.__name__ == class_name:
return cls
except Exception:
pass
return None
def load_quantized_model(model_dir, arch=None, class_name=None):
from sdnq.loader import load_sdnq_model
model = load_sdnq_model(model_dir, model_cls=resolve_transformer_cls(arch, class_name), dtype=torch.bfloat16, device='cpu')
layers = {}
for name, module in model.named_modules():
if getattr(module, 'sdnq_dequantizer', None) is not None:
layers[name] = module
elif module.__class__.__name__ == 'Linear' and getattr(module, 'weight', None) is not None:
layers[name] = module
del model # layer modules own their tensors; the dict keeps them alive
return layers
class Bf16Repo:
"""Lazy per-module weight access for a sharded bf16 transformer repo."""
def __init__(self, model_dir):
self.model_dir = model_dir
self.handles = {} # reopening a multi-gb shard per module dominates runtime over many loras
index = os.path.join(model_dir, 'diffusion_pytorch_model.safetensors.index.json')
if os.path.isfile(index):
with open(index, encoding='utf-8') as f:
self.weight_map = json.load(f)['weight_map']
else:
single = os.path.join(model_dir, 'diffusion_pytorch_model.safetensors')
with safe_open(single, framework='pt', device='cpu') as f:
self.weight_map = dict.fromkeys(f.keys(), 'diffusion_pytorch_model.safetensors')
def get(self, key):
shard = self.weight_map.get(key)
if shard is None:
return None
f = self.handles.get(shard)
if f is None:
f = safe_open(os.path.join(self.model_dir, shard), framework='pt', device='cpu')
self.handles[shard] = f
return f.get_tensor(key)
def analyze_module(W_dq, deq_params, mods, calib_rms=None):
"""Return fidelity metrics for one quantized module and the adapters targeting it.
Deltas come from each module's production calc_updown and sum the way the
loader stacks them, so every family (and dora / dense-bias / diff_b variant)
is measured as applied. A module is factor-path eligible only when every
contribution is a plain additive lora. With ``calib_rms``, hosting mirrors
the calibrated production path and its rho is scored in the weighted norm.
"""
D = None
for mod in mods:
d = mod.calc_updown(W_dq)[0].to(device, torch.float32).reshape(W_dq.shape)
D = d if D is None else D + d
nD = D.norm()
control = deq_params['weights_dtype'] == 'bf16' # unquantized reference: the delta just rounds into bf16
factor_eligible = (not control) and all(lora_sdnq.get_module_factors(m, device, torch.bfloat16) is not None for m in mods)
if float(nD) == 0.0: # an all-zero delta (some full-rank extractions carry empty .diff): retention is undefined, not erased
return dict(rank=getattr(mods[0], 'dim', None), rms_delta=0.0, rms_weight=float(W_dq.pow(2).mean().sqrt()),
step_ratio=None, crossers=None, requant_rho=None, requant_resid=None,
factor_eligible=factor_eligible, hosted=False, applied_rho=None, delta_energy=0.0)
step_ratio, crossers = None, None
if control:
W2 = (W_dq + D).to(torch.bfloat16).float()
else:
# mirror network_add_weights: it requantizes with the layer's own svd setting and rank,
# and an svd checkpoint's dequantized weight is not on the plain integer grid
use_svd = deq_params.get('use_svd', False)
kw = dict(layer_class_name='Linear', torch_dtype=torch.bfloat16, group_size=deq_params['group_size'],
hadamard_group_size=deq_params['hadamard_group_size'], use_hadamard=deq_params['use_hadamard'],
weights_dtype=deq_params['weights_dtype'], use_svd=use_svd, svd_rank=deq_params.get('svd_rank', 32),
svd_steps=deq_params.get('svd_steps', 8), use_quantized_matmul=False, dequantize_fp32=False)
deq2, data2 = sdnq_quantize_layer_weight(W_dq + D, **kw)
W2 = deq2(data2['weight'], data2['scale'], zero_point=data2['zero_point'],
svd_up=data2['svd_up'], svd_down=data2['svd_down'], dtype=torch.float32, skip_compile=True)
Dh = rotate_hadamard(D, group_size=deq_params['hadamard_group_size']) if deq_params['use_hadamard'] else D
step = data2['scale'].float()
Dg = Dh.unflatten(-1, (step.shape[1], -1)) if step.ndim == 3 else Dh
step_ratio = float((Dg.abs() / step).mean())
crossers = float((Dg.abs() > step / 2).float().mean())
E = W2 - W_dq
rho = float(E.flatten() @ D.flatten() / nD.square())
resid = float((E - D).norm() / nD)
hosted = False
if factor_eligible:
# the factor path stores the delta losslessly, but the dequantizer materializes
# base + factors in the result dtype (bf16 here), so realized fidelity floors at
# the same ULP rounding an unquantized bf16 model applies to a merged delta
base16 = W_dq.to(torch.bfloat16).float()
realized = (W_dq.to(torch.bfloat16) + D.to(torch.bfloat16)).float() - base16
applied_rho = float(realized.flatten() @ D.flatten() / nD.square())
else:
applied_rho = rho
if (not control) and cli_args.host_rank > 0:
from sdnq.common import dtype_dict
if dtype_dict[deq_params['weights_dtype']]['num_bits'] < 8:
# mirror lora_sdnq.apply_hosted: seeded svd truncation, realized through the bf16 materialize
q = min(cli_args.host_rank, *D.shape)
rms = None
if calib_rms is not None and calib_rms.shape[-1] == D.shape[-1]:
rms = calib_rms.to(D.device, torch.float32).clamp(min=1e-8)
Dw = D * rms if rms is not None else D
with torch.random.fork_rng(devices=[D.device] if D.device.type == 'cuda' else []):
torch.manual_seed(0)
U, S, V = torch.svd_lowrank(Dw, q=q, niter=2)
Dk = (U * S) @ V.t()
if rms is not None:
Dk = Dk / rms
base16 = W_dq.to(torch.bfloat16).float()
realized = (W_dq.to(torch.bfloat16) + Dk.to(torch.bfloat16)).float() - base16
if rms is not None: # weighted norm: the diagonal-covariance output-error proxy the calibrated truncation optimizes
Dr = D * rms
applied_rho = float((realized * rms).flatten() @ Dr.flatten() / Dr.square().sum())
else:
applied_rho = float(realized.flatten() @ D.flatten() / nD.square())
hosted = True
return dict(rank=getattr(mods[0], 'dim', None), rms_delta=float(D.pow(2).mean().sqrt()), rms_weight=float(W_dq.pow(2).mean().sqrt()),
step_ratio=step_ratio, crossers=crossers, requant_rho=rho, requant_resid=resid,
factor_eligible=factor_eligible, hosted=hosted, applied_rho=applied_rho,
delta_energy=float(nD.square()))
def main():
args = cli_args
model_dir = resolve_model_dir(args.model)
arch_mod = resolve_arch(args.arch)
with open(os.path.join(model_dir, 'config.json'), encoding='utf-8') as f:
model_config = json.load(f)
pre_quantized = model_config.get('quantization_config') is not None
quant_layers, bf16_repo = {}, None
quant_stamps, bf16_stamps = {}, {}
if pre_quantized:
rprint(f'model: "{model_dir}" pre-quantized={pre_quantized}')
quant_layers = load_quantized_model(model_dir, arch=args.arch, class_name=model_config.get('_class_name'))
quant_stamps = stamp_index(quant_layers)
else:
bf16_repo = Bf16Repo(model_dir)
bf16_stamps = stamp_index(k[:-len('.weight')] for k in bf16_repo.weight_map if k.endswith('.weight'))
if args.dtype is None:
rprint('model is not quantized and no --dtype given: loras apply exactly, nothing to analyze')
return 0
rprint(f'model: "{model_dir}" simulating dtype={args.dtype} group={args.group} hadamard={args.hadamard_group}')
calib_stats = {}
if args.calib:
with safe_open(os.path.expanduser(args.calib), framework='pt', device='cpu') as f:
calib_stats = {k: f.get_tensor(k) for k in f.keys()}
rprint(f'calib: "{args.calib}" layers={len(calib_stats)}')
report = {'model': model_dir, 'pre_quantized': pre_quantized, 'loras': []}
worst_effective = 1.0
def write_report():
if args.json:
with open(args.json, 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2)
for lora_path in args.lora:
lora_path = os.path.expanduser(lora_path)
try:
mapped, census, chunked = map_lora_modules(lora_path, arch_mod)
net = network.Network(os.path.basename(lora_path), StubOnDisk(lora_path))
rows, unquantized, unmatched, failed, non_matrix = [], [], [], [], []
keys = sorted(mapped)
if not args.full and len(keys) > args.sample:
keys = keys[::max(1, len(keys) // args.sample)][:args.sample]
for path in keys:
entries = mapped[path]
lname = path
if pre_quantized:
if path not in quant_layers:
lname = quant_stamps.get(path.replace('.', '_'), '')
layer = quant_layers.get(lname)
if layer is None:
unmatched.append(path)
continue
deq = getattr(layer, 'sdnq_dequantizer', None)
if deq is None:
unquantized.append(path)
continue
if len(deq.original_shape) != 2:
non_matrix.append(path)
continue
W_dq = deq(layer.weight, layer.scale, zero_point=layer.zero_point, svd_up=layer.svd_up, svd_down=layer.svd_down,
skip_quantized_matmul=deq.use_quantized_matmul, dtype=torch.float32, skip_compile=True).to(device)
params = dict(weights_dtype=deq.weights_dtype, group_size=deq.group_size, hadamard_group_size=deq.hadamard_group_size,
use_hadamard=deq.use_hadamard, use_svd=layer.svd_up is not None, svd_rank=deq.svd_rank, svd_steps=deq.svd_steps)
sd_module = layer
else:
W = bf16_repo.get(f'{path}.weight')
if W is None:
W = bf16_repo.get(f'{bf16_stamps.get(path.replace(".", "_"), "")}.weight')
if W is None:
unmatched.append(path)
continue
if W.ndim != 2: # norm/scale targets (e.g. adaLN_modulation) are 1-D; the quantizer and the stub both expect a matrix
non_matrix.append(path)
continue
if args.dtype == 'bf16':
W_dq = W.to(device, torch.bfloat16).float()
params = dict(weights_dtype='bf16', group_size=0, hadamard_group_size=0, use_hadamard=False)
else:
deq0, data0 = sdnq_quantize_layer_weight(W.to(device, torch.float32), layer_class_name='Linear', weights_dtype=args.dtype,
group_size=args.group, hadamard_group_size=args.hadamard_group, use_hadamard=args.hadamard_group > 0,
use_svd=False, use_quantized_matmul=False, dequantize_fp32=False, torch_dtype=torch.bfloat16)
W_dq = deq0(data0['weight'], data0['scale'], zero_point=data0['zero_point'], svd_up=None, svd_down=None, dtype=torch.float32, skip_compile=True)
params = dict(weights_dtype=args.dtype, group_size=deq0.group_size, hadamard_group_size=deq0.hadamard_group_size, use_hadamard=deq0.use_hadamard)
sd_module = make_stub(W.shape)
try:
mods = [build_module(fam, path, w, net, sd_module) for fam, w in entries]
row = analyze_module(W_dq, params, mods, calib_rms=calib_stats.get(lname))
except Exception as e: # a family the tool cannot rebuild must not read as a clean module
failed.append(f'{path}: {type(e).__name__}: {e}')
del W_dq
continue
row.update(module=path, dtype=params['weights_dtype'], family='+'.join(f for f, _w in entries))
rows.append(row)
del W_dq # the caching allocator reuses these; emptying it per module costs more than it saves
scored = [r for r in rows if r['applied_rho'] is not None] # zero-delta modules have no retention to report
applied = sorted(r['applied_rho'] for r in scored)
median_applied = applied[len(applied) // 2] if applied else None
energy = sum(r['delta_energy'] for r in scored)
weighted = (sum(r['applied_rho'] * r['delta_energy'] for r in scored) / energy) if energy > 0 else None
n_exact = sum(1 for r in scored if r['factor_eligible'])
fb = [r['requant_rho'] for r in scored if not r['factor_eligible']]
fb_median = sorted(fb)[len(fb) // 2] if fb else None
if median_applied is not None:
worst_effective = min(worst_effective, median_applied)
report['loras'].append({'file': lora_path, 'families': census, 'targets': len(mapped), 'unquantized': unquantized,
'unmatched': unmatched, 'non_matrix': non_matrix, 'chunked': chunked, 'failed': failed,
'exact_modules': n_exact, 'fallback_modules': len(fb), 'fallback_median_rho': fb_median,
'median_applied_rho': median_applied, 'weighted_applied_rho': weighted, 'modules': rows})
write_report() # rewrite per file so a crash keeps completed work
rprint(f'\nlora: "{os.path.basename(lora_path)}" families={census or "none"} targets={len(mapped)} analyzed={len(rows)} scored={len(scored)} exact={n_exact} fallback={len(fb)} unquantized={len(unquantized)} unmatched={len(unmatched)} non_matrix={len(non_matrix)} chunked={chunked} failed={len(failed)}')
if median_applied is None:
rprint(' no analyzable modules: nothing measured')
else:
rprint(f' applied fidelity: median={median_applied:.3f} energy-weighted={weighted:.3f}' + (f' (fallback modules land at median rho={fb_median:.3f})' if fb_median is not None else ''))
for f in failed[:3]:
rprint(f' [red]could not rebuild[/red]: {f}')
if fb:
worst = sorted((r for r in scored if not r['factor_eligible']), key=lambda r: r['requant_rho'])[:5]
rprint(' lowest-retention modules:')
for r in worst:
grid = f'step-ratio={r["step_ratio"]:.3f} crossers={r["crossers"]*100:5.1f}%' if r['step_ratio'] is not None else 'unquantized reference'
rprint(f' {r["module"]:48s} fam={r["family"]:5s} dtype={r["dtype"]} {grid} rho={r["requant_rho"]:.3f}')
del mapped, net
except KeyboardInterrupt:
raise
except Exception as e: # one broken file must not cost the rest of the batch
rprint(f'\n[red]lora failed[/red]: "{os.path.basename(lora_path)}" {type(e).__name__}: {e}')
report['loras'].append({'file': lora_path, 'error': f'{type(e).__name__}: {e}'})
write_report()
if device.type == 'cuda':
torch.cuda.empty_cache() # once per file, after its modules are done
report['complete'] = True
write_report()
if args.json:
rprint(f'\nreport: "{args.json}"')
if args.fail_under is not None and worst_effective < args.fail_under:
rprint(f'FAIL: effective fidelity {worst_effective:.3f} < {args.fail_under}')
return 2
return 0
if __name__ == '__main__':
with torch.inference_mode():
sys.exit(main())
+4 -2
View File
@@ -178,6 +178,8 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork):
return [f'{name}:{te}:{unet}' for name, te, unet in zip(names, te_multipliers, unet_multipliers, strict=False)]
def changed(self, requested: list[str], include: list[str] | None = None, exclude: list[str] | None = None) -> tuple[bool, str]:
from modules.lora import lora_sdnq
requested = requested + [f'stack={lora_sdnq.signature()}'] # settings-only mechanism changes must re-trigger activation
if shared.opts.lora_force_reload:
debug_log(f'Network check: type=LoRA requested={requested} status="forced"')
return True, "forced"
@@ -254,7 +256,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork):
if has_changed:
jobid = shared.state.begin('LoRA')
if len(l.previously_loaded_networks) > 0:
log.info(f'Network unload: type=LoRA networks={[n.name for n in l.previously_loaded_networks]} mode={"fuse" if shared.opts.lora_fuse_native else "backup"}')
log.info(f'Network unload: type=LoRA networks={[n.name for n in l.previously_loaded_networks]} mode={"fuse" if lora_overrides.fuse_native() else "backup"}')
networks.network_deactivate(include, exclude)
networks.network_activate(include, exclude)
debug_log(f'Network change: type=LoRA previous={[n.name for n in l.previously_loaded_networks]} current={[n.name for n in l.loaded_networks]}')
@@ -267,7 +269,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork):
prompt(p)
if has_changed and len(include) == 0: # print only once
actual_method = 'native' if any(len(n.modules) > 0 for n in l.loaded_networks) else load_method
log.info(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} load={load_method}({load_reason}) method={actual_method} mode={"fuse" if shared.opts.lora_fuse_native else "backup"} te={te_multipliers} unet={unet_multipliers} time={l.timer.summary} reason="{reason}"')
log.info(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} load={load_method}({load_reason}) method={actual_method} mode={"fuse" if lora_overrides.fuse_native() else "backup"} te={te_multipliers} unet={unet_multipliers} time={l.timer.summary} reason="{reason}"')
def deactivate(self, p, force=False):
if len(lora_diffusers.diffuser_loaded) > 0 and (shared.opts.lora_force_reload or force):
+4 -4
View File
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
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):
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()
@@ -24,7 +24,7 @@ def network_backup_weights(self: torch.nn.Conv2d | torch.nn.Linear | torch.nn.Gr
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 (shared.opts.lora_fuse_native and not isinstance(weights_backup, bool)) or (not shared.opts.lora_fuse_native and isinstance(weights_backup, bool)): # invalidate so we can change direct/backup on-the-fly
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
@@ -33,7 +33,7 @@ def network_backup_weights(self: torch.nn.Conv2d | torch.nn.Linear | torch.nn.Gr
if weights_backup is None and wanted_names != (): # pylint: disable=C1803
weight = getattr(self, 'weight', None)
self.network_weights_backup = None
if shared.opts.lora_fuse_native:
if fuse:
self.network_weights_backup = True
else:
self.network_weights_backup = weight.clone().to(devices.cpu)
@@ -53,7 +53,7 @@ def network_backup_weights(self: torch.nn.Conv2d | torch.nn.Linear | torch.nn.Gr
if bias_backup is None:
if getattr(self, 'bias', None) is not None:
if shared.opts.lora_fuse_native:
if fuse:
self.network_bias_backup = True
else:
bias_backup = self.bias.clone()
+175
View File
@@ -0,0 +1,175 @@
"""Per-checkpoint activation calibration for svd hosting on quantized layers.
Plain svd truncation of a hosted delta is optimal in weight space but not in
output space: transformer activations concentrate energy in a few input
channels (per-channel RMS spreads by one to three orders of magnitude), so
the directions that matter most for the output are not the largest in
Frobenius norm. Scaling the delta by per-channel input RMS before the svd
and folding the inverse scale into the down factor spends the same rank
budget on output error instead; measured on real checkpoints this raises
output-delta retention by ~0.05 at rank 256 and ~0.09 at rank 64, most on
MLP down projections whose inputs carry the largest outlier channels.
Statistics come from the model's own forwards: when a sub-8-bit SDNQ model
loads and no calibration is cached for it, streaming sum-of-squares hooks
attach to its quantized linears, accumulate during normal generations,
persist once enough tokens are seen, and go inert. Cached statistics load
at model load and sit on each layer as ``sdnq_calib_rms``; the hosting path
reads them through ``rms_for``. Capture is skipped when the model is
compiled (hooks would break the graph) and everything is gated by the
``lora_sdnq_host_calib`` option.
"""
import os
import torch
from modules import paths, shared, script_callbacks
from modules.logger import log
TOKENS_DONE = 65536
calib_root = os.path.join(paths.models_path, 'calibration')
capture = {'model': None, 'recs': {}, 'handles': [], 'complete': False}
def enabled():
return bool(getattr(shared.opts, 'lora_sdnq_host_calib', False))
def calib_file(model_name):
key = model_name.replace('/', '--').replace('\\', '--').replace(':', '-')
return os.path.join(calib_root, f'{key}.safetensors')
def checkpoint_name(sd_model):
info = getattr(sd_model, 'sd_checkpoint_info', None)
return getattr(info, 'name', None)
def eligible_modules(sd_model):
"""Sub-8-bit 2-D SDNQ linears of the model's transformer: the layers hosting applies to."""
transformer = getattr(sd_model, 'transformer', None)
if transformer is None:
return []
from sdnq.common import dtype_dict
out = []
for name, m in transformer.named_modules():
deq = getattr(m, 'sdnq_dequantizer', None)
if deq is None or len(deq.original_shape) != 2:
continue
if dtype_dict[deq.weights_dtype]['num_bits'] >= 8:
continue
out.append((name, m))
return out
def detach_capture():
for h in capture['handles']:
h.remove()
capture['handles'].clear()
capture['recs'].clear()
capture['model'] = None
capture['complete'] = False
def hook_for(rec, in_features):
def hook(module, hook_args): # pylint: disable=unused-argument
if rec['done'] or capture['complete']:
return
x = hook_args[0] if hook_args else None
if not torch.is_tensor(x) or x.shape[-1] != in_features:
return
ss = x.detach().reshape(-1, in_features).float().square().sum(dim=0)
if rec['ss'] is None:
rec['ss'] = ss
else:
if rec['ss'].device != ss.device: # offload moves blocks between devices mid-run
rec['ss'] = rec['ss'].to(ss.device)
rec['ss'] += ss
rec['n'] += x.numel() // in_features
if rec['n'] >= TOKENS_DONE:
rec['done'] = True
if all(r['done'] for r in capture['recs'].values()):
persist()
return hook
def persist():
"""Write completed statistics and stamp them onto the layers.
Runs from the last completing hook, inside a forward; the write is a few
MB once per checkpoint ever. Handles stay registered but inert until the
next safe point removes them (hook removal here would mutate the hook
dict the forward is iterating).
"""
if capture['complete']:
return
capture['complete'] = True
from safetensors.torch import save_file
tensors, min_n = {}, None
for name, rec in capture['recs'].items():
rms = (rec['ss'] / max(rec['n'], 1)).sqrt().float().cpu().contiguous().clone()
tensors[name] = rms
rec['m'].sdnq_calib_rms = rms
min_n = rec['n'] if min_n is None else min(min_n, rec['n'])
path = calib_file(capture['model'])
try:
os.makedirs(calib_root, exist_ok=True)
save_file(tensors, path, metadata={'version': '1', 'model': capture['model'], 'tokens': str(min_n)})
log.info(f'Network calibration: model="{capture["model"]}" layers={len(tensors)} tokens={min_n} saved="{path}"')
except Exception as e:
log.warning(f'Network calibration: save failed path="{path}" {e}')
def maybe_detach():
"""Remove inert hooks once capture finished; safe only outside a model forward."""
if capture['complete'] and capture['handles']:
detach_capture()
def load_stats(model_name, modules_list):
from safetensors import safe_open
path = calib_file(model_name)
loaded = 0
with safe_open(path, framework='pt', device='cpu') as f:
keys = set(f.keys())
for name, m in modules_list:
if name in keys:
m.sdnq_calib_rms = f.get_tensor(name)
loaded += 1
log.info(f'Network calibration: model="{model_name}" layers={loaded} loaded="{path}"')
def on_model_loaded(sd_model):
detach_capture()
if not enabled():
return
name = checkpoint_name(sd_model)
if name is None:
return
modules_list = eligible_modules(sd_model)
if len(modules_list) == 0:
return
if os.path.isfile(calib_file(name)):
load_stats(name, modules_list)
return
if 'Model' in (getattr(shared.opts, 'cuda_compile', None) or []):
return # hooks inside a compiled module graph-break or misbehave; skip capture entirely
capture['model'] = name
for mod_name, m in modules_list:
rec = {'m': m, 'ss': None, 'n': 0, 'done': False}
capture['recs'][mod_name] = rec
capture['handles'].append(m.register_forward_pre_hook(hook_for(rec, int(m.sdnq_dequantizer.original_shape[-1]))))
log.info(f'Network calibration: model="{name}" layers={len(modules_list)} collecting activation statistics')
def rms_for(layer):
"""Per-channel input RMS for a layer, or None when absent or disabled."""
maybe_detach()
if not enabled():
return None
return getattr(layer, 'sdnq_calib_rms', None)
script_callbacks.on_model_loaded(on_model_loaded)
+2 -1
View File
@@ -4,6 +4,7 @@ import diffusers
from modules import shared, errors
from modules.logger import log
from modules.lora import network
from modules.lora import lora_overrides
from modules.lora import lora_common as l
@@ -54,7 +55,7 @@ def load_diffusers(name: str, network_on_disk: network.NetworkOnDisk, lora_scale
t0 = time.time()
name = name.replace(".", "_")
sd_model: diffusers.DiffusionPipeline = getattr(shared.sd_model, "pipe", shared.sd_model)
log.debug(f'Network load: type=LoRA name="{name}" file="{network_on_disk.filename}" detected={network_on_disk.sd_version} method=diffusers reason={reason or "unknown"} scale={lora_scale} fuse={shared.opts.lora_fuse_native}:{shared.opts.lora_fuse_diffusers}')
log.debug(f'Network load: type=LoRA name="{name}" file="{network_on_disk.filename}" detected={network_on_disk.sd_version} method=diffusers reason={reason or "unknown"} scale={lora_scale} fuse={lora_overrides.fuse_native()}:{shared.opts.lora_fuse_diffusers}')
if not hasattr(sd_model, 'load_lora_weights'):
log.error(f'Network load: type=LoRA class={sd_model.__class__} does not implement load lora')
return None
+2 -2
View File
@@ -149,7 +149,7 @@ def load_safetensors(name, network_on_disk: network.NetworkOnDisk) -> network.Ne
if l.debug:
log.debug(f'Network load: type=LoRA name="{name}" unmatched={keys_failed_to_match}')
else:
log.debug(f'Network load: type=LoRA name="{name}" type={set(network_types)} keys={len(matched_networks)} dtypes={dtypes} fuse={shared.opts.lora_fuse_native}:{shared.opts.lora_fuse_diffusers}')
log.debug(f'Network load: type=LoRA name="{name}" type={set(network_types)} keys={len(matched_networks)} dtypes={dtypes} fuse={lora_overrides.fuse_native()}:{shared.opts.lora_fuse_diffusers}')
if len(matched_networks) == 0:
return None
lora_cache[name] = net
@@ -350,7 +350,7 @@ def network_load(names, te_multipliers=None, unet_multipliers=None, dyn_dims=Non
networks.network_activate()
if len(l.loaded_networks) > 0 and l.debug:
log.debug(f'Network load: type=LoRA loaded={[n.name for n in l.loaded_networks]} cache={list(lora_cache)} fuse={shared.opts.lora_fuse_native}:{shared.opts.lora_fuse_diffusers}')
log.debug(f'Network load: type=LoRA loaded={[n.name for n in l.loaded_networks]} cache={list(lora_cache)} fuse={lora_overrides.fuse_native()}:{shared.opts.lora_fuse_diffusers}')
if recompile_model:
log.info("Network load: type=LoRA recompiling model")
+48 -5
View File
@@ -77,13 +77,56 @@ def get_method(shorthash=''):
return 'native', 'default'
# Roles a LoRA is fused into; a quantized component in any of them makes fusing unsafe.
fuse_roots = ('transformer', 'unet', 'text_encoder', 'llm_adapter')
def fuse_components(sd_model):
"""Component names a network fuses into, matched by role prefix so numbered and reference siblings are covered."""
names = getattr(sd_model, 'components', None)
if not isinstance(names, dict):
names = vars(sd_model)
return [name for name in names if name.startswith(fuse_roots)]
def is_quantized(module):
"""Return True when ``module`` carries a quantization config.
``config.quantization_config`` is read first: SDNQ sets both it and the plain
attribute when it quantizes in place, but a checkpoint that ships pre-quantized
only reaches the plain attribute through the diffusers ConfigMixin name proxy,
which is deprecated for removal.
"""
if module is None:
return False
config = getattr(module, 'config', None)
if config is not None and getattr(config, 'quantization_config', None) is not None:
return True
return getattr(module, 'quantization_config', None) is not None
def disable_fuse():
if hasattr(shared.sd_model, 'quantization_config'):
"""Return True when fusing a network into model weights is unsafe.
Fusing keeps no pristine copy of the weight, so each apply and restore
round-trips it through its storage format. On quantized weights that is a
dequantize-add-requantize cycle per network swap whose error compounds.
"""
sd_model = getattr(shared.sd_model, 'pipe', shared.sd_model)
if is_quantized(sd_model):
return True
if hasattr(shared.sd_model, 'transformer') and hasattr(shared.sd_model.transformer, 'quantization_config'):
if any(is_quantized(getattr(sd_model, name, None)) for name in fuse_components(sd_model)):
return True
if hasattr(shared.sd_model, 'transformer_2') and hasattr(shared.sd_model.transformer_2, 'quantization_config'):
return True
if hasattr(shared.sd_model, '_lora_partial'):
if hasattr(sd_model, '_lora_partial'):
return True
return shared.sd_model_type in fuse_ignore
def fuse_native():
"""Return True when the native apply path may fuse into model weights.
The single source of truth for the native fuse decision: it must agree across
the backup, activate and deactivate passes, since backup mode restores from a
stored tensor while fuse mode restores by subtracting the delta.
"""
return shared.opts.lora_fuse_native and not disable_fuse()
+299
View File
@@ -0,0 +1,299 @@
"""Exact LoRA application for SDNQ-quantized layers.
Baking a LoRA into a quantized weight requantizes it: dequantize, add the
delta, re-round onto the integer grid. When the per-element delta is smaller
than half a quantization step (a rank-decomposed delta on a uint4 layer sits
at a few percent of a step), rounding erases it; what survives is the two
grid-extrema elements per quantization group (2/group_size of the signal)
plus grid-shift noise of the same norm as the delta. The optimal in-grid
representation provably retains ~0%, so no rewrite of the stored integers
can fix this.
The exact path instead rides the SDNQ svd side-channel: the dequantizer
computes ``W = dq(q) + svd_up @ svd_down`` in the rotated domain at full
precision, in every forward mode. A LoRA delta ``B @ A`` is appended as
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. 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 ride the channel exactly (plain LoRA: no
DoRA, no CP ``mid``, no LyCORIS dense-bias, no ``diff_b``). On sub-8-bit
formats, sets with non-factorable contributions are hosted instead: the
families' own ``calc_updown`` delta is truncated to its top singular
directions and appended the same way. Truncation keeps the dominant part
of the effect and drops an orthogonal residual, where requantize keeps
only the grid extrema and adds grid-shift noise of the delta's own
magnitude. When activation statistics for the checkpoint exist (see
``lora_calib``), the truncation is channel-weighted to minimize output
error instead of weight error. At 8 bits and above requantize retains
most of the delta, so hosting is skipped there and the requantize path
remains.
"""
import torch
from modules import devices, shared
from modules.lora import lora_calib
from modules.lora import lora_common as l
from modules.logger import log
fallback_layers: list[str] = []
hosted_layers: list[tuple[str, float, bool]] = []
def enabled():
"""True while the exact svd-channel machinery may take quantized layers; the requantize choice routes every layer to the legacy weight-rewrite path."""
return getattr(shared.opts, 'lora_sdnq_apply', 'exact') != 'requantize'
def signature():
"""Identity suffix for the per-module apply stamp; empty on the default exact mechanism."""
return '' if enabled() else '|quant=requantize'
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
LoRA; the scalars fold into the up factor. ``dyn_dim`` slices ranks the
same way ``lyco_helpers.rebuild_conventional`` does.
"""
if module.__class__.__name__ != 'NetworkModuleLora':
return None
if module.dora_scale is not None or module.bias is not None or module.ex_bias is not None:
return None
if getattr(module, 'mid_model', None) is not None:
return None
up = module.up_model.weight
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]
down = down[:dyn_dim]
scalar = module.calc_scale() * module.multiplier()
up_eff = up.to(device=device, dtype=torch.float32) * scalar
return up_eff.to(dtype=dtype), down.to(device=device, dtype=dtype)
def factor_candidate(self, network_layer_name, wanted_names, use_previous=False):
"""True when this layer should take the exact svd-append path.
Requires an SDNQ linear layer whose active networks all contribute plain
factorable LoRA modules for this layer. An empty ``wanted_names`` is a
removal request and qualifies whenever factors are currently attached.
"""
if not enabled():
return False # declined layers with factors still attached are stripped by the activate fallthrough
if getattr(self, 'sdnq_dequantizer', None) is None or self.__class__.__name__ != 'SDNQLinear':
return False
if hasattr(self, 'sdnq_lora_svd_stash'):
return True
if wanted_names == (): # nothing attached, nothing to remove
return False
loaded = l.loaded_networks if not use_previous else l.previously_loaded_networks
seen = False
for net in loaded:
module = net.modules.get(network_layer_name, None)
if module is None:
continue
seen = True
if module.__class__.__name__ != 'NetworkModuleLora':
return False
if module.dora_scale is not None or module.bias is not None or module.ex_bias is not None or getattr(module, 'mid_model', None) is not None:
return False
if module.up_model.weight.ndim != 2 or module.down_model.weight.ndim != 2:
return False
if module.up_model.weight.shape[0] != self.sdnq_dequantizer.original_shape[0] or module.down_model.weight.shape[1] != self.sdnq_dequantizer.original_shape[-1]:
return False
return seen
def remove_factors(self):
"""Restore the layer's original svd factors; True when factors were attached."""
stash = getattr(self, 'sdnq_lora_svd_stash', None)
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
return True
def apply_factors(self, network_layer_name, wanted_names, use_previous=False):
"""Attach the active networks' LoRA factors to this layer's svd side-channel.
Replaces any previously attached factors (multiplier changes re-enter
here with a new ``wanted_names`` signature). Returns True when the layer
changed. Falls back to the caller's requantize path by returning None
when factor extraction fails at this stage.
"""
from sdnq.quant_utils import rotate_hadamard
changed = remove_factors(self)
if wanted_names == ():
return changed
deq = self.sdnq_dequantizer
dtype = deq.result_dtype
loaded = l.loaded_networks if not use_previous else l.previously_loaded_networks
ups, downs = [], []
for net in loaded:
module = net.modules.get(network_layer_name, None)
if module is None:
continue
factors = get_module_factors(module, devices.device, dtype, original_shape=deq.original_shape)
if factors is None:
return None
up_eff, down = factors
if deq.use_hadamard:
down = rotate_hadamard(down.to(dtype=torch.float32), group_size=deq.hadamard_group_size).to(dtype=dtype)
ups.append(up_eff)
downs.append(down)
if not ups:
return changed
append_factors(self, ups, downs)
return True
def append_factors(self, ups, downs):
"""Concatenate ``[out, r]`` / ``[r, in]`` factor pairs onto the layer's svd channel and stash the originals."""
deq = self.sdnq_dequantizer
device = self.scale.device
dtype = deq.result_dtype
orig_up, orig_down = self.svd_up, self.svd_down
if deq.use_quantized_matmul:
# matmul layout stores factors transposed: svd_up [r, out], svd_down [in, r]
parts_up = ([orig_up.to(device=devices.device, dtype=dtype)] if orig_up is not None else []) + [u.t() for u in ups]
parts_down = ([orig_down.to(device=devices.device, dtype=dtype)] if orig_down is not None else []) + [d.t() for d in downs]
new_up = torch.cat(parts_up, dim=0).contiguous()
new_down = torch.cat(parts_down, dim=1).contiguous()
else:
parts_up = ([orig_up.to(device=devices.device, dtype=dtype)] if orig_up is not None else []) + ups
parts_down = ([orig_down.to(device=devices.device, dtype=dtype)] if orig_down is not None else []) + downs
new_up = torch.cat(parts_up, dim=1).contiguous()
new_down = torch.cat(parts_down, dim=0).contiguous()
self.sdnq_lora_svd_stash = (orig_up, orig_down)
self.svd_up = torch.nn.Parameter(new_up.to(device=device), requires_grad=False)
self.svd_down = torch.nn.Parameter(new_down.to(device=device), requires_grad=False)
def host_candidate(self, network_layer_name, wanted_names, use_previous=False):
"""True when a non-factorable set on this layer should be hosted as a truncated svd."""
if not enabled():
return False
if int(getattr(shared.opts, 'lora_sdnq_host_rank', 0) or 0) <= 0:
return False
if getattr(self, 'sdnq_dequantizer', None) is None or self.__class__.__name__ != 'SDNQLinear':
return False
if wanted_names == ():
return False
from sdnq.common import dtype_dict
if dtype_dict[self.sdnq_dequantizer.weights_dtype]['num_bits'] >= 8:
return False # requantize retains most of the delta at 8 bits and above; truncation would lose more than it saves
loaded = l.loaded_networks if not use_previous else l.previously_loaded_networks
return any(net.modules.get(network_layer_name, None) is not None for net in loaded)
def apply_hosted(self, network_layer_name, updown, wanted_names, use_previous=False):
"""Host a set's delta on the svd channel: exact factors for factorable
members, the top-k singular directions of the remainder for the rest.
The delta comes from the families' own ``calc_updown``, so every family
and scaling quirk is included; factorable members are subtracted out and
appended exactly so they never compete with the hosted remainder for
rank. When per-checkpoint activation statistics exist (``lora_calib``),
input channels are weighted by their RMS before truncation so the kept
directions minimize output error rather than weight error. Returns None
when the delta cannot ride the channel (wrong shape); the caller falls
back to requantize.
"""
from sdnq.quant_utils import rotate_hadamard
deq = self.sdnq_dequantizer
changed = remove_factors(self)
if wanted_names == ():
return changed
if updown is None or updown.ndim != 2 or tuple(updown.shape) != tuple(deq.original_shape):
return None
dtype = deq.result_dtype
D = updown.detach().to(devices.device, torch.float32)
ups, downs = [], []
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
factors = get_module_factors(module, devices.device, dtype, original_shape=deq.original_shape)
if factors is None:
continue
up_eff, down = factors
D = D.sub_(up_eff.to(torch.float32) @ down.to(torch.float32)) # factorable members ride exactly; host only the remainder
if deq.use_hadamard:
down = rotate_hadamard(down.to(dtype=torch.float32), group_size=deq.hadamard_group_size).to(dtype=dtype)
ups.append(up_eff)
downs.append(down)
cap = int(shared.opts.lora_sdnq_host_rank)
q = min(cap, *D.shape)
rms = lora_calib.rms_for(self)
if rms is not None and rms.shape[-1] == D.shape[-1]:
# scale input channels by their activation RMS so truncation minimizes output error rather than weight error
rms = rms.to(device=D.device, dtype=torch.float32).clamp(min=1e-8)
D = D.mul_(rms)
else:
rms = None
# svd_lowrank draws random projections; fork so user generation seeds are untouched and re-applies are deterministic
with torch.random.fork_rng(devices=[D.device] if D.device.type == 'cuda' else []):
torch.manual_seed(0)
U, S, V = torch.svd_lowrank(D, q=q, niter=2)
energy = float(S.square().sum() / D.square().sum().clamp(min=1e-30)) # captured fraction, in the weighted domain when calibrated
up_h = (U * S).to(dtype=dtype)
down_h = V.t()
if rms is not None:
down_h = down_h / rms # unscale in the original input basis, before any rotation
if deq.use_hadamard:
down_h = rotate_hadamard(down_h, group_size=deq.hadamard_group_size)
append_factors(self, ups + [up_h], downs + [down_h.to(dtype=dtype)])
hosted_layers.append((network_layer_name, energy, rms is not None))
return True
def note_fallback(self, network_layer_name):
"""Record a quantized layer taking the lossy requantize path (summary-logged per pass)."""
if getattr(self, 'sdnq_dequantizer', None) is not None:
fallback_layers.append(network_layer_name)
def report_fallbacks():
if len(hosted_layers) > 0:
energies = sorted(e for _name, e, _c in hosted_layers)
median = energies[len(energies) // 2]
calibrated = sum(1 for _name, _e, c in hosted_layers if c)
log.info(f'Network load: type=LoRA quant=sdnq hosted={len(hosted_layers)} rank={int(shared.opts.lora_sdnq_host_rank)}{f" calib={calibrated}" if calibrated else ""} energy={median:.2f} min={energies[0]:.2f} non-factorable networks hosted on the svd side-channel')
if l.debug:
log.debug(f'Network load: type=LoRA quant=sdnq hosted={[(n, round(e, 3)) for n, e, _c in hosted_layers[:8]]}{"..." if len(hosted_layers) > 8 else ""}')
hosted_layers.clear()
if len(fallback_layers) > 0:
if enabled():
log.warning(f'Network load: type=LoRA quant=sdnq layers={len(fallback_layers)} non-factorable networks requantized in place (reduced fidelity on quantized weights)')
else:
log.info(f'Network load: type=LoRA quant=sdnq apply=requantize layers={len(fallback_layers)} reason=setting')
if l.debug:
log.debug(f'Network load: type=LoRA quant=sdnq requantized={fallback_layers[:8]}{"..." if len(fallback_layers) > 8 else ""}')
fallback_layers.clear()
+65 -7
View File
@@ -3,6 +3,8 @@ import time
import rich.progress as rp
from modules.errorlimiter import limit_errors
from modules.lora import lora_common as l
from modules.lora import lora_overrides
from modules.lora import lora_sdnq
from modules.lora.lora_apply import network_apply_weights, network_apply_direct, network_backup_weights, network_calc_weights
from modules import shared, devices, sd_models
from modules.logger import log, console
@@ -53,6 +55,7 @@ def network_activate(include=None, exclude=None):
net.unet_multiplier = pending['unet']
net.dyn_dim = pending['dyn']
t0 = time.time()
fuse = lora_overrides.fuse_native() # resolve once: backup and apply passes must agree
with limit_errors("network_activate") as elimit:
sd_model = getattr(shared.sd_model, "pipe", shared.sd_model)
if shared.opts.diffusers_offload_mode == "sequential":
@@ -86,7 +89,10 @@ def network_activate(include=None, exclude=None):
refused = 0
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 ()
stack_sig = lora_sdnq.signature() # apply-mechanism token tracked beside network_current_names so a settings-only flip re-applies
applied_layers.clear()
lora_sdnq.fallback_layers.clear() # a raise mid-pass leaves stale entries behind
lora_sdnq.hosted_layers.clear()
backup_size = 0
for component in modules.keys():
component_wanted = wanted_names if component in components else ()
@@ -94,13 +100,54 @@ def network_activate(include=None, exclude=None):
for _, module in modules[component]:
network_layer_name = getattr(module, 'network_layer_name', None)
current_names = getattr(module, "network_current_names", ())
if getattr(module, 'weight', None) is None or shared.state.interrupted or (network_layer_name is None) or (current_names == component_wanted):
if getattr(module, 'weight', None) is None or shared.state.interrupted or (network_layer_name is None) or (current_names == component_wanted and getattr(module, 'network_current_stack', '') == stack_sig):
if task is not None:
pbar.update(task, advance=1)
continue
if group_offload and component not in group_stripped and group_will_mutate(module, network_layer_name, l.loaded_networks):
device = group_offload_strip(sd_model, component, group_stripped)
backup_size += network_backup_weights(module, network_layer_name, component_wanted)
if lora_sdnq.factor_candidate(module, network_layer_name, component_wanted):
weights_backup = getattr(module, "network_weights_backup", None)
if weights_backup is not None and not isinstance(weights_backup, bool):
network_apply_weights(module, None, None, device=device) # an earlier non-factorable set requantized this layer, restore the pristine base before attaching factors
applied = lora_sdnq.apply_factors(module, network_layer_name, component_wanted)
if applied is not None: # exact path took the layer; None falls through to hosting or requantize
if applied and component_wanted:
applied_layers.append(network_layer_name)
applied_weight += 1
module.network_current_names = component_wanted
module.network_current_stack = stack_sig
if task is not None:
pbar.update(task, advance=1)
continue
if lora_sdnq.host_candidate(module, network_layer_name, component_wanted):
weights_backup = getattr(module, "network_weights_backup", None)
if weights_backup is not None and not isinstance(weights_backup, bool):
network_apply_weights(module, None, None, device=device) # the hosted delta is measured against the pristine base
batch_updown, batch_ex_bias = network_calc_weights(module, network_layer_name, elimit=elimit)
if batch_ex_bias is None: # bias deltas need the plain path; weight-only sets ride the side-channel without a weight backup
hosted = lora_sdnq.apply_hosted(module, network_layer_name, batch_updown, component_wanted)
if hosted is not None:
if hosted and component_wanted:
applied_layers.append(network_layer_name)
applied_weight += 1
module.network_current_names = component_wanted
module.network_current_stack = stack_sig
batch_updown, batch_ex_bias = None, None
del batch_updown, batch_ex_bias
if task is not None:
pbar.update(task, advance=1)
continue
batch_updown, batch_ex_bias = None, None
del batch_updown, batch_ex_bias
stripped = lora_sdnq.remove_factors(module) # the mechanism gate can decline a layer still carrying attached factors; the weight path must start from the pristine channel
if stripped and not component_wanted: # factor-mode layers have no tensor backup, dropping the factors is the whole restore
module.network_current_names = ()
module.network_current_stack = stack_sig
if task is not None:
pbar.update(task, advance=1)
continue
backup_size += network_backup_weights(module, network_layer_name, component_wanted, fuse)
if not component_wanted:
weights_backup = getattr(module, "network_weights_backup", None)
if weights_backup is None or isinstance(weights_backup, bool): # fuse mode has no tensor backup, restore stays with network_deactivate
@@ -110,7 +157,9 @@ def network_activate(include=None, exclude=None):
batch_updown, batch_ex_bias = None, None # restore-only pass, apply with no weights reverts to backup
else:
batch_updown, batch_ex_bias = network_calc_weights(module, network_layer_name, elimit=elimit)
if shared.opts.lora_fuse_native:
if batch_updown is not None:
lora_sdnq.note_fallback(module, network_layer_name) # only layers whose quantized weight actually takes a delta
if fuse:
weight_written, bias_written = network_apply_direct(module, batch_updown, batch_ex_bias, device=device)
else:
weight_written, bias_written = network_apply_weights(module, batch_updown, batch_ex_bias, device=device)
@@ -122,6 +171,7 @@ def network_activate(include=None, exclude=None):
batch_updown, batch_ex_bias = None, None
del batch_updown, batch_ex_bias
module.network_current_names = component_wanted
module.network_current_stack = stack_sig
if task is not None:
bs = round(backup_size/1024/1024/1024, 2) if backup_size > 0 else None
pbar.update(task, advance=1, description=f'networks={len(l.loaded_networks)} modules={active_components} layers={total} weights={applied_weight} bias={applied_bias} backup={bs} device={device}')
@@ -129,13 +179,14 @@ def network_activate(include=None, exclude=None):
if task is not None and len(applied_layers) == 0:
pbar.remove_task(task) # hide progress bar for no action
global native_active, refused_writes # pylint: disable=global-statement
lora_sdnq.report_fallbacks()
native_active = len(l.loaded_networks) > 0
refused_writes = refused
l.timer.activate += time.time() - t0
if refused > 0:
log.error(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} weights={applied_weight} bias={applied_bias} refused={refused} network partially applied')
if l.debug and len(l.loaded_networks) > 0:
log.debug(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} modules={active_components} layers={total} weights={applied_weight} bias={applied_bias} refused={refused} backup={round(backup_size/1024/1024/1024, 2)} fuse={shared.opts.lora_fuse_native}:{shared.opts.lora_fuse_diffusers} device={device} time={l.timer.summary}')
log.debug(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} modules={active_components} layers={total} weights={applied_weight} bias={applied_bias} refused={refused} backup={round(backup_size/1024/1024/1024, 2)} fuse={fuse}:{shared.opts.lora_fuse_diffusers} device={device} time={l.timer.summary}')
modules.clear()
if len(applied_layers) > 0 or shared.opts.diffusers_offload_mode == "sequential" or len(group_stripped) > 0:
sd_models.set_diffuser_offload(sd_model, op="model")
@@ -146,7 +197,8 @@ def network_deactivate(include=None, exclude=None):
exclude = []
if include is None:
include = []
if not shared.opts.lora_fuse_native or shared.opts.lora_force_diffusers:
fuse = lora_overrides.fuse_native() # must match network_activate: backup mode restores in its restore-only pass instead
if not fuse or shared.opts.lora_force_diffusers:
return
if len(l.previously_loaded_networks) == 0:
return
@@ -190,8 +242,14 @@ def network_deactivate(include=None, exclude=None):
continue
if group_offload and component not in group_stripped and group_will_mutate(module, network_layer_name, l.previously_loaded_networks):
device = group_offload_strip(sd_model, component, group_stripped)
if lora_sdnq.remove_factors(module): # exact inverse for factor-mode layers, weights were never touched
applied_layers.append(network_layer_name)
module.network_current_names = ()
if task is not None:
pbar.update(task, advance=1)
continue
batch_updown, batch_ex_bias = network_calc_weights(module, network_layer_name, use_previous=True, elimit=elimit)
if shared.opts.lora_fuse_native:
if fuse:
weight_written, bias_written = network_apply_direct(module, batch_updown, batch_ex_bias, device=device, deactivate=True)
else:
weight_written, bias_written = network_apply_weights(module, batch_updown, batch_ex_bias, device=device, deactivate=True)
@@ -206,7 +264,7 @@ def network_deactivate(include=None, exclude=None):
if refused > 0:
log.error(f'Network unload: type=LoRA networks={[n.name for n in l.previously_loaded_networks]} unapply={len(applied_layers)} refused={refused} network partially removed')
if l.debug and len(l.previously_loaded_networks) > 0:
log.debug(f'Network deactivate: type=LoRA networks={[n.name for n in l.previously_loaded_networks]} modules={active_components} layers={total} apply={len(applied_layers)} refused={refused} fuse={shared.opts.lora_fuse_native}:{shared.opts.lora_fuse_diffusers} time={l.timer.summary}')
log.debug(f'Network deactivate: type=LoRA networks={[n.name for n in l.previously_loaded_networks]} modules={active_components} layers={total} apply={len(applied_layers)} refused={refused} fuse={fuse}:{shared.opts.lora_fuse_diffusers} time={l.timer.summary}')
modules.clear()
if len(applied_layers) > 0 or shared.opts.diffusers_offload_mode == "sequential" or len(group_stripped) > 0:
sd_models.set_diffuser_offload(sd_model, op="model")
+25 -12
View File
@@ -676,18 +676,6 @@ def create_settings(cmd_opts):
"extra_network_reference_enable": OptionInfo(True, "Enable use of reference models", gr.Checkbox),
"extra_network_reference_values": OptionInfo(False, "Use reference values when available", gr.Checkbox),
"extra_networks_lora_sep": OptionInfo("<h2>LoRA</h2>", "", gr.HTML),
"extra_networks_default_multiplier": OptionInfo(1.0, "Default strength", gr.Slider, {"minimum": 0.0, "maximum": 2.0, "step": 0.01}),
"lora_force_reload": OptionInfo(False, "LoRA force reload always"),
"lora_force_diffusers": OptionInfo(False if not cmd_opts.use_openvino else True, "LoRA load using Diffusers method"),
"lora_apply_te": OptionInfo(False, "LoRA native apply to text encoder"),
"lora_fuse_native": OptionInfo(True, "LoRA native fuse with model"),
"lora_fuse_diffusers": OptionInfo(False, "LoRA diffusers fuse with model"),
"lora_apply_tags": OptionInfo(0, "LoRA auto-apply tags", gr.Slider, {"minimum": -1, "maximum": 32, "step": 1}),
"lora_in_memory_limit": OptionInfo(1, "LoRA memory cache", gr.Slider, {"minimum": 0, "maximum": 32, "step": 1}),
"lora_add_hashes_to_infotext": OptionInfo(False, "LoRA add hash info to metadata"),
"extra_networks_styles_sep": OptionInfo("<h2>Styles</h2>", "", gr.HTML),
"extra_networks_styles": OptionInfo(True, "Show reference styles"),
"extra_networks_apply_unparsed": OptionInfo(True, "Restore unparsed prompt"),
@@ -700,6 +688,31 @@ def create_settings(cmd_opts):
"wildcards_enabled": OptionInfo(True, "Enable file wildcards support"),
}))
# --- LoRA ---
options_templates.update(options_section(('lora', "LoRA"), {
"lora_load_sep": OptionInfo("<h2>Load options</h2>", "", gr.HTML),
"lora_force_reload": OptionInfo(False, "LoRA force reload always"),
"lora_force_diffusers": OptionInfo(False if not cmd_opts.use_openvino else True, "LoRA load using Diffusers method"),
"lora_in_memory_limit": OptionInfo(1, "LoRA memory cache", gr.Slider, {"minimum": 0, "maximum": 32, "step": 1}),
"lora_prompt_sep": OptionInfo("<h2>Prompt helpers</h2>", "", gr.HTML),
"extra_networks_default_multiplier": OptionInfo(1.0, "Default strength", gr.Slider, {"minimum": 0.0, "maximum": 2.0, "step": 0.01}),
"lora_apply_tags": OptionInfo(0, "LoRA auto-apply tags", gr.Slider, {"minimum": -1, "maximum": 32, "step": 1}),
"lora_apply_sep": OptionInfo("<h2>Apply method</h2>", "", gr.HTML),
"lora_apply_te": OptionInfo(False, "LoRA native apply to text encoder"),
"lora_fuse_native": OptionInfo(True, "LoRA native fuse with model"),
"lora_fuse_diffusers": OptionInfo(False, "LoRA diffusers fuse with model"),
"lora_quant_sep": OptionInfo("<h2>Quantization options</h2>", "", gr.HTML),
"lora_sdnq_apply": OptionInfo("exact", "LoRA quantized apply method", gr.Radio, {"choices": ["exact", "requantize"]}),
"lora_sdnq_host_rank": OptionInfo(256, "LoRA quantized host rank", gr.Slider, {"minimum": 0, "maximum": 1024, "step": 32}),
"lora_sdnq_host_calib": OptionInfo(True, "LoRA quantized host calibration"),
"lora_meta_sep": OptionInfo("<h2>Metadata</h2>", "", gr.HTML),
"lora_add_hashes_to_infotext": OptionInfo(False, "LoRA add hash info to metadata"),
}))
# --- Extensions ---
options_templates.update(options_section(('extensions', "Extensions"), {
"disable_all_extensions": OptionInfo("none", "Disable all extensions", gr.Radio, {"choices": ["none", "user", "all"]}),
+992
View File
@@ -0,0 +1,992 @@
#!/usr/bin/env python
"""
Offline unit tests for LoRA application on SDNQ-quantized layers.
Pins two facts established on real checkpoints (see cli/lora-quant-fidelity.py
for the per-model analyzer):
- The requantize path (dequantize + add + requantize) erases sub-step deltas
on low-bit formats: retention collapses to the ~2/group_size grid-extrema
floor on uint4, while int8 retains most of the delta. Guards against the
erasure law silently changing.
- The factor path (modules/lora/lora_sdnq.py) applies plain LoRA deltas
through the svd side-channel exactly, in both svd layouts and across
quantization configs (hadamard on/off, checkpoint svd correction present
or absent), with exact stacking, multiplier scaling and bit-exact
removal, wired through the real networks.network_activate /
network_deactivate control flow.
- Multi-LoRA set transitions keep the base pristine: a layer that fell back
to requantize (mixed factorable/non-factorable set) restores from backup
before re-entering the factor path, layers targeted by only some of the
loaded networks stay independent, and untargeted quantized layers are not
flagged as requantized.
- Robustness: factor removal restores onto the layer's current device after
an offload-style move, and a shape-mismatched network stacked onto a
factor-mode layer downgrades to the legacy path instead of raising.
- Hosting: on sub-8-bit layers, non-factorable sets ride the side-channel as
a truncated svd of their calc_updown delta: low-rank content survives
whole, dense content beats the requantize floor by a wide margin, int8
and rank 0 keep the requantize path, removal stays bit-exact, and the
svd's random projections never touch the generation rng stream.
- Calibration: per-channel activation statistics weight the hosted
truncation toward loud input channels for better output-space retention;
low-rank content still survives whole, disabling the option reproduces
plain truncation bit-exact, and the capture hooks accumulate, persist
and reload statistics correctly, gated by option, format width and
model compile.
All tensors are synthetic; no model files or running server required.
Usage:
python test/test-sdnq-lora-factors.py
"""
import os
import sys
import time
from contextlib import contextmanager
import torch
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, script_dir)
os.chdir(script_dir)
os.environ['SD_INSTALL_QUIET'] = '1'
# Bootstrap cmd_args before any module that pulls in shared.py.
import modules.cmd_args # pylint: disable=wrong-import-position
import installer # pylint: disable=wrong-import-position
_orig_argv = sys.argv
sys.argv = [sys.argv[0]]
try:
modules.cmd_args.parse_args()
finally:
sys.argv = _orig_argv
installer.add_args(modules.cmd_args.parser)
modules.cmd_args.parsed, _ = modules.cmd_args.parser.parse_known_args([])
from modules.errors import log # pylint: disable=wrong-import-position
from modules import shared, sd_models # pylint: disable=wrong-import-position
from modules.lora import network, network_lora, lora_sdnq, networks # pylint: disable=wrong-import-position
from modules.lora import lora_common as l_common # pylint: disable=wrong-import-position
from sdnq.quantizer import sdnq_quantize_layer, SDNQConfig # pylint: disable=wrong-import-position
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
OUT_F, IN_F, RANK = 512, 512, 8
results: dict[str, dict] = {}
def category(name: str):
if name not in results:
results[name] = {'passed': 0, 'failed': 0, 'tests': []}
return name
def record(cat: str, passed: bool, name: str, detail: str = ''):
status = 'PASS' if passed else 'FAIL'
results[cat]['passed' if passed else 'failed'] += 1
results[cat]['tests'].append((status, name))
msg = f' {status}: {name}'
if detail:
msg += f' ({detail})'
if passed:
log.info(msg)
else:
log.error(msg)
def run_test(cat: str, fn):
name = fn.__name__
try:
ok = fn()
record(cat, ok is not False, name)
except AssertionError as e:
record(cat, False, name, str(e))
except Exception as e: # pylint: disable=broad-except
record(cat, False, name, f'exception: {e}')
import traceback
traceback.print_exc()
def build_layer(weights_dtype='uint4', use_quantized_matmul=False, seed=0, use_hadamard=True, use_svd=False):
torch.manual_seed(seed)
lin = torch.nn.Linear(IN_F, OUT_F, bias=False, dtype=torch.bfloat16, device=DEVICE)
with torch.no_grad():
lin.weight.copy_(torch.randn(OUT_F, IN_F, device=DEVICE) * 0.04)
cfg = SDNQConfig(weights_dtype=weights_dtype, group_size=0, hadamard_group_size=256, use_hadamard=use_hadamard,
use_svd=use_svd, svd_rank=32, use_quantized_matmul=use_quantized_matmul, dequantize_fp32=False,
quantization_device=str(DEVICE), return_device=str(DEVICE))
layer, _ = sdnq_quantize_layer(lin, cfg, torch_dtype=torch.bfloat16, param_name='test.weight')
layer.network_layer_name = 'lora_transformer_test'
layer.network_current_names = ()
return layer
def dq(layer):
return layer.sdnq_dequantizer(layer.weight, layer.scale, zero_point=layer.zero_point,
svd_up=layer.svd_up, svd_down=layer.svd_down,
skip_quantized_matmul=layer.sdnq_dequantizer.use_quantized_matmul,
dtype=torch.float32, skip_compile=True)
def make_delta(seed=1, sigma=3e-4):
torch.manual_seed(seed)
A = torch.randn(RANK, IN_F, device=DEVICE) * (sigma ** 0.5)
B = torch.randn(OUT_F, RANK, device=DEVICE) * (sigma ** 0.5)
return A, B, B @ A
class MockNOD:
def __init__(self, name):
self.filename = f'/tmp/{name}.safetensors'
self.name = name
self.shorthash = ''
self.sd_version = 'unknown'
def make_net(name, layer, A, B, te_mult=1.0, alpha=None, dora=False):
net = network.Network(name, MockNOD(name))
net.te_multiplier = te_mult
net.unet_multiplier = [te_mult] * 3
w = {'lora_up.weight': B.cpu(), 'lora_down.weight': A.cpu()}
if alpha is not None:
w['alpha'] = torch.tensor(float(alpha))
if dora:
w['dora_scale'] = torch.ones(B.shape[0], 1)
nw = network.NetworkWeights(network_key=layer.network_layer_name, sd_key=layer.network_layer_name, w=w, sd_module=layer)
mod = network_lora.NetworkModuleLora(net, nw)
net.modules[layer.network_layer_name] = mod
return net
def rho_of(E, D):
return float(E.flatten() @ D.flatten() / D.flatten().square().sum())
def requant_effective(layer, D):
"""The lossy fallback path: quantize(W_dq + D) fresh with the layer's own params."""
from sdnq.quantizer import sdnq_quantize_layer_weight
deq = layer.sdnq_dequantizer
Wdq = dq(layer)
deq2, data2 = sdnq_quantize_layer_weight(Wdq + D, layer_class_name='Linear', weights_dtype=deq.weights_dtype,
group_size=deq.group_size, hadamard_group_size=deq.hadamard_group_size,
use_hadamard=deq.use_hadamard, use_svd=False, use_quantized_matmul=False,
dequantize_fp32=False, torch_dtype=torch.bfloat16)
W2 = deq2(data2['weight'], data2['scale'], zero_point=data2['zero_point'], svd_up=None, svd_down=None, dtype=torch.float32, skip_compile=True)
return W2 - Wdq
# ============================================================
# Tests - the erasure law (why the factor path exists)
# ============================================================
CAT_LAW = category('erasure-law')
def test_uint4_erases_substep_delta():
layer = build_layer('uint4')
_A, _B, D = make_delta(sigma=2e-4)
rho = rho_of(requant_effective(layer, D), D)
group = layer.sdnq_dequantizer.group_size
floor = 2.0 / group
assert rho < 4 * floor, f'rho={rho:.4f} expected near extrema floor {floor:.4f}'
return True
def test_int8_retains_delta():
layer = build_layer('int8')
_A, _B, D = make_delta(sigma=2e-4)
rho = rho_of(requant_effective(layer, D), D)
assert rho > 0.5, f'rho={rho:.4f} expected int8 to retain most of the delta'
return True
# ============================================================
# Tests - factor path exactness
# ============================================================
CAT_FACTOR = category('factor-path')
def test_apply_exact_and_remove_bitexact():
layer = build_layer('uint4')
A, B, D = make_delta()
net = make_net('one', layer, A, B)
l_common.loaded_networks.clear()
l_common.loaded_networks.append(net)
wanted = (('one', 1.0, 1.0, None),)
assert lora_sdnq.factor_candidate(layer, layer.network_layer_name, wanted) is True
Wdq0 = dq(layer)
assert lora_sdnq.apply_factors(layer, layer.network_layer_name, wanted) is True
rho = rho_of(dq(layer) - Wdq0, D)
assert rho > 0.99, f'rho={rho:.4f}'
assert lora_sdnq.remove_factors(layer) is True
assert torch.equal(dq(layer), Wdq0), 'remove must be bit-exact'
assert layer.svd_up is None and not hasattr(layer, 'sdnq_lora_svd_stash')
l_common.loaded_networks.clear()
return True
def test_multiplier_and_alpha_scaling():
layer = build_layer('uint4')
A, B, D = make_delta()
net = make_net('one', layer, A, B, te_mult=0.5, alpha=RANK // 2) # alpha/rank = 0.5
l_common.loaded_networks.clear()
l_common.loaded_networks.append(net)
Wdq0 = dq(layer)
lora_sdnq.apply_factors(layer, layer.network_layer_name, (('one', 0.5, 0.5, None),))
rho = rho_of(dq(layer) - Wdq0, D)
assert abs(rho - 0.25) < 0.01, f'expected 0.5*0.5 scaling, rho={rho:.4f}'
lora_sdnq.remove_factors(layer)
l_common.loaded_networks.clear()
return True
def test_stacking_two_networks():
layer = build_layer('uint4')
A1, B1, D1 = make_delta(seed=1)
A2, B2, D2 = make_delta(seed=2)
l_common.loaded_networks.clear()
l_common.loaded_networks.extend([make_net('a', layer, A1, B1), make_net('b', layer, A2, B2)])
Wdq0 = dq(layer)
lora_sdnq.apply_factors(layer, layer.network_layer_name, (('a', 1.0, 1.0, None), ('b', 1.0, 1.0, None)))
rho = rho_of(dq(layer) - Wdq0, D1 + D2)
assert rho > 0.99, f'rho={rho:.4f}'
lora_sdnq.remove_factors(layer)
l_common.loaded_networks.clear()
return True
def test_matmul_layout_transposed():
layer = build_layer('uint4', use_quantized_matmul=True)
A, B, D = make_delta()
l_common.loaded_networks.clear()
l_common.loaded_networks.append(make_net('one', layer, A, B))
Wdq0 = dq(layer)
res = lora_sdnq.apply_factors(layer, layer.network_layer_name, (('one', 1.0, 1.0, None),))
rho = rho_of(dq(layer) - Wdq0, D)
assert res is True and rho > 0.99, f'rho={rho:.4f}'
lora_sdnq.remove_factors(layer)
assert torch.equal(dq(layer), Wdq0)
l_common.loaded_networks.clear()
return True
def test_dora_falls_back():
layer = build_layer('uint4')
A, B, _D = make_delta()
l_common.loaded_networks.clear()
l_common.loaded_networks.append(make_net('dora', layer, A, B, dora=True))
assert lora_sdnq.factor_candidate(layer, layer.network_layer_name, (('dora', 1.0, 1.0, None),)) is False
l_common.loaded_networks.clear()
return True
def assert_factor_roundtrip(layer, tag):
"""Apply-exact plus bit-exact removal on the given layer, whatever its quantization config."""
A, B, D = make_delta()
Wdq0 = dq(layer)
orig_up = layer.svd_up
l_common.loaded_networks.clear()
l_common.loaded_networks.append(make_net('one', layer, A, B))
wanted = (('one', 1.0, 1.0, None),)
assert lora_sdnq.factor_candidate(layer, layer.network_layer_name, wanted) is True, f'{tag}: not a factor candidate'
assert lora_sdnq.apply_factors(layer, layer.network_layer_name, wanted) is True, f'{tag}: apply failed'
E = dq(layer) - Wdq0
rho = rho_of(E, D)
resid = float((E - D).norm() / D.norm())
assert rho > 0.99 and resid < 0.2, f'{tag}: rho={rho:.4f} resid={resid:.4f}'
assert lora_sdnq.remove_factors(layer) and torch.equal(dq(layer), Wdq0), f'{tag}: remove not bit-exact'
assert layer.svd_up is orig_up, f'{tag}: original svd factors not restored'
l_common.loaded_networks.clear()
def test_no_hadamard_checkpoint():
"""Checkpoints quantized without hadamard: factors attach unrotated."""
assert_factor_roundtrip(build_layer('uint4', use_hadamard=False), 'plain')
assert_factor_roundtrip(build_layer('uint4', use_hadamard=False, use_quantized_matmul=True), 'matmul')
return True
def test_checkpoint_svd_factors_preserved():
"""Checkpoints quantized with their own svd correction keep it under apply/remove."""
layer = build_layer('uint4', use_svd=True)
assert layer.svd_up is not None, 'quantizer produced no svd correction'
assert_factor_roundtrip(layer, 'plain')
assert_factor_roundtrip(build_layer('uint4', use_svd=True, use_quantized_matmul=True), 'matmul')
return True
# ============================================================
# Tests - memory accounting across apply modes
# ============================================================
CAT_MEM = category('memory')
def tensor_bytes(t):
return t.numel() * t.element_size() if isinstance(t, torch.Tensor) else 0
def test_factor_path_memory_is_factors_only():
"""Factor path: no weight/quant-state backups; added memory = the factor tensors."""
layer = build_layer('uint4')
A, B, _D = make_delta()
l_common.loaded_networks.clear()
l_common.loaded_networks.append(make_net('one', layer, A, B))
lora_sdnq.apply_factors(layer, layer.network_layer_name, (('one', 1.0, 1.0, None),))
assert getattr(layer, 'network_weights_backup', None) is None
assert not hasattr(layer, 'sdnq_dequantizer_backup') and not hasattr(layer, 'sdnq_scale_backup')
added = tensor_bytes(layer.svd_up) + tensor_bytes(layer.svd_down)
expected = RANK * (OUT_F + IN_F) * 2 # bf16 factors
assert added == expected, f'factor bytes {added} != expected {expected}'
would_be_backup = tensor_bytes(layer.weight) + tensor_bytes(layer.scale) + tensor_bytes(layer.zero_point)
assert added < would_be_backup / 4, f'factors {added}B should undercut the {would_be_backup}B backup this layer would otherwise clone'
lora_sdnq.remove_factors(layer)
assert layer.svd_up is None and layer.svd_down is None
l_common.loaded_networks.clear()
return True
def test_backup_mode_clones_full_quant_state():
"""Fallback in backup mode: packed weight + scale + zero_point are cloned to cpu."""
from modules.lora.lora_apply import network_backup_weights
layer = build_layer('uint4')
A, B, _D = make_delta()
l_common.loaded_networks.clear()
l_common.loaded_networks.append(make_net('dora', layer, A, B, dora=True)) # non-factorable
reported = network_backup_weights(layer, layer.network_layer_name, (('dora', 1.0, 1.0, None),), fuse=False)
assert isinstance(layer.network_weights_backup, torch.Tensor) and layer.network_weights_backup.device.type == 'cpu'
assert hasattr(layer, 'sdnq_dequantizer_backup') and isinstance(layer.sdnq_scale_backup, torch.Tensor)
assert reported == tensor_bytes(layer.weight), f'reported {reported} != packed weight bytes {tensor_bytes(layer.weight)}'
total = reported + tensor_bytes(layer.sdnq_scale_backup) + tensor_bytes(layer.sdnq_zero_point_backup)
expected_min = OUT_F * IN_F // 2 # uint4 packs two weights per byte
assert total >= expected_min, f'backup {total}B below packed-weight floor {expected_min}B'
l_common.loaded_networks.clear()
return True
def test_fuse_mode_marker_takes_no_memory():
"""Fuse mode stores a boolean marker instead of tensors; guard forces backup on quantized models."""
from modules.lora.lora_apply import network_backup_weights
from modules.lora import lora_overrides
layer = build_layer('uint4')
A, B, _D = make_delta()
l_common.loaded_networks.clear()
l_common.loaded_networks.append(make_net('dora', layer, A, B, dora=True))
reported = network_backup_weights(layer, layer.network_layer_name, (('dora', 1.0, 1.0, None),), fuse=True)
assert layer.network_weights_backup is True and reported == 0
assert not hasattr(layer, 'sdnq_dequantizer_backup')
# the guard: a quantized component forces fuse off model-wide regardless of the option
class MockCfg:
quantization_config = {'quant_method': 'sdnq'}
class MockSd:
pass
sd = MockSd()
sd.transformer = torch.nn.Linear(4, 4)
sd.transformer.config = MockCfg()
from modules.modeldata import model_data
prev_model = model_data.sd_model
old_fuse = shared.opts.lora_fuse_native
try:
model_data.sd_model = sd
shared.opts.lora_fuse_native = True
assert lora_overrides.disable_fuse() is True
assert lora_overrides.fuse_native() is False
finally:
shared.opts.lora_fuse_native = old_fuse
model_data.sd_model = prev_model
l_common.loaded_networks.clear()
return True
# ============================================================
# Tests - integration through networks.network_activate
# ============================================================
CAT_E2E = category('activate-e2e')
class MockHolder(torch.nn.Module):
@property
def device(self):
return DEVICE
@contextmanager
def mock_model(**layers):
"""Install a one-component mock pipeline holding the given layers as shared.sd_model."""
class MockPipe:
pass
class MockSd:
pass
holder = MockHolder()
for attr, lyr in layers.items():
setattr(holder, attr, lyr)
pipe = MockPipe()
pipe.transformer = holder
sd = MockSd()
sd.pipe = pipe
from modules.modeldata import model_data
model_data.sd_model = sd
real_offload = sd_models.set_diffuser_offload
sd_models.set_diffuser_offload = lambda *a, **k: None
old_fuse = shared.opts.lora_fuse_native
shared.opts.lora_fuse_native = False # a real quantized model forces backup mode; the mock carries no quantization config, so pin it instead of inheriting the running config
try:
yield
finally:
shared.opts.lora_fuse_native = old_fuse
sd_models.set_diffuser_offload = real_offload
l_common.loaded_networks.clear()
l_common.previously_loaded_networks.clear()
def activate(*nets):
l_common.loaded_networks.clear()
l_common.loaded_networks.extend(nets)
networks.network_activate()
def test_network_activate_roundtrip():
layer = build_layer('uint4')
A, B, D = make_delta()
net = make_net('one', layer, A, B)
with mock_model(lin=layer):
Wdq0 = dq(layer)
activate(net)
rho = rho_of(dq(layer) - Wdq0, D)
assert rho > 0.99, f'rho={rho:.4f}'
assert getattr(layer, 'network_weights_backup', None) is None, 'factor path must not take weight backups'
activate() # restore pass
assert torch.equal(dq(layer), Wdq0), 'unload must restore bit-exact'
# fuse-mode deactivate route
layer.network_current_names = ()
activate(net)
l_common.previously_loaded_networks[:] = l_common.loaded_networks
shared.opts.lora_fuse_native = True
networks.network_deactivate()
assert torch.equal(dq(layer), Wdq0), 'fuse-mode deactivate must restore bit-exact'
return True
# ============================================================
# Tests - multi-LoRA set transitions between the two paths
# ============================================================
CAT_TRANS = category('transitions')
def test_mixed_family_transition_restores_base():
layer = build_layer('uint4')
bystander = build_layer('uint4', seed=7)
bystander.network_layer_name = 'lora_transformer_bystander'
A, B, D = make_delta()
net_plain = make_net('plain', layer, A, B)
A2, B2, _ = make_delta(seed=5, sigma=3e-3)
net_dora = make_net('doranet', layer, A2, B2, dora=True)
noted = []
real_report = lora_sdnq.report_fallbacks
def capture_report():
noted.append(len(lora_sdnq.fallback_layers))
real_report()
lora_sdnq.report_fallbacks = capture_report
try:
with host_rank(0), mock_model(lin=layer, bystander=bystander): # pins the requantize fallback; hosted transitions are covered in the hosting category
Wdq0 = dq(layer)
activate(net_plain)
assert hasattr(layer, 'sdnq_lora_svd_stash'), 'plain set must take the factor path'
assert noted[-1] == 0, f'untargeted quantized layers must not be flagged as requantized: noted={noted[-1]}'
activate(net_plain, net_dora)
assert not hasattr(layer, 'sdnq_lora_svd_stash') and isinstance(layer.network_weights_backup, torch.Tensor), 'mixed set must fall back with a tensor backup'
assert not torch.equal(dq(layer), Wdq0), 'fallback must have requantized the weights'
assert noted[-1] == 1, f'exactly the requantized layer must be flagged: noted={noted[-1]}'
activate(net_plain)
assert hasattr(layer, 'sdnq_lora_svd_stash'), 'plain-only set must re-enter the factor path'
rho = rho_of(dq(layer) - Wdq0, D)
assert rho > 0.99, f'rho={rho:.4f}'
stash, up, down = layer.sdnq_lora_svd_stash, layer.svd_up, layer.svd_down
lora_sdnq.remove_factors(layer)
base_clean = torch.equal(dq(layer), Wdq0)
layer.sdnq_lora_svd_stash, layer.svd_up, layer.svd_down = stash, up, down
assert base_clean, 'base under factors must be restored from backup on mixed-set exit'
activate()
assert torch.equal(dq(layer), Wdq0), 'unload must return bit-exact pristine'
finally:
lora_sdnq.report_fallbacks = real_report
return True
def test_partial_coverage_layers_stay_independent():
layer_plain = build_layer('uint4')
layer_dora = build_layer('uint4', seed=7)
layer_dora.network_layer_name = 'lora_transformer_other'
A, B, D = make_delta()
net_plain = make_net('plain', layer_plain, A, B)
A2, B2, _ = make_delta(seed=5, sigma=3e-3)
net_dora = make_net('dorafar', layer_dora, A2, B2, dora=True)
with host_rank(0), mock_model(lin=layer_plain, other=layer_dora): # pins the requantize fallback for the non-factorable layer
Wdq0, Wdq0_dora = dq(layer_plain), dq(layer_dora)
activate(net_plain, net_dora)
assert hasattr(layer_plain, 'sdnq_lora_svd_stash') and getattr(layer_plain, 'network_weights_backup', None) is None, 'plain layer must stay on the factor path'
assert isinstance(getattr(layer_dora, 'network_weights_backup', None), torch.Tensor), 'dora layer must take the backup fallback'
rho = rho_of(dq(layer_plain) - Wdq0, D)
assert rho > 0.99, f'rho={rho:.4f}'
activate()
assert torch.equal(dq(layer_plain), Wdq0), 'factor layer must restore bit-exact'
assert torch.equal(dq(layer_dora), Wdq0_dora), 'fallback layer must restore bit-exact'
return True
@contextmanager
def apply_method(value):
old = getattr(shared.opts, 'lora_sdnq_apply', 'exact')
shared.opts.lora_sdnq_apply = value
try:
yield
finally:
shared.opts.lora_sdnq_apply = old
def test_mechanism_gate_declines_candidates():
"""The requantize option must gate every svd-channel entry point and flip the apply-stamp token."""
layer = build_layer('uint4')
A, B, _D = make_delta()
l_common.loaded_networks.clear()
l_common.loaded_networks.append(make_net('one', layer, A, B))
wanted = (('one', 1.0, 1.0, None),)
try:
assert lora_sdnq.factor_candidate(layer, layer.network_layer_name, wanted)
with host_rank(64):
assert lora_sdnq.host_candidate(layer, layer.network_layer_name, wanted)
assert lora_sdnq.signature() == ''
with apply_method('requantize'):
assert not lora_sdnq.factor_candidate(layer, layer.network_layer_name, wanted)
with host_rank(64):
assert not lora_sdnq.host_candidate(layer, layer.network_layer_name, wanted)
assert lora_sdnq.signature() == '|quant=requantize'
finally:
l_common.loaded_networks.clear()
return True
def test_requantize_option_routes_to_legacy_path():
"""With the option set, a factorable set must take the classic backup-and-requantize path end to end."""
layer = build_layer('uint4')
A, B, _D = make_delta(sigma=3e-3)
net = make_net('one', layer, A, B)
with apply_method('requantize'), mock_model(lin=layer):
shared.opts.lora_fuse_native = False # a real quantized model forces backup mode; the mock carries no quantization config
Wdq0 = dq(layer)
activate(net)
assert not hasattr(layer, 'sdnq_lora_svd_stash'), 'legacy path must not touch the svd channel'
assert layer.svd_up is None, 'legacy path must leave the channel empty'
assert isinstance(layer.network_weights_backup, torch.Tensor), 'legacy path must take a tensor backup'
assert not torch.equal(dq(layer), Wdq0), 'legacy path must requantize the weights'
activate()
assert torch.equal(dq(layer), Wdq0), 'legacy restore must be bit-exact from backup'
return True
def test_mechanism_flip_strips_attached_factors():
"""Flipping to requantize with factors attached must strip them before the weight path takes the layer; flipping back must re-enter the factor path."""
layer = build_layer('uint4')
A, B, _D = make_delta(sigma=3e-3)
net = make_net('one', layer, A, B)
with mock_model(lin=layer):
shared.opts.lora_fuse_native = False # a real quantized model forces backup mode; the mock carries no quantization config
Wdq0 = dq(layer)
activate(net)
assert hasattr(layer, 'sdnq_lora_svd_stash'), 'default mechanism must take the factor path'
E_exact = dq(layer) - Wdq0
with apply_method('requantize'):
activate(net) # same set; the mechanism token in the apply stamp must force re-processing
assert not hasattr(layer, 'sdnq_lora_svd_stash'), 'flip must strip the attached factors'
assert layer.svd_up is None, 'stripped channel must be empty, or the requantized delta double-applies'
assert isinstance(layer.network_weights_backup, torch.Tensor), 'flipped layer must continue on the backup path'
activate(net) # flip back within the same loaded set
assert hasattr(layer, 'sdnq_lora_svd_stash'), 'flip back must re-enter the factor path'
assert torch.equal(dq(layer) - Wdq0, E_exact), 'exact re-apply must restore the base from backup before attaching'
activate()
assert torch.equal(dq(layer), Wdq0), 'unload must return bit-exact pristine'
return True
def test_mechanism_flip_restore_pass_strips():
"""A restore-only pass under the requantize option must still drop attached factors."""
layer = build_layer('uint4')
A, B, _D = make_delta(sigma=3e-3)
net = make_net('one', layer, A, B)
with mock_model(lin=layer):
Wdq0 = dq(layer)
activate(net)
assert hasattr(layer, 'sdnq_lora_svd_stash')
with apply_method('requantize'):
activate() # unload with the gate closed: the fallthrough strip is the only removal route
assert not hasattr(layer, 'sdnq_lora_svd_stash'), 'restore pass must strip the factors'
assert torch.equal(dq(layer), Wdq0), 'strip must restore bit-exact'
assert layer.network_current_names == (), 'stripped layer must be stamped restored'
return True
CAT_HOST = category('hosting')
@contextmanager
def host_rank(rank):
old = getattr(shared.opts, 'lora_sdnq_host_rank', 0)
shared.opts.lora_sdnq_host_rank = rank
try:
yield
finally:
shared.opts.lora_sdnq_host_rank = old
def make_dense_net(name, layer, D):
"""A full-family (dense diff) network module: non-factorable by construction."""
from modules.lora import network_full
net = network.Network(name, MockNOD(name))
net.te_multiplier = 1.0
net.unet_multiplier = [1.0] * 3
nw = network.NetworkWeights(network_key=layer.network_layer_name, sd_key=layer.network_layer_name,
w={'diff': D.cpu()}, sd_module=layer)
net.modules[layer.network_layer_name] = network_full.NetworkModuleFull(net, nw)
return net
def test_hosted_low_rank_delta_is_kept():
layer = build_layer('uint4')
_A, _B, D = make_delta(sigma=3e-3)
net = make_dense_net('densenet', layer, D) # low-rank content in a non-factorable container
with host_rank(64), mock_model(lin=layer):
Wdq0 = dq(layer)
activate(net)
assert hasattr(layer, 'sdnq_lora_svd_stash'), 'hosted set must ride the side-channel'
assert getattr(layer, 'network_weights_backup', None) is None, 'hosted layers must not take a weight backup'
rho = rho_of(dq(layer) - Wdq0, D)
assert rho > 0.95, f'rank-8 delta under cap 64 must be kept nearly whole: rho={rho:.4f}'
activate()
assert torch.equal(dq(layer), Wdq0), 'unload must restore bit-exact'
return True
def test_hosted_dense_delta_beats_requant():
layer = build_layer('uint4')
torch.manual_seed(3)
D = torch.randn(OUT_F, IN_F, device=DEVICE) * 3e-4 # full-rank, sub-step: requant erases it
requant_rho = rho_of(requant_effective(layer, D), D)
net = make_dense_net('densefull', layer, D)
with host_rank(256), mock_model(lin=layer):
Wdq0 = dq(layer)
activate(net)
hosted_rho = rho_of(dq(layer) - Wdq0, D)
assert hosted_rho > 0.4, f'hosted rho={hosted_rho:.3f}'
assert hosted_rho > requant_rho + 0.3, f'hosting must beat requant by a wide margin: {hosted_rho:.3f} vs {requant_rho:.3f}'
activate()
assert torch.equal(dq(layer), Wdq0)
return True
def test_hosted_skips_int8():
layer = build_layer('int8')
_A, _B, D = make_delta(sigma=3e-3)
net = make_dense_net('int8net', layer, D)
with host_rank(256), mock_model(lin=layer):
activate(net)
assert not hasattr(layer, 'sdnq_lora_svd_stash'), 'int8 must keep the requantize path'
assert isinstance(getattr(layer, 'network_weights_backup', None), torch.Tensor), 'int8 fallback must take the backup'
activate()
return True
def test_hosted_disabled_by_option():
layer = build_layer('uint4')
_A, _B, D = make_delta(sigma=3e-3)
net = make_dense_net('offnet', layer, D)
with host_rank(0), mock_model(lin=layer):
activate(net)
assert not hasattr(layer, 'sdnq_lora_svd_stash'), 'rank 0 must disable hosting'
activate()
return True
def test_hosted_transitions_and_rng_isolation():
layer = build_layer('uint4')
A, B, D = make_delta()
net_plain = make_net('plainh', layer, A, B)
_A2, _B2, D2 = make_delta(seed=9, sigma=3e-3)
net_dense = make_dense_net('denseh', layer, D2)
with host_rank(256), mock_model(lin=layer):
Wdq0 = dq(layer)
rng0 = torch.cuda.get_rng_state() if DEVICE.type == 'cuda' else torch.get_rng_state()
activate(net_dense) # hosted
rng1 = torch.cuda.get_rng_state() if DEVICE.type == 'cuda' else torch.get_rng_state()
assert torch.equal(rng0, rng1), 'hosting must not consume the generation rng stream'
assert hasattr(layer, 'sdnq_lora_svd_stash')
activate(net_plain) # exact replaces hosted
rho = rho_of(dq(layer) - Wdq0, D)
assert rho > 0.99, f'exact set after hosted set: rho={rho:.4f}'
activate(net_plain, net_dense) # mixed set hosts the combined delta
rho_mix = rho_of(dq(layer) - Wdq0, D + D2)
assert rho_mix > 0.9, f'mixed hosted rho={rho_mix:.4f}'
activate()
assert torch.equal(dq(layer), Wdq0), 'unload must restore bit-exact'
return True
CAT_CALIB = category('calibration')
@contextmanager
def host_calib(value):
old = getattr(shared.opts, 'lora_sdnq_host_calib', False)
shared.opts.lora_sdnq_host_calib = value
try:
yield
finally:
shared.opts.lora_sdnq_host_calib = old
def test_calibrated_hosting_beats_plain():
layer = build_layer('uint4')
torch.manual_seed(11)
scale = torch.ones(IN_F, device=DEVICE)
scale[:32] = 40.0 # a few loud input channels, the shape real activations have
D = torch.randn(OUT_F, IN_F, device=DEVICE) * 3e-4
X = torch.randn(1024, IN_F, device=DEVICE) * scale
Y = X @ D.t()
net = make_dense_net('calnet', layer, D)
def out_rho(E):
return float((X @ E.t()).flatten() @ Y.flatten() / Y.square().sum())
with host_rank(32), host_calib(True), mock_model(lin=layer):
Wdq0 = dq(layer)
activate(net)
plain = out_rho(dq(layer) - Wdq0)
activate()
layer.sdnq_calib_rms = scale.cpu() # statistics as the capture leaves them
activate(net)
weighted = out_rho(dq(layer) - Wdq0)
activate()
del layer.sdnq_calib_rms
assert torch.equal(dq(layer), Wdq0), 'unload must restore bit-exact'
assert weighted > plain + 0.2, f'calibrated hosting must beat plain in output space: {weighted:.3f} vs {plain:.3f}'
return True
def test_calibrated_low_rank_delta_survives():
layer = build_layer('uint4')
_A, _B, D = make_delta(sigma=3e-3)
net = make_dense_net('calfull', layer, D)
with host_rank(64), host_calib(True), mock_model(lin=layer):
Wdq0 = dq(layer)
torch.manual_seed(21)
layer.sdnq_calib_rms = torch.rand(IN_F) * 10 + 0.1 # arbitrary positive statistics: unscale must round-trip
activate(net)
rho = rho_of(dq(layer) - Wdq0, D)
activate()
del layer.sdnq_calib_rms
assert rho > 0.95, f'rank-8 delta under weighted cap 64 must be kept nearly whole: rho={rho:.4f}'
assert torch.equal(dq(layer), Wdq0)
return True
def test_calib_option_off_matches_plain():
layer = build_layer('uint4')
torch.manual_seed(31)
D = torch.randn(OUT_F, IN_F, device=DEVICE) * 3e-4
net = make_dense_net('caloff', layer, D)
with host_rank(64), mock_model(lin=layer):
Wdq0 = dq(layer)
with host_calib(False):
layer.sdnq_calib_rms = torch.rand(IN_F) + 0.5
activate(net)
off = dq(layer)
activate()
del layer.sdnq_calib_rms
with host_calib(True):
activate(net) # no statistics attribute: plain truncation
plain = dq(layer)
activate()
assert torch.equal(off, plain), 'option off must reproduce the uncalibrated truncation bit-exact'
assert torch.equal(dq(layer), Wdq0)
return True
class MockCheckpointInfo:
def __init__(self, name):
self.name = name
class MockCalibSd:
def __init__(self, name, **layers):
self.transformer = MockHolder()
for attr, lyr in layers.items():
setattr(self.transformer, attr, lyr)
self.sd_checkpoint_info = MockCheckpointInfo(name)
def test_calib_capture_persist_roundtrip():
import tempfile
from modules.lora import lora_calib
layer_a = build_layer('uint4', seed=41)
layer_b = build_layer('uint4', seed=42)
sd = MockCalibSd('test/calib-model', la=layer_a, lb=layer_b)
old_root, old_tokens = lora_calib.calib_root, lora_calib.TOKENS_DONE
with tempfile.TemporaryDirectory() as tmp, host_calib(True):
try:
lora_calib.calib_root = tmp
lora_calib.TOKENS_DONE = 2048
lora_calib.on_model_loaded(sd)
assert len(lora_calib.capture['handles']) == 2, 'both sub-8-bit linears must hook'
torch.manual_seed(51)
scale = torch.linspace(0.1, 4.0, IN_F, device=DEVICE)
xs = []
for _ in range(2): # exactly the completion threshold, so statistics cover every forward
x = (torch.randn(1024, IN_F, device=DEVICE) * scale).to(torch.bfloat16)
xs.append(x.float())
layer_a(x)
layer_b(x)
assert lora_calib.capture['complete'], 'capture must complete once enough tokens are seen'
path = lora_calib.calib_file('test/calib-model')
assert os.path.isfile(path), f'statistics must persist to {path}'
expected = torch.cat(xs).square().mean(dim=0).sqrt().cpu()
assert torch.allclose(layer_a.sdnq_calib_rms, expected, rtol=1e-3, atol=1e-5), 'streamed rms must match the seen activations'
del layer_a.sdnq_calib_rms, layer_b.sdnq_calib_rms
lora_calib.on_model_loaded(sd) # second load takes the cached path
assert len(lora_calib.capture['handles']) == 0, 'cached statistics must not re-attach capture hooks'
assert torch.allclose(layer_a.sdnq_calib_rms, expected, rtol=1e-3, atol=1e-5), 'reload must restore the persisted rms'
del layer_a.sdnq_calib_rms, layer_b.sdnq_calib_rms
finally:
lora_calib.calib_root, lora_calib.TOKENS_DONE = old_root, old_tokens
lora_calib.detach_capture()
return True
def test_calib_capture_gates():
from modules.lora import lora_calib
sd_int8 = MockCalibSd('test/calib-int8', lin=build_layer('int8', seed=43))
with host_calib(True):
lora_calib.on_model_loaded(sd_int8)
assert len(lora_calib.capture['handles']) == 0, 'int8-only models have nothing to calibrate'
sd_u4 = MockCalibSd('test/calib-gates', lin=build_layer('uint4', seed=44))
with host_calib(False):
lora_calib.on_model_loaded(sd_u4)
assert len(lora_calib.capture['handles']) == 0, 'option off must disable capture'
old_compile = getattr(shared.opts, 'cuda_compile', None)
with host_calib(True):
shared.opts.cuda_compile = ['Model']
try:
lora_calib.on_model_loaded(sd_u4)
assert len(lora_calib.capture['handles']) == 0, 'model compile must disable capture'
finally:
shared.opts.cuda_compile = old_compile
lora_calib.detach_capture()
return True
CAT_ROBUST = category('robustness')
def test_remove_factors_after_device_move():
layer = build_layer('uint4', use_svd=True) # checkpoint svd correction so the stash holds real tensors
A, B, _D = make_delta()
net = make_net('mover', layer, A, B)
with mock_model(lin=layer):
Wdq0 = dq(layer)
orig_up = layer.svd_up.detach().clone()
activate(net)
assert hasattr(layer, 'sdnq_lora_svd_stash')
layer.to('cpu') # offload moves registered params, never the stash tuple
activate()
assert layer.svd_up.device == layer.scale.device, f'restored svd must live on the layer device, got {layer.svd_up.device} vs {layer.scale.device}'
assert torch.equal(layer.svd_up, orig_up.to('cpu')), 'restored svd values must match the original factors'
layer.to(DEVICE)
assert torch.equal(dq(layer), Wdq0), 'round trip must restore bit-exact'
return True
def test_stacked_shape_mismatch_falls_back():
from types import SimpleNamespace
layer = build_layer('uint4')
A, B, _D = make_delta()
net_good = make_net('good', layer, A, B)
torch.manual_seed(9)
A_bad = torch.randn(RANK, IN_F, device=DEVICE) * 0.01
B_bad = torch.randn(OUT_F // 2, RANK, device=DEVICE) * 0.01 # wrong out_features for this layer
net_bad = make_net('badshape', layer, A_bad, B_bad)
prev_enl = l_common.extra_network_lora
l_common.extra_network_lora = SimpleNamespace(errors={}) # the error path reports through the extra-networks registry
try:
with host_rank(0), mock_model(lin=layer):
Wdq0 = dq(layer)
activate(net_good)
assert hasattr(layer, 'sdnq_lora_svd_stash')
activate(net_good, net_bad) # must not raise: a malformed stack downgrades the layer to the legacy path
assert not hasattr(layer, 'sdnq_lora_svd_stash'), 'shape-mismatched stack must leave factor mode'
activate()
assert torch.equal(dq(layer), Wdq0), 'unload must restore bit-exact pristine'
finally:
l_common.extra_network_lora = prev_enl
return True
def run_tests():
t0 = time.time()
log.warning('=== Erasure law ===')
for fn in [test_uint4_erases_substep_delta, test_int8_retains_delta]:
run_test(CAT_LAW, fn)
log.warning('=== Factor path ===')
for fn in [test_apply_exact_and_remove_bitexact, test_multiplier_and_alpha_scaling, test_stacking_two_networks, test_matmul_layout_transposed, test_dora_falls_back, test_no_hadamard_checkpoint, test_checkpoint_svd_factors_preserved]:
run_test(CAT_FACTOR, fn)
log.warning('=== Memory accounting ===')
for fn in [test_factor_path_memory_is_factors_only, test_backup_mode_clones_full_quant_state, test_fuse_mode_marker_takes_no_memory]:
run_test(CAT_MEM, fn)
log.warning('=== Activate integration ===')
for fn in [test_network_activate_roundtrip]:
run_test(CAT_E2E, fn)
log.warning('=== Set transitions ===')
for fn in [test_mixed_family_transition_restores_base, test_partial_coverage_layers_stay_independent,
test_mechanism_gate_declines_candidates, test_requantize_option_routes_to_legacy_path,
test_mechanism_flip_strips_attached_factors, test_mechanism_flip_restore_pass_strips]:
run_test(CAT_TRANS, fn)
log.warning('=== Hosting ===')
for fn in [test_hosted_low_rank_delta_is_kept, test_hosted_dense_delta_beats_requant, test_hosted_skips_int8,
test_hosted_disabled_by_option, test_hosted_transitions_and_rng_isolation]:
run_test(CAT_HOST, fn)
log.warning('=== Calibration ===')
for fn in [test_calibrated_hosting_beats_plain, test_calibrated_low_rank_delta_survives, test_calib_option_off_matches_plain,
test_calib_capture_persist_roundtrip, test_calib_capture_gates]:
run_test(CAT_CALIB, fn)
log.warning('=== Robustness ===')
for fn in [test_remove_factors_after_device_move, test_stacked_shape_mismatch_falls_back]:
run_test(CAT_ROBUST, fn)
elapsed = time.time() - t0
log.warning('=== Results ===')
total_pass = total_fail = 0
for cat, info in results.items():
status = 'PASS' if info['failed'] == 0 else 'FAIL'
log.info(f' {cat}: {info["passed"]} passed, {info["failed"]} failed [{status}]')
total_pass += info['passed']
total_fail += info['failed']
log.warning(f'Total: {total_pass} passed, {total_fail} failed in {elapsed:.2f}s')
return total_fail == 0
if __name__ == '__main__':
with torch.inference_mode():
ok = run_tests()
sys.exit(0 if ok else 1)
+12 -9
View File
@@ -414,7 +414,7 @@
{"id":"","label":"Desktop","localized":"","hint":"","ui":"settings_ui"},
{"id":"","label":"Downscale high resolution live previews","localized":"","hint":"","ui":"settings_live-preview"},
{"id":"","label":"Detailer use model augment","localized":"","hint":"Run detailer detection models at extra precision","ui":"settings_postprocessing"},
{"id":"","label":"Default strength","localized":"","hint":"When adding extra network such as Lora to prompt, use this multiplier for it","ui":"settings_extra_networks"},
{"id":"","label":"Default strength","localized":"","hint":"When adding extra network such as Lora to prompt, use this multiplier for it","ui":"settings_lora"},
{"id":"","label":"Do not change selected model when reading generation parameters","localized":"","hint":"","ui":"settings_legacy_options"},
{"id":"","label":"Do conditional and unconditional denoising in one batch","localized":"","hint":"","ui":"settings_legacy_options"},
{"id":"","label":"Disable NaN check","localized":"","hint":"","ui":"settings_legacy_options"},
@@ -857,14 +857,17 @@
{"id":"","label":"Log view update period","localized":"","hint":"Log view update period, in milliseconds","ui":"settings_ui"},
{"id":"","label":"Live preview display period","localized":"","hint":"Request preview image every n steps, set to 0 to disable","ui":"settings_live-preview"},
{"id":"","label":"Load custom Diffusers pipeline","localized":"","hint":"","ui":"settings_huggingface"},
{"id":"","label":"LoRA force reload always","localized":"","hint":"Forces LoRA networks to reload from storage on every generation, even if already cached.<br>Useful for debugging or when LoRA files are being modified externally.<br>Disable for normal use to benefit from caching.","ui":"settings_extra_networks"},
{"id":"","label":"LoRA load using Diffusers method","localized":"","hint":"Alternative method uses diffusers built-in LoRA capabilities instead of native SD.Next implementation (may reduce LoRA compatibility)","ui":"settings_extra_networks"},
{"id":"","label":"LoRA native apply to text encoder","localized":"","hint":"","ui":"settings_extra_networks"},
{"id":"","label":"LoRA native fuse with model","localized":"","hint":"Merge LoRA into the model for lower memory usage.<br><br><b style=\"color: #ef4444\">Warning:</b> After removing or switching a LoRA, you may still see its style in generated images. To get a clean model, reload it from the model selector.","ui":"settings_extra_networks"},
{"id":"","label":"LoRA diffusers fuse with model","localized":"","hint":"Merge LoRA into the model for lower memory usage and torch.compile compatibility.<br><br><b style=\"color: #ef4444\">Warning:</b> After removing or switching a LoRA, you may still see its style in generated images. To get a clean model, reload it from the model selector.","ui":"settings_extra_networks"},
{"id":"","label":"LoRA auto-apply tags","localized":"","hint":"Automatically add trigger words/tags from LoRA metadata to your prompt.<br>Set to the number of tags to auto-apply, e.g., 3 = add top 3 trigger tags.<br>Set to 0 to disable, -1 to add all available tags.","ui":"settings_extra_networks"},
{"id":"","label":"LoRA memory cache","localized":"","hint":"How many LoRAs to keep in network for future use before requiring reloading from storage","ui":"settings_extra_networks"},
{"id":"","label":"LoRA add hash info to metadata","localized":"","hint":"Include LoRA file hashes in generated image metadata.<br>Useful for reproducibility and tracking which exact LoRA versions were used.","ui":"settings_extra_networks"},
{"id":"","label":"LoRA force reload always","localized":"","hint":"Forces LoRA networks to reload from storage on every generation, even if already cached.<br>Useful for debugging or when LoRA files are being modified externally.<br>Disable for normal use to benefit from caching.","ui":"settings_lora"},
{"id":"","label":"LoRA load using Diffusers method","localized":"","hint":"Alternative method uses diffusers built-in LoRA capabilities instead of native SD.Next implementation (may reduce LoRA compatibility)","ui":"settings_lora"},
{"id":"","label":"LoRA native apply to text encoder","localized":"","hint":"","ui":"settings_lora"},
{"id":"","label":"LoRA native fuse with model","localized":"","hint":"Merge LoRA into the model for lower memory usage.<br><br><b style=\"color: #ef4444\">Warning:</b> After removing or switching a LoRA, you may still see its style in generated images. To get a clean model, reload it from the model selector.","ui":"settings_lora"},
{"id":"","label":"LoRA diffusers fuse with model","localized":"","hint":"Merge LoRA into the model for lower memory usage and torch.compile compatibility.<br><br><b style=\"color: #ef4444\">Warning:</b> After removing or switching a LoRA, you may still see its style in generated images. To get a clean model, reload it from the model selector.","ui":"settings_lora"},
{"id":"","label":"LoRA quantized apply method","localized":"","hint":"How networks are applied to SDNQ-quantized model weights:<br>- <b>exact</b>: adapters are carried alongside the quantized weights at full precision; apply and removal are exact and the quantized weights are never modified. The carried factors take additional VRAM, growing with adapter rank, size and count<br>- <b>requantize</b>: adapters are merged into the quantized weights, matching the behavior of earlier releases. Uses no additional VRAM (a weight backup for network removal is held in system RAM); on models quantized below 8 bits rounding typically loses much of the adapter effect, with strong adapters retaining more<br><br>With <b>requantize</b> selected, the host rank and calibration options below have no effect.<br><br>Default is <b>exact</b>.","ui":"settings_lora"},
{"id":"","label":"LoRA quantized host rank","localized":"","hint":"Maximum rank used to carry non-factorable adapter types (LoKR, LoHA, OFT, DoRA) on the side-channel of SDNQ models quantized below 8 bits, where merging would erase most of the adapter. Higher values keep more of the adapter at proportionally higher memory cost. Set to 0 to disable and merge into the quantized weights instead.","ui":"settings_lora"},
{"id":"","label":"LoRA quantized host calibration","localized":"","hint":"Collection of per-channel activation statistics during normal generations on models quantized below 8 bits, cached per checkpoint. When available, the statistics weight side-channel hosting of non-factorable adapter types toward the channels carrying the most activation energy, improving delivered fidelity at the same host rank.","ui":"settings_lora"},
{"id":"","label":"LoRA auto-apply tags","localized":"","hint":"Automatically add trigger words/tags from LoRA metadata to your prompt.<br>Set to the number of tags to auto-apply, e.g., 3 = add top 3 trigger tags.<br>Set to 0 to disable, -1 to add all available tags.","ui":"settings_lora"},
{"id":"","label":"LoRA memory cache","localized":"","hint":"How many LoRAs to keep in network for future use before requiring reloading from storage","ui":"settings_lora"},
{"id":"","label":"LoRA add hash info to metadata","localized":"","hint":"Include LoRA file hashes in generated image metadata.<br>Useful for reproducibility and tracking which exact LoRA versions were used.","ui":"settings_lora"},
{"id":"","label":"LDSR Path","localized":"","hint":"","ui":"settings_legacy_options"},
{"id":"","label":"LoRA load using legacy method","localized":"","hint":"","ui":"settings_legacy_options"},
{"id":"","label":"Loaded LoRA","localized":"","hint":"","ui":"component-5851"},