Merges srv1n's correctness refactor (PR #1) with conflict resolution to preserve our recent setting renames and fixes while incorporating their core improvements: - Target-key grouping: analysis/merge operates on resolved model weights instead of raw LoRA prefixes, fixing trainer-specific aliasing bugs - Exact low-rank linear patches: lossless linear merges for weighted_sum and weighted_average when no nonlinear ops are needed - Excess conflict metric: sign disagreement beyond statistical expectation (cosine-sim aware), replacing raw conflict ratio in decisions - Subspace overlap: measures how much LoRA weight perturbations align - Block smoothing: reduces noisy per-prefix mode flips - Activation-aware calibration scoring for Phase 2 quality evaluation - ACE-Step architecture support - Soft LoKr/LoHa imports for graceful degradation - RS-LoRA compensation re-enabled with safe heuristic - 519-line test suite (19 tests, all passing) Conflict resolution kept our: - Setting renames (merge_refinement, patch_compression, strategy_set) - Qwen-Image detection fix - LoRACompatibilityAnalyzer node - Conflict-mode masking in _apply_conflict_modes (PR's approach) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
383 lines
12 KiB
Markdown
383 lines
12 KiB
Markdown
# 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 |
|
||
|
||
---
|
||
|
||
## 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 `v1.2` behavior profile when LoRAs are orthogonal
|
||
|
||
### Properties
|
||
|
||
- Fair blending — each LoRA contributes proportionally
|
||
- Prevents magnitude inflation from stacking
|
||
- Fully compressible by SVD (linear operation)
|
||
- Doesn't handle sign conflicts (opposing contributions cancel partially)
|
||
|
||
---
|
||
|
||
## 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
|
||
|
||
- v1.2 behavior profile: auto-selected when conflict ≤ 25% AND LoRAs are detected as orthogonal (low cosine similarity)
|
||
- Not auto-selected in `no_slerp` or `classic` profiles
|
||
- 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
|
||
|
||
- v1.2 behavior profile: auto-selected when conflict ≤ 25% AND cosine similarity > 0.7 (highly similar LoRAs)
|
||
- Not available in `classic` profile
|
||
- 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 (enhanced+)
|
||
|
||
*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 (enhanced+)
|
||
|
||
*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 (enhanced+)
|
||
|
||
*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 (maximum)
|
||
|
||
*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.
|