Per-block LoRA: type-zone suppression, semantic anchor handling, quality signal

- Add LORA_TYPE_BLOCK_ZONES: type-aware block zones that suppress bad-fit
  blocks to 0.0 and mildly boost good-zone blocks per LoRA type
- Add SEMANTIC_ANCHOR_BLOCKS {14, 19}: confirmed semantic focal points
  (PAG default=14, STG defaults=14,19) protected from all suppression
- Add _semantic_anchor_quality: uses LoRA energy share in blocks 14+19
  as a quality signal, scaling all block weights 0.92-1.08x accordingly
- Stack priority: weaker LoRA keeps base scale on anchor blocks instead
  of being damped, letting the dominant signal carry semantic content
- Fix range display: status line now shows effective weights (model_weight
  * scale) instead of raw multipliers; adds zeroed= and anchor_q= fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-05-18 19:14:13 +03:00
co-authored by Claude Sonnet 4.6
parent 7a4f86a695
commit 51a9478dd7
+88 -12
View File
@@ -31,6 +31,22 @@ LORA_BLOCK_TYPE_PROFILES = {
"style": {"priority": 0.96, "yield": 0.96},
"general": {"priority": 0.90, "yield": 1.12},
}
# Normalized block position zones (0.0=first block, 1.0=last block).
# good: primary contribution zone - blocks here get a mild boost
# bad: blocks here actively work against the type's purpose - suppressed to 0.0
LORA_TYPE_BLOCK_ZONES = {
"character": {"good": (0.45, 1.00), "bad": (0.00, 0.20)},
"action": {"good": (0.20, 0.80), "bad": None},
"concept": {"good": (0.20, 0.80), "bad": None},
"quality": {"good": None, "bad": None},
"style": {"good": (0.00, 0.50), "bad": (0.65, 1.00)},
"general": {"good": None, "bad": None},
}
LORA_ZONE_BOOST = 1.12
LORA_ZONE_SCALE_CAP = 1.75
# Blocks confirmed as primary semantic focal points (PAG default=14, STG defaults=14,19).
# These are never zeroed by type-zone suppression and never damped by stack pressure.
SEMANTIC_ANCHOR_BLOCKS = frozenset({14, 19})
@PromptServer.instance.routes.get("/funpack/loras")
@@ -597,12 +613,56 @@ class FunPackLoraLoader:
self._cache_put(self.block_profile_cache, patch_cache_key, template, LORA_PROFILE_CACHE_SIZE)
return template
def _apply_type_zone_scales(self, scales, entry):
lora_type = normalize_lora_type(entry.get("type", "general"))
zone = LORA_TYPE_BLOCK_ZONES.get(lora_type, {})
good = zone.get("good")
bad = zone.get("bad")
if not good and not bad:
return scales, 0
block_indices = list(scales.keys())
if not block_indices:
return scales, 0
max_idx = max(block_indices)
if max_idx == 0:
return scales, 0
result = dict(scales)
suppressed = 0
for block_index, scale in scales.items():
pos = block_index / max_idx
if bad and bad[0] <= pos <= bad[1]:
if block_index in SEMANTIC_ANCHOR_BLOCKS:
continue
result[block_index] = 0.0
suppressed += 1
elif good and good[0] <= pos <= good[1]:
result[block_index] = min(LORA_ZONE_SCALE_CAP, scale * LORA_ZONE_BOOST)
return result, suppressed
def _semantic_anchor_quality(self, block_scores):
total = sum(block_scores.values())
if total <= 0.0:
return 1.0
anchor_energy = sum(block_scores.get(b, 0.0) for b in SEMANTIC_ANCHOR_BLOCKS)
anchor_share = anchor_energy / total
expected = len(SEMANTIC_ANCHOR_BLOCKS) / max(len(block_scores), 1)
ratio = anchor_share / max(expected, 1e-9)
return max(0.92, min(1.08, 1.0 + (ratio - 1.0) * 0.08))
def _lora_block_profile(self, entry, loaded, model_weight, patch_cache_key=None):
template = self._block_profile_template(loaded, patch_cache_key)
if template is None:
return None
type_profile = self._block_type_profile(entry)
stack_scales, suppressed = self._apply_type_zone_scales(dict(template["base_scales"]), entry)
quality = self._semantic_anchor_quality(template["block_scores"])
stack_scales = {
k: min(LORA_ZONE_SCALE_CAP, v * quality) if v > 0.0 else v
for k, v in stack_scales.items()
}
return {
"entry": entry,
"loaded": loaded,
@@ -611,7 +671,9 @@ class FunPackLoraLoader:
"block_count": template["block_count"],
"block_scores": template["block_scores"],
"base_scales": template["base_scales"],
"stack_scales": dict(template["base_scales"]),
"stack_scales": stack_scales,
"suppressed_count": suppressed,
"anchor_quality": quality,
"normalized_scores": template["normalized_scores"],
"top_blocks": template["top_blocks"],
"concentration": template["concentration"],
@@ -652,6 +714,8 @@ class FunPackLoraLoader:
continue
for profile in contributors:
if profile["stack_scales"].get(block_index, 1.0) == 0.0:
continue
own_presence = profile["normalized_scores"].get(block_index, 0.0)
own_weight = max(0.05, abs(profile["model_weight"]))
own_signal = own_presence * own_weight * profile["priority"]
@@ -675,6 +739,8 @@ class FunPackLoraLoader:
advantage = (own_signal - strongest_other) / max(own_signal + strongest_other, 1e-9)
if advantage >= 0.0:
multiplier = 1.0 + min(0.18, advantage * 0.14) * min(1.0, overlap_ratio * 1.25)
elif block_index in SEMANTIC_ANCHOR_BLOCKS:
continue
else:
pressure = min(1.0, overlap_ratio * 1.35)
damp = min(0.35, (-advantage) * 0.22 * profile["yield"] * pressure)
@@ -710,14 +776,23 @@ class FunPackLoraLoader:
def _per_block_status(self, profile):
scales = profile["stack_scales"]
min_scale = min(scales.values())
max_scale = max(scales.values())
w = profile["model_weight"]
active = {k: v for k, v in scales.items() if v > 0.0}
min_eff = w * min(active.values()) if active else 0.0
max_eff = w * max(active.values()) if active else 0.0
suppressed = profile.get("suppressed_count", 0)
quality = profile.get("anchor_quality", 1.0)
mode = "smart-per-block" if profile["overlap_score"] > 0.0 else "per-block"
return (
f"{mode} blocks={len(scales)} non_block={profile['global_count']} "
f"range={min_scale:.2f}..{max_scale:.2f} "
f"top={profile['top_blocks']} overlap={profile['overlap_score']:.2f}"
)
parts = [
f"{mode} blocks={len(scales)} non_block={profile['global_count']}",
f"range={min_eff:.3f}..{max_eff:.3f}",
f"top={profile['top_blocks']} overlap={profile['overlap_score']:.2f}",
]
if suppressed:
parts.insert(2, f"zeroed={suppressed}")
if quality != 1.0:
parts.insert(-1, f"anchor_q={quality:.2f}")
return " ".join(parts)
def _load_lora_per_block(self, model, lora, model_weight):
lora_cache_key = None
@@ -732,11 +807,12 @@ class FunPackLoraLoader:
model_weight,
block_scales=profile["stack_scales"],
)
min_scale = min(profile["stack_scales"].values())
max_scale = max(profile["stack_scales"].values())
scales = profile["stack_scales"]
min_eff = model_weight * min(scales.values())
max_eff = model_weight * max(scales.values())
status = (
f"per-block blocks={len(profile['stack_scales'])} non_block={non_block_count} "
f"range={min_scale:.2f}..{max_scale:.2f}"
f"per-block blocks={len(scales)} non_block={non_block_count} "
f"range={min_eff:.3f}..{max_eff:.3f}"
)
return new_model, status