mirror of
https://github.com/vladmandic/automatic
synced 2026-09-04 03:50:44 +02:00
2620b0cc5b
<lora:name:1.0:lbw=VALUE> scales each targeted layer's delta by a slot of a per-architecture block vector. VALUE is a preset name, a scalar, or a comma vector; presets stretch onto the block count of the current model and the a1111 17-slot and 12-slot layouts are accepted on sd and sdxl. The factor enters through the module multiplier, so every apply path carries it: the exact factor channel, hosting, requantize routing, dense stack combines and select scoring. - modules/lora/lora_blocks.py: slot classification from network_layer_mapping (namespace-first, anchored chain prefixes), preset resolution reusing the merge block-weight tables with BASE forced neutral, generated classic segment names plus DOUBLE/SINGLE chain names, per-model memoization - the raw spec stages through pending_config and promotes with the other multipliers, keeping fuse removal consistent - block weights join the activation signature, the per-module apply stamp and the factor cache identity; entries without block weights keep their existing signature bytes - non-native load methods warn once and ignore the argument
349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""Per-block LoRA strength: <lora:name:1.0:lbw=VALUE>.
|
|
|
|
Each targeted layer maps to one slot of a per-architecture weight vector and
|
|
the network's multiplier is scaled by that slot. Slot 0 is BASE: on unet
|
|
architectures it covers the text encoder and the unet layers outside the
|
|
block chain, on transformer architectures the layers outside the block
|
|
chain(s). The remaining slots follow the merge block-weight layout on unet
|
|
architectures (26 on sd, 20 on sdxl: input blocks, mid, output blocks) and
|
|
the transformer chain(s) in depth order elsewhere, with chain lengths
|
|
scanned from the live network_layer_mapping rather than hardcoded.
|
|
|
|
VALUE is a preset name (case-insensitive), a single number broadcast to
|
|
every slot, or a comma list with one number per slot. Named presets force
|
|
BASE to 1.0, since the merge tables carry 0 there with merge semantics, and
|
|
stretch onto the block count of the current model; classic segment names
|
|
(INS, OUTALL, ...) generate from ranges, so they also work on transformer
|
|
chains via thirds, and DOUBLE/SINGLE mute one chain on two-chain
|
|
architectures. Explicit vectors are taken verbatim at the slot count, with
|
|
the a1111 17-slot (sd) and 12-slot (sdxl) layouts accepted and expanded,
|
|
omitted slots neutral. A value that fits nothing is ignored with a warning
|
|
and the network applies at its plain strength.
|
|
"""
|
|
|
|
import re
|
|
|
|
from modules import shared
|
|
from modules.logger import log
|
|
from modules.lora import lora_common as l
|
|
|
|
|
|
UNET_ARCHES = ('sd', 'sdxl')
|
|
CHAINS = { # arch -> anchored tail prefixes, one per chain, in depth order
|
|
'sd3': ('transformer_blocks_',),
|
|
'anima': ('transformer_blocks_',),
|
|
'f1': ('transformer_blocks_', 'single_transformer_blocks_'),
|
|
'f2': ('transformer_blocks_', 'single_transformer_blocks_'),
|
|
'chroma': ('transformer_blocks_', 'single_transformer_blocks_'),
|
|
'zimage': ('layers_',),
|
|
'ernieimage': ('layers_',),
|
|
'krea2': ('blocks_',),
|
|
}
|
|
CLASSIC = ('ALL', 'NONE', 'INALL', 'INS', 'IND', 'MIDD', 'OUTALL', 'OUTD', 'OUTS')
|
|
CHAIN_NAMES = ('DOUBLE', 'SINGLE')
|
|
SD1_17 = (0, 2, 3, 5, 6, 8, 9, 13, 17, 18, 19, 20, 21, 22, 23, 24, 25) # BASE, IN01, IN02, IN04, IN05, IN07, IN08, MID, OUT03..OUT11
|
|
SDXL_12 = (0, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16) # BASE, IN04, IN05, IN07, IN08, MID, OUT00..OUT05
|
|
VECTOR_MEMO_CAP = 64
|
|
MISS = object()
|
|
|
|
re_down = re.compile(r'^down_blocks_(\d+)_(resnets|attentions|downsamplers)_(\d+)')
|
|
re_up = re.compile(r'^up_blocks_(\d+)_(resnets|attentions|upsamplers)_(\d+)')
|
|
re_chain_index = re.compile(r'^(\d+)')
|
|
|
|
state: dict = {'stamp': None, 'layout': None, 'index': {}, 'vectors': {}}
|
|
warned: set = set()
|
|
|
|
|
|
def warn_once(key, message):
|
|
if key not in warned:
|
|
warned.add(key)
|
|
log.warning(message)
|
|
|
|
|
|
def build_unet_layout(arch, mapping):
|
|
down, up = -1, -1
|
|
for key in mapping:
|
|
if not key.startswith('lora_unet_'):
|
|
continue
|
|
tail = key[len('lora_unet_'):]
|
|
m = re_down.match(tail)
|
|
if m is not None:
|
|
down = max(down, int(m.group(1)))
|
|
continue
|
|
m = re_up.match(tail)
|
|
if m is not None:
|
|
up = max(up, int(m.group(1)))
|
|
if down < 0 or up < 0:
|
|
return None
|
|
n_in = 3 * (down + 1) # conv_in plus two pairs and a sampler slot per group: the compvis input_blocks count
|
|
n_out = 3 * (up + 1)
|
|
n = 2 + n_in + n_out
|
|
return {
|
|
'arch': arch, 'kind': 'unet', 'n': n, 'n_in': n_in,
|
|
'ins': list(range(1, 1 + n_in)),
|
|
'mids': [1 + n_in],
|
|
'outs': list(range(2 + n_in, n)),
|
|
}
|
|
|
|
|
|
def build_dit_layout(arch, mapping):
|
|
prefixes = CHAINS.get(arch)
|
|
if prefixes is None:
|
|
return None
|
|
counts = [0 for _ in prefixes]
|
|
for key in mapping:
|
|
if not key.startswith('lora_transformer_'):
|
|
continue
|
|
tail = key[len('lora_transformer_'):]
|
|
for i, prefix in enumerate(prefixes):
|
|
if tail.startswith(prefix):
|
|
m = re_chain_index.match(tail[len(prefix):])
|
|
if m is not None:
|
|
counts[i] = max(counts[i], int(m.group(1)) + 1)
|
|
break
|
|
total = sum(counts)
|
|
if total == 0:
|
|
return None
|
|
chains = []
|
|
offset = 0
|
|
for prefix, count in zip(prefixes, counts, strict=False):
|
|
chains.append((prefix, count, offset))
|
|
offset += count
|
|
n = 1 + total
|
|
blocks = list(range(1, n))
|
|
return {
|
|
'arch': arch, 'kind': 'dit', 'n': n, 'chains': chains,
|
|
'ins': [s for i, s in enumerate(blocks) if i * 3 // total == 0],
|
|
'mids': [s for i, s in enumerate(blocks) if i * 3 // total == 1],
|
|
'outs': [s for i, s in enumerate(blocks) if i * 3 // total == 2],
|
|
}
|
|
|
|
|
|
def layout():
|
|
sd_model = getattr(shared, 'sd_model', None)
|
|
mapping = getattr(sd_model, 'network_layer_mapping', None) if sd_model is not None else None
|
|
if not mapping:
|
|
return None
|
|
arch = shared.sd_model_type
|
|
stamp = (arch, id(mapping))
|
|
if state['stamp'] == stamp:
|
|
return state['layout']
|
|
state['stamp'] = stamp
|
|
state['layout'] = build_unet_layout(arch, mapping) if arch in UNET_ARCHES else build_dit_layout(arch, mapping)
|
|
state['index'].clear()
|
|
state['vectors'].clear()
|
|
return state['layout']
|
|
|
|
|
|
def classify(sd_key, lay):
|
|
if sd_key.startswith('lora_te'):
|
|
return 0 if lay['kind'] == 'unet' else None # BASE covers the TE on unet arches; transformer vectors do not model the TE
|
|
if sd_key.startswith('lora_llm_adapter_'):
|
|
return None
|
|
if lay['kind'] == 'unet':
|
|
if not sd_key.startswith('lora_unet_'):
|
|
return None
|
|
tail = sd_key[len('lora_unet_'):]
|
|
m = re_down.match(tail)
|
|
if m is not None:
|
|
slot = 1 + 3 * int(m.group(1)) + (2 if m.group(2) == 'downsamplers' else int(m.group(3)))
|
|
return 1 + slot
|
|
m = re_up.match(tail)
|
|
if m is not None:
|
|
slot = 3 * int(m.group(1)) + (2 if m.group(2) == 'upsamplers' else int(m.group(3)))
|
|
return 2 + lay['n_in'] + slot
|
|
if tail.startswith('mid_block'):
|
|
return 1 + lay['n_in']
|
|
if tail.startswith('conv_in'):
|
|
return 1 # IN00
|
|
if tail.startswith('conv_out') or tail.startswith('conv_norm_out'):
|
|
return lay['n'] - 1 # the compvis out group belongs to the last output block
|
|
return 0 # time_embedding, add_embedding and other non-block leaves
|
|
if not sd_key.startswith('lora_transformer_'):
|
|
return None
|
|
tail = sd_key[len('lora_transformer_'):]
|
|
for prefix, count, offset in lay['chains']:
|
|
if tail.startswith(prefix):
|
|
m = re_chain_index.match(tail[len(prefix):])
|
|
if m is not None and int(m.group(1)) < count:
|
|
return 1 + offset + int(m.group(1))
|
|
return 0
|
|
return 0 # embedders, projections, refiners and other non-chain layers
|
|
|
|
|
|
def block_index(sd_key):
|
|
lay = layout()
|
|
if lay is None:
|
|
return None
|
|
cached = state['index'].get(sd_key, MISS)
|
|
if cached is not MISS:
|
|
return cached
|
|
idx = classify(sd_key, lay)
|
|
state['index'][sd_key] = idx
|
|
return idx
|
|
|
|
|
|
def fill_band(vec, slots, lo, hi):
|
|
k = len(slots)
|
|
for i, s in enumerate(slots):
|
|
if lo * k <= i < hi * k:
|
|
vec[s] = 1.0
|
|
|
|
|
|
def classic_vector(name, lay):
|
|
if name == 'ALL':
|
|
return [1.0] * lay['n']
|
|
vec = [0.0] * lay['n']
|
|
if name == 'NONE':
|
|
return vec
|
|
vec[0] = 1.0
|
|
if name == 'INALL':
|
|
fill_band(vec, lay['ins'], 0.0, 1.0)
|
|
elif name == 'INS': # shallow half of the input side
|
|
fill_band(vec, lay['ins'], 0.0, 0.5)
|
|
elif name == 'IND': # deep half of the input side
|
|
fill_band(vec, lay['ins'], 0.5, 1.0)
|
|
elif name == 'MIDD': # the middle of the network: deep input half, mid, deep output half
|
|
fill_band(vec, lay['ins'], 0.5, 1.0)
|
|
fill_band(vec, lay['mids'], 0.0, 1.0)
|
|
fill_band(vec, lay['outs'], 0.0, 0.5)
|
|
elif name == 'OUTALL':
|
|
fill_band(vec, lay['outs'], 0.0, 1.0)
|
|
elif name == 'OUTD': # deep half of the output side, nearest the mid
|
|
fill_band(vec, lay['outs'], 0.0, 0.5)
|
|
elif name == 'OUTS': # shallow half of the output side, nearest the image
|
|
fill_band(vec, lay['outs'], 0.5, 1.0)
|
|
return vec
|
|
|
|
|
|
def chain_vector(name, lay):
|
|
chains = lay.get('chains') or []
|
|
if len(chains) != 2:
|
|
return None
|
|
vec = [1.0] * lay['n']
|
|
keep = 0 if name == 'DOUBLE' else 1
|
|
for i, (_prefix, count, offset) in enumerate(chains):
|
|
val = 1.0 if i == keep else 0.0
|
|
for s in range(1 + offset, 1 + offset + count):
|
|
vec[s] = val
|
|
return vec
|
|
|
|
|
|
def stretch(src, k):
|
|
if k == len(src):
|
|
return [float(v) for v in src]
|
|
out = []
|
|
for i in range(k):
|
|
x = i * (len(src) - 1) / (k - 1) if k > 1 else 0.0
|
|
lo = int(x)
|
|
hi = min(lo + 1, len(src) - 1)
|
|
f = x - lo
|
|
out.append(float(src[lo]) * (1.0 - f) + float(src[hi]) * f)
|
|
return out
|
|
|
|
|
|
def preset_vector(name, lay):
|
|
from modules.merging.merge_presets import BLOCK_WEIGHTS_PRESETS, SDXL_BLOCK_WEIGHTS_PRESETS
|
|
if name in CHAIN_NAMES:
|
|
return chain_vector(name, lay)
|
|
if name in CLASSIC:
|
|
return classic_vector(name, lay)
|
|
if lay['arch'] == 'sdxl':
|
|
src = SDXL_BLOCK_WEIGHTS_PRESETS.get(name) or SDXL_BLOCK_WEIGHTS_PRESETS.get('SDXL_' + name)
|
|
if src is not None:
|
|
return [1.0] + [float(v) for v in src[1:]] # merge tables carry 0 in the BASE slot; a preset must leave the TE alone
|
|
if name.startswith('SDXL_'):
|
|
return None # explicitly arch-tagged, not reinterpreted elsewhere
|
|
src = BLOCK_WEIGHTS_PRESETS.get(name)
|
|
if src is None:
|
|
return None
|
|
if lay['arch'] == 'sd':
|
|
return [1.0] + [float(v) for v in src[1:]]
|
|
return [1.0] + stretch(src[1:], lay['n'] - 1)
|
|
|
|
|
|
def parse_vector(parts, lay):
|
|
try:
|
|
vals = [float(x) for x in parts]
|
|
except ValueError:
|
|
return None
|
|
n = lay['n']
|
|
if len(vals) == n:
|
|
return vals
|
|
if len(vals) == n - 1:
|
|
return [1.0] + vals
|
|
legacy = SD1_17 if lay['arch'] == 'sd' else (SDXL_12 if lay['arch'] == 'sdxl' else None)
|
|
if legacy is not None and len(vals) == len(legacy):
|
|
vec = [1.0] * n # slots the a1111 layouts omit stay neutral
|
|
for slot, v in zip(legacy, vals, strict=False):
|
|
vec[slot] = v
|
|
return vec
|
|
return None
|
|
|
|
|
|
def resolve(spec):
|
|
"""Resolve a raw lbw value into a slot vector for the current model, or None when it fits nothing."""
|
|
lay = layout()
|
|
if lay is None:
|
|
return None
|
|
raw = str(spec).strip()
|
|
key = raw.lower()
|
|
if key in state['vectors']:
|
|
return state['vectors'][key]
|
|
if len(state['vectors']) > VECTOR_MEMO_CAP:
|
|
state['vectors'].clear()
|
|
vec = None
|
|
if ',' in raw:
|
|
vec = parse_vector([x.strip() for x in raw.split(',')], lay)
|
|
if vec is None:
|
|
warn_once(f'lbw-vector:{key}:{lay["arch"]}', f'Network blocks: value="{raw}" arch={lay["arch"]} expected={lay["n"]} fallback=none')
|
|
else:
|
|
try:
|
|
vec = [float(raw)] * lay['n']
|
|
except ValueError:
|
|
vec = preset_vector(raw.upper(), lay)
|
|
if vec is None:
|
|
warn_once(f'lbw-name:{key}:{lay["arch"]}', f'Network blocks: preset="{raw}" arch={lay["arch"]} fallback=none')
|
|
if vec is not None:
|
|
log.info(f'Network blocks: value="{raw}" arch={lay["arch"]} slots={lay["n"]} range={min(vec):.2f}-{max(vec):.2f}')
|
|
state['vectors'][key] = vec
|
|
return vec
|
|
|
|
|
|
def factor(sd_key, net):
|
|
"""Per-layer scale from a network's block vector; 1.0 whenever the vector does not apply."""
|
|
try:
|
|
spec = getattr(net, 'block_spec', None)
|
|
if not spec:
|
|
return 1.0
|
|
vec = resolve(spec)
|
|
if vec is None:
|
|
return 1.0
|
|
idx = block_index(sd_key)
|
|
if idx is None:
|
|
return 1.0
|
|
return float(vec[idx])
|
|
except Exception as e:
|
|
warn_once('lbw-error', f'Network blocks: {e} fallback=none')
|
|
return 1.0
|
|
|
|
|
|
def net_signature(net):
|
|
"""Normalized spec of one network, or None; joins content identities such as the factor cache signature."""
|
|
spec = getattr(net, 'block_spec', None)
|
|
if not spec:
|
|
return None
|
|
return str(spec).strip().lower()
|
|
|
|
|
|
def active():
|
|
return any(getattr(net, 'block_spec', None) for net in l.loaded_networks)
|
|
|
|
|
|
def signature():
|
|
"""Identity suffix for the per-module apply stamp; empty while no loaded network carries block weights."""
|
|
specs = [f'{net.name}:{net_signature(net)}' for net in l.loaded_networks if getattr(net, 'block_spec', None)]
|
|
if len(specs) == 0:
|
|
return ''
|
|
return '|lbw=' + ','.join(specs)
|