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>
13 KiB
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 | Single-LoRA prefixes | Lossless, fully SVD-compressible |
| Weighted Average | Low-conflict overlapping prefixes | Lossless, fully SVD-compressible |
| TIES | High-conflict prefixes (>25% sign conflict) | Nonlinear, lossy SVD |
| SLERP | Low-conflict, similar-magnitude LoRAs | Magnitude-preserving |
| Consensus | 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
preserveflag 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".
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
fullstrategy 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 itspreserveflag (it is then added on top of the blend rather than averaged in).
TIES Merging
Trim, Interfere, Elect Sign — specifically designed to resolve sign conflicts.
Reference: Yadav et al., NeurIPS 2023
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
Spherical Linear intERPolation — 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
fullstrategy set: auto-selected when conflict ≤ 25% AND LoRAs are detected as orthogonal (low cosine similarity)- Not auto-selected in the
no_slerporbasicstrategy 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
fullstrategy set: auto-selected when conflict ≤ 25% AND cosine similarity > 0.7 (highly similar LoRAs)- Not available in the
basicstrategy 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
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
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
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
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
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
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.