Files
ethanfel-ComfyUI-LoRA-Optim…/docs/wiki/Merge-Algorithms.md
T
EthanfelandClaude Opus 4.8 785983d13f fix(merge): revert automatic sum_preserve; style preservation is preserve-flag only
A real 3-LoRA merge (characters/concept, no style) oversaturated badly (energy
2.52x vs main's 1.02x): additive stacking is wrong for a balanced multi-LoRA blend.
The analyzer cannot tell 'preserve this style' from 'blend these characters' — both
are orthogonal — so additive must never be auto-selected; it has to be user intent.

- Revert _decide_prefix_mode: orthogonal groups blend by default again
  (weighted_average / SLERP), exactly like main. No auto sum_preserve.
- Make the preserve flag a general overlay in _merge_diffs: blend the non-preserved
  LoRAs with the chosen mode, then add each preserved LoRA's full delta on top
  (works for weighted_average/slerp/ties; still exempt from sparsification + TIES
  sign-election). Skip the exact-linear fast path when the stack has a preserve flag.
- Remove the standalone sum_preserve mode, its exact-linear handling,
  _ORTHO_SUM_HEADROOM, and the sum+ report/legend/strategy-count display.
- Tests: SumPreserveMergeTests -> PrefixModeRoutingTests (asserts additive is never
  auto-selected); add preserve-overlay-on-weighted_average tests. 299 pass.
- Docs (Merge-Algorithms, Tips) + design-doc postmortem updated; algo note reworded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:53:36 +02:00

385 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Merge Algorithms
Deep dive into each merge algorithm: the math, when each is selected, and how they interact with sparsification and quality enhancements.
For the overall pipeline and composition order, see [[How It Works]].
---
## Overview
The optimizer has five merge algorithms. Each prefix in the model can use a different one based on local conflict data.
| Algorithm | Best For | Properties |
|-----------|----------|------------|
| [Weighted Sum](#weighted-sum) | Single-LoRA prefixes | Lossless, fully SVD-compressible |
| [Weighted Average](#weighted-average) | Low-conflict overlapping prefixes | Lossless, fully SVD-compressible |
| [TIES](#ties-merging) | High-conflict prefixes (>25% sign conflict) | Nonlinear, lossy SVD |
| [SLERP](#slerp) | Low-conflict, similar-magnitude LoRAs | Magnitude-preserving |
| [Consensus](#consensus-merging) | Highly similar LoRAs (high cosine similarity) | Importance-weighted, spectral cleanup |
> **Preserving a style LoRA.** Orthogonal LoRAs are *blended* (weighted_average / SLERP) by default — the right behaviour for combining characters/concepts, but it averages a style LoRA down. To keep one LoRA at full strength, turn on its **`preserve`** flag on the Stack node: the rest blend normally and the tagged LoRA's full delta is added on top. This is opt-in — the analyzer can't tell "preserve this style" from "blend these characters", so it never does additive automatically. See [Tips → "my style LoRA disappears"](Tips-and-Troubleshooting#my-style-lora-disappears-when-merged-with-a-contentcharacter-lora).
---
## Weighted Sum
The simplest merge: plain scaled addition.
### Formula
```
output[i] = Σ_k (strength_k × diff_k[i])
```
Each LoRA's contribution is scaled by its strength and summed. No normalization, no conflict resolution.
### When Selected
- Automatically used for prefixes where **only one LoRA** has weights (no overlap, no conflict)
- Forced everywhere when `optimization_mode=additive`
### Properties
- Preserves every contribution exactly
- No information loss — output is the exact linear combination
- Fully compressible by SVD (rank = sum of input ranks)
- Can oversaturate when LoRAs reinforce each other at high strength
---
## Weighted Average
Normalized sum that prevents magnitude inflation.
### Formula
```
total_weight = Σ_k |strength_k|
output[i] = Σ_k (strength_k / total_weight × diff_k[i])
```
Same as weighted sum, but divided by the sum of strengths. This ensures the output magnitude stays proportional to individual LoRAs rather than growing with the number of LoRAs.
### When Selected
- Per-prefix: 2+ LoRAs present AND sign conflict ratio ≤ 25%
- Global: aggregate conflict ≤ 25% AND cosine similarity between 0.3–0.7
- Can be upgraded to SLERP in the `full` strategy set when LoRAs are orthogonal
### Properties
- Fair blending — each LoRA contributes proportionally (the right default for combining characters/concepts)
- Prevents magnitude inflation from stacking
- Fully compressible by SVD (linear operation)
- **Note:** because it divides by `Σ|strength|`, a LoRA's *absolute* contribution is a convex fraction — a single style LoRA gets averaged down. That is intentional for blends; to hold one LoRA at full strength instead, use its **`preserve`** flag (it is then added on top of the blend rather than averaged in).
---
## TIES Merging
**T**rim, **I**nterfere, **E**lect **S**ign — specifically designed to resolve sign conflicts.
*Reference: [Yadav et al., NeurIPS 2023](https://arxiv.org/abs/2306.01708)*
### Algorithm
TIES has three steps applied at each position independently:
#### Step 1 — Trim (Sparsification)
Keep only the top-k% of weights by absolute magnitude. The rest are zeroed out.
```
for each LoRA k:
threshold_k = quantile(|diff_k|, 1 - density)
trimmed_k[i] = diff_k[i] if |diff_k[i]| ≥ threshold_k else 0
```
The `density` parameter controls how aggressive the trim is.
**Interaction with DARE/DELLA:** If sparsification is enabled, it **replaces** this trim step entirely. Both achieve the same goal (zeroing out low-value weights), so running both would be redundant.
#### Step 2 — Elect Sign
For each position, vote on the majority sign direction across all LoRAs.
**Frequency method** (equal votes):
```
elected_sign[i] = sign(Σ_k sign(trimmed_k[i]))
```
Each LoRA gets one vote per position.
**Total method** (magnitude-weighted votes):
```
elected_sign[i] = sign(Σ_k trimmed_k[i])
```
Larger magnitudes carry more weight in the vote.
The optimizer picks the method based on magnitude ratio:
- Ratio > 2× → `total` (stronger LoRA dominates)
- Ratio ≤ 2× → `frequency` (equal votes)
#### Step 3 — Disjoint Merge
Average only the LoRAs that agree with the elected sign. Contributors opposing the majority are excluded.
```
agreeing_k = {k : sign(trimmed_k[i]) == elected_sign[i]}
output[i] = Σ_{k ∈ agreeing_k} (strength_k × trimmed_k[i]) / Σ_{k ∈ agreeing_k} |strength_k|
```
### When Selected
- Per-prefix: 2+ LoRAs present AND sign conflict ratio > 25%
- Global: aggregate conflict ratio > 25%
### Properties
- Resolves sign conflicts without cancellation — opposing LoRAs don't interfere
- Nonlinear operations (trim + sign election) produce full-rank output
- SVD compression is lossy on TIES prefixes
- Best for genuinely conflicting LoRAs
---
## SLERP
**S**pherical **L**inear int**ERP**olation — magnitude-preserving directional blend along the geodesic on a hypersphere.
### Formula (2 LoRAs)
```
Ω = arccos(cos_sim(diff_A, diff_B))
output = sin((1-t)Ω)/sin(Ω) × diff_A + sin(tΩ)/sin(Ω) × diff_B
```
Where `t` is the interpolation weight derived from LoRA strengths.
For near-parallel vectors (Ω ≈ 0), falls back to linear interpolation to avoid numerical instability.
### Formula (3+ LoRAs)
Iterative pairwise SLERP, sorted by descending weight:
```
1. Sort diffs by strength (descending)
2. result = diff_strongest
3. for each remaining diff_k (by decreasing strength):
t_k = adjusted interpolation weight
result = slerp(result, diff_k, t_k)
4. Correct final magnitude to match weighted average of input norms
```
The strongest LoRA anchors the direction; subsequent LoRAs blend in.
### When Selected
- `full` strategy set: auto-selected when conflict ≤ 25% AND LoRAs are detected as orthogonal (low cosine similarity)
- Not auto-selected in the `no_slerp` or `basic` strategy sets
- Can be forced via `merge_strategy_override`
### Properties
- Preserves weight magnitude (no inflation or deflation)
- Smooth directional interpolation on the unit sphere
- Magnitude correction ensures output norm matches expected value
- Best for low-conflict LoRAs of similar magnitude
---
## Consensus Merging
Three-stage algorithm designed for highly similar LoRAs where importance-weighting and spectral cleanup produce cleaner results than simple averaging.
### Stage 1 — Fisher-Proxy Importance Weighting
Weight each parameter by `|diff|²` as a proxy for Fisher information (parameter importance):
```
numerator[i] = Σ_k (diff_k[i] × strength_k × |diff_k[i]|²)
denominator[i] = Σ_k (|strength_k| × |diff_k[i]|²)
output[i] = numerator[i] / denominator[i]
```
Parameters where LoRAs make large changes get more influence. Positions with near-zero changes contribute little.
### Stage 2 — MAGIC Calibration
Rescale the merged result so its L2 norm matches the weighted average of input norms:
```
target_norm = Σ_k (|strength_k| × ||diff_k||₂) / Σ_k |strength_k|
output = output × (target_norm / ||output||₂)
```
This prevents magnitude drift from the importance weighting.
### Stage 3 — MonoSoup Spectral Cleanup
SVD-based entropy filtering on 2D+ tensors:
```
1. Compute SVD: output = U × S × Vᵀ
2. Compute entropy of singular values: H = -Σ (p_i × log(p_i))
where p_i = s_i / Σ s_j
3. Effective rank = exp(H)
4. Keep only top effective_rank singular values
5. Reconstruct: output = U[:,:r] × S[:r] × Vᵀ[:r,:]
```
This removes noise — singular values with negligible contribution are filtered out, producing a cleaner merge.
### When Selected
- `full` strategy set: auto-selected when conflict ≤ 25% AND cosine similarity > 0.7 (highly similar LoRAs)
- Not available in the `basic` strategy set
- Can be forced via `merge_strategy_override`
### Properties
- Importance-weighted — parameters that change most get the most influence
- Magnitude-calibrated — no drift from weighting
- Spectral cleanup — removes merge noise via SVD filtering
- Best for similar LoRAs (same concept, different epochs/seeds)
---
## Sparsification Methods
Sparsification is a preprocessing step that reduces parameter interference by zeroing out weights before merging. Two algorithms with two variants each.
### DARE (Drop And REscale)
*Reference: [Yu et al., ICML 2024](https://arxiv.org/abs/2311.03099)*
```
mask[i] ~ Bernoulli(density) # each position kept with probability = density
sparse_diff[i] = diff[i] × mask[i] # zero out dropped positions
sparse_diff[i] = sparse_diff[i] / density # rescale survivors to preserve expected value
```
**DAREx enhancement** (ICLR 2025): The `dare_dampening` parameter `λ` interpolates rescaling:
```
rescale_factor = 1 / (density^(1-λ))
```
At λ=0: standard DARE. Higher λ: less noise amplification at low density.
### DELLA (magnitude-aware dropout)
*Reference: [Deep et al., 2024](https://arxiv.org/abs/2406.11617)*
```
for each row of diff:
rank positions by |value| (ascending)
drop_probability[i] = (1 - density) × (rank[i] / num_positions)
mask[i] ~ Bernoulli(1 - drop_probability[i])
sparse_diff[i] = diff[i] × mask[i] / (1 - drop_probability[i])
```
Low-magnitude weights have higher drop probability. High-magnitude weights are preserved. More surgical than DARE.
### Conflict-Aware Variants
Standard sparsification operates everywhere. Conflict-aware variants (`dare_conflict`, `della_conflict`) only sparsify positions where LoRAs actually interfere:
```
conflict_mask[i] = (count of LoRAs with opposing signs at position i) ≥ 2
if conflict_mask[i]:
apply DARE or DELLA sparsification
else:
keep full weight (no sparsification)
```
This preserves unique contributions and same-sign reinforcements while still reducing interference at contested positions.
### Interaction with Merge Strategies
| Merge Strategy | Sparsification Role |
|---------------|-------------------|
| TIES | **Replaces** the TIES trim step (Step 1) |
| Weighted Sum | Preprocessing before sum |
| Weighted Average | Preprocessing before average |
| SLERP | Preprocessing before interpolation |
| Consensus | Preprocessing before importance weighting |
---
## Quality Enhancements
Quality enhancements compose with all merge strategies. They modify the diffs or the merge process to improve results. See [[How It Works#quality-enhancements]] for the pipeline position.
### DO-Merging (refine+)
*Reference: [arXiv 2505.15875](https://arxiv.org/abs/2505.15875)*
Orthogonalizes LoRA direction vectors via Modified Gram-Schmidt:
```
1. Flatten each diff to a 1D vector
2. Apply Gram-Schmidt orthogonalization (order: by descending norm)
3. Record original magnitudes
4. Reconstruct: new_diff = orthogonal_direction × original_magnitude
```
After orthogonalization, LoRAs that were partially aligned become truly independent. This reduces directional interference while preserving each LoRA's strength.
### Column-Wise Conflict Resolution (refine+)
*Inspired by [ZipLoRA, Shah et al., 2025](https://arxiv.org/abs/2311.13600)*
Instead of each element voting on sign direction independently, entire output neurons (rows of the weight matrix) vote as a unit:
```
for each row j of the weight matrix:
row_sign_votes = Σ_k sign(diff_k[j, :]) × |diff_k[j, :]|
elected_row_sign = sign(Σ row_sign_votes)
apply elected_row_sign to entire row
```
Preserves structural coherence — a neuron's input weights work together as a functional unit. Falls back to element-wise for 1D tensors (biases, layer norms).
### TALL-Masks (refine+)
*Reference: [Wang et al., 2024](https://arxiv.org/abs/2406.12832)*
Identifies "selfish" weights — positions where one LoRA dominates:
```
for each position i:
contributions = [|strength_k × diff_k[i]| for each LoRA k]
dominant = argmax(contributions)
if contributions[dominant] ≥ sum(others) × lambda:
mark as selfish (belongs to LoRA dominant)
1. Separate selfish weights from consensus pool
2. Run merge algorithm on consensus weights only
3. Add selfish weights back to the result
```
Threshold `lambda=1.0` — a LoRA's contribution must exceed all others combined to be considered selfish. This protects unique features from being averaged away during consensus merging.
### KnOTS SVD Alignment (full)
*Reference: [Ramé et al., 2024](https://arxiv.org/abs/2407.09095)*
Projects all diffs into a shared singular value basis for better comparability:
```
1. Concatenate diffs column-wise: M = [diff_1 | diff_2 | ... | diff_N]
2. Compute truncated SVD: M ≈ U × S × Vᵀ (rank ≤ 256)
3. For each diff k:
proj_k = Uᵀ × diff_k # project into shared basis
aligned_k = U × proj_k # reconstruct in aligned space
4. Use aligned diffs for all subsequent operations
```
This makes diffs more directly comparable by representing them in a shared coordinate system. Falls back to CPU on GPU OOM; skips gracefully if both fail.