Files
ethanfel-ComfyUI-LoRA-Optim…/docs/wiki/How-It-Works.md
T
EthanfelandClaude Opus 4.6 4ae79d0c9e docs: add v2 workflow, rename Legacy, update wiki for Settings nodes
- Add example_workflows/LoRa Optimizer v2.json for the new node layout
- Rename LoRA Optimizer (Advanced) → (Legacy) across all wiki pages
- Document Settings nodes (Merge, Optimizer, AutoTuner) in Nodes.md
- Add Settings and Compatibility Analyzer workflows to Workflows.md
- Add decision_smoothing, smooth_slerp_gate, scoring_speed/formula,
  and Settings node architecture to Configuration-Guide.md
- Update How-It-Works.md node registry and variant descriptions
- Rename patch_compression non_ties → smart in all references
- Update Home.md feature table (8 architectures, settings nodes)
- Add zit_example_1.png asset
- Update SVG assets (timeline, pipeline, bridge, ties, merge-use-cases)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:56:19 +01:00

942 lines
50 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.
# How the LoRA Optimizer Works
This page explains the full pipeline — from building a LoRA stack through analysis, strategy selection, merge execution, and post-processing. Each section builds on the previous one to show how algorithms interact and why each step exists.
---
## Table of Contents
- [The Problem](#the-problem)
- [Pipeline Overview](#pipeline-overview)
- [Step 1 — Building the LoRA Stack](#step-1--building-the-lora-stack)
- [Step 2 — Key Normalization](#step-2--key-normalization)
- [Step 3 — Pass 1: Analysis](#step-3--pass-1-analysis)
- [Computing Weight Diffs](#computing-weight-diffs)
- [Conflict Detection](#conflict-detection)
- [Cosine Similarity](#cosine-similarity)
- [Auto-Strength Calibration](#auto-strength-calibration)
- [Suggested Max Output Strength](#suggested-max-output-strength)
- [Step 4 — Strategy Selection](#step-4--strategy-selection)
- [Per-Prefix Decision Tree](#per-prefix-decision-tree)
- [Global Merge Mode Selection](#global-merge-mode-selection)
- [Architecture Presets](#architecture-presets)
- [Behavior Profiles](#behavior-profiles)
- [Sign Method Selection](#sign-method-selection)
- [Step 5 — Pass 2: Merge Execution](#step-5--pass-2-merge-execution)
- [Preprocessing: Sparsification](#preprocessing-sparsification)
- [Quality Enhancements](#quality-enhancements)
- [Merge Algorithms](#merge-algorithms)
- [How Algorithms Interact](#how-algorithms-interact)
- [Step 6 — Post-Processing](#step-6--post-processing)
- [SVD Patch Compression](#svd-patch-compression)
- [Dtype Downcasting](#dtype-downcasting)
- [VRAM Budget & Patch Placement](#vram-budget--patch-placement)
- [Patch Application](#patch-application)
- [Algorithm Interaction Map](#algorithm-interaction-map)
- [Node Reference](#node-reference)
- [Appendix: AutoTuner](#appendix-autotuner)
- [Appendix: Conflict Editor](#appendix-conflict-editor)
- [Appendix: Per-LoRA Controls](#appendix-per-lora-controls)
- [Appendix: Output Nodes](#appendix-output-nodes)
- [Appendix: Supported Architectures](#appendix-supported-architectures)
---
## The Problem
When you stack multiple LoRAs in ComfyUI, each one is applied independently — their effects simply add together on the base model weights. This works fine when LoRAs touch different parts of the model, but breaks down when they overlap:
- **Oversaturation** — Two style LoRAs at strength 1.0 can produce blown-out results because their effects compound in the same weight regions
- **Sign conflicts** — LoRA A wants to push a weight positive while LoRA B pushes it negative. Simple addition cancels out both contributions
- **Noise amplification** — Minor disagreements across thousands of parameters accumulate into visible artifacts
The LoRA Optimizer solves this by analyzing _where_ LoRAs conflict and applying the right merge strategy to each region independently.
---
## Pipeline Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ LORA OPTIMIZER PIPELINE │
│ │
│ LoRA Stack ──► Key Normalization ──► Pass 1: Analysis ──► Strategy Selection│
│ │ │ │
│ ▼ ▼ │
│ Pass 2: Merge Execution │
│ │ │
│ ▼ │
│ Post-Processing │
│ (SVD compress, dtype cast, │
│ VRAM budget placement) │
│ │ │
│ ▼ │
│ MODEL + CLIP + Report + LORA_DATA │
└─────────────────────────────────────────────────────────────────────────────┘
```
The pipeline has two physical passes through the weights but several logical stages. This two-pass design keeps peak memory close to “one target group at a time,” but the exact peak still depends on the largest layer being processed, how many LoRAs overlap on it, and whether extra quality/compression steps are enabled.
---
## Step 1 — Building the LoRA Stack
Before anything is analyzed or merged, you assemble a list of LoRAs using **LoRA Stack** or **LoRA Stack (Dynamic)** nodes.
Each entry in the stack contains:
- **LoRA file** — the `.safetensors` file to load
- **Strength** — how much this LoRA should contribute (default 1.0)
- **Conflict mode** — controls where this LoRA's contributions are kept (`all`, `low_conflict`, `high_conflict`)
- **Key filter** — controls which weight prefixes this LoRA contributes to (`all`, `shared_only`, `unique_only`)
The stack is just a list — no analysis happens yet. LoRAs are loaded lazily when the optimizer needs them.
### LoRA Stack (Dynamic)
The Dynamic variant provides 1–10 adjustable LoRA slots in a single node, replacing the need to chain multiple Stack nodes. It has two visibility modes controlled by `settings_visibility`:
| Mode | Per-LoRA Controls |
|------|-------------------|
| **Simple** | Single `strength` slider per LoRA — clean and beginner-friendly |
| **Advanced** | Separate `model_strength` and `clip_strength`, plus `conflict_mode` and `key_filter` per LoRA |
It also supports two input modes via `input_mode`:
- **Dropdown** — standard file picker from your LoRAs folder
- **Text** — type a name or path; auto-matches against installed LoRAs
An optional `base_model_filter` integrates with ComfyUI-Lora-Manager to filter LoRAs by base model (when that extension is installed).
### Stack Formats
The optimizer accepts stacks from multiple sources:
- **Native format** — dict-based entries from LoRA Stack / LoRA Stack (Dynamic)
- **Tuple format** — `(lora_name, model_strength, clip_strength)` from Efficiency Nodes, Comfyroll, and other packs
Both are normalized internally before analysis begins.
---
## Step 2 — Key Normalization
> _Controlled by: `normalize_keys` (disabled / enabled)_
Different LoRA trainers name their weights differently. Kohya uses underscores (`lora_transformer_double_blocks_0_...`), AI-Toolkit uses dots (`transformer.transformer_blocks.0...`), diffusers uses yet another format. If you mix LoRAs from different trainers, the optimizer sees _no key overlap_ — it thinks they touch completely different parts of the model and can't merge them.
Key normalization solves this by:
1. **Detecting the model architecture** from LoRA key patterns (FLUX, SDXL, Wan, Z-Image, LTX, Qwen)
2. **Remapping all keys** to a canonical format so LoRAs from any trainer can be compared
### Special Case: Z-Image QKV Fusion
Z-Image LoRAs often fuse Q, K, and V projections into a single `attention.qkv` weight. The normalizer:
1. Detects fused QKV (lora_up shape = `[3×out_features, rank]`)
2. **Splits** into separate `to_q`, `to_k`, `to_v` components
3. Allows per-component conflict analysis during the merge
4. **Re-fuses** back to native `attention.qkv` format after merging
This splitting is critical — Q, K, and V weights serve different purposes. Two LoRAs might agree on Q weights but conflict on V weights. Without splitting, the optimizer would average their conflicts and get a suboptimal result for both.
### Special Case: WanVideo `_orig_mod` Keys
WanVideo models compiled with `torch.compile` prefix their keys with `_orig_mod.`. The normalizer detects this mismatch and strips the prefix so LoRA keys can match correctly against model weights.
---
## Step 3 — Pass 1: Analysis
Pass 1 iterates through every unique weight prefix (e.g., `input_blocks.4`, `middle_block.1`) across all LoRAs. For each prefix, it computes statistics and then **immediately discards the full weight diffs**. Only lightweight scalars survive — this is what keeps memory usage low.
### Computing Weight Diffs
For each LoRA at each prefix, the optimizer computes the **full-rank diff** — the actual change this LoRA would make to the base model weights:
```
diff = lora_up @ lora_down × (alpha / rank) × strength
```
For LoCon layers (with `lora_mid`), the reconstruction handles the additional intermediate matrix.
These diffs are the ground truth — they show exactly what each LoRA wants to do to each weight. All analysis operates on these diffs, not on the raw low-rank factors.
### Conflict Detection
For each pair of LoRAs that overlap at a given prefix, the optimizer measures **sign conflict ratio**: the fraction of weight positions where the two LoRAs push in opposite directions.
```
conflict_ratio = count(sign(diff_A) ≠ sign(diff_B)) / total_overlapping_positions
```
A high conflict ratio (>25%) means the LoRAs are fighting each other at this prefix — they want opposite things. A low ratio means they mostly agree.
The conflict ratio is measured per-prefix, not globally. Two LoRAs might have 5% conflict in attention layers but 40% conflict in feed-forward layers. The optimizer handles each independently.
### Cosine Similarity
Beyond sign conflicts, the optimizer measures **cosine similarity** between LoRA diff vectors:
```
cos_sim = dot(diff_A, diff_B) / (||diff_A|| × ||diff_B||)
```
This captures directional alignment:
- **cos ≈ 1** → LoRAs point the same direction (reinforcing)
- **cos ≈ 0** → LoRAs are orthogonal (independent)
- **cos ≈ -1** → LoRAs point opposite directions (canceling)
Cosine similarity is used by:
- **Auto-strength calibration** — to compute interference energy
- **Global mode selection** — consensus and SLERP are preferred when similarity is high
- **Behavior profiles** — orthogonal detection triggers SLERP upgrade
### Auto-Strength Calibration
> _Controlled by: `auto_strength` (enabled / disabled)_
When enabled, the optimizer reduces all LoRA strengths proportionally to prevent oversaturation from stacking. The algorithm is **interference-aware energy normalization**:
1. Measure pairwise cosine similarity between all LoRAs
2. Compute total combined energy accounting for directional alignment:
```
||Σ v_i||² = Σ||v_i||² + 2 × Σ ||v_i|| × ||v_j|| × cos(v_i, v_j)
```
3. Scale all strengths so combined energy = energy of the strongest single LoRA alone
The key insight is that aligned LoRAs (cos ≈ 1) produce more combined energy than orthogonal ones (cos ≈ 0), which produce more than opposing ones (cos ≈ -1). The calibration adapts to the actual geometry:
| Scenario | Scale Factor |
|----------|-------------|
| 2 aligned LoRAs (cos≈1) | ~0.50 each |
| 2 orthogonal LoRAs (cos≈0) | ~0.71 each |
| 2 opposing LoRAs (cos≈-1) | ~1.0 each |
Original strength _ratios_ are always preserved — only the overall scale changes.
### Suggested Max Output Strength
The analysis report now includes a **suggested maximum output strength** — automatically calculated from the magnitude ratio of the merged LoRAs:
```
suggested_max = min(1.0 / norm_ratio, architecture_cap)
```
The cap varies by architecture preset (3.0 for UNet, 5.0 for DiT, 3.0 for LLM). This helps you avoid oversaturation by showing a safe strength ceiling. It appears in the report alongside the current `output_strength` value.
---
## Step 4 — Strategy Selection
After analysis, the optimizer decides how to merge each prefix. This is where `optimization_mode` matters:
| Mode | Behavior |
|------|----------|
| `per_prefix` (default) | Each weight prefix gets its own strategy based on local conflict data |
| `global` | One strategy for all prefixes, chosen from aggregate statistics |
| `additive` | Force simple addition everywhere |
### Per-Prefix Decision Tree
For each prefix independently:
```
How many LoRAs touch this prefix?
│
┌─────────┴─────────┐
│ │
Only 1 2 or more
│ │
weighted_sum Sign conflict ratio?
(full strength, │
no dilution) ┌─────────┴─────────┐
│ │
≤ 25% > 25%
│ │
weighted_average TIES
(compatible, (resolve conflicts
blend fairly) via trim/elect/merge)
```
This is the fundamental innovation: non-overlapping regions get 100% of their LoRA's effect (no dilution from averaging), while genuinely conflicting regions get proper TIES conflict resolution.
### Global Merge Mode Selection
When using `global` optimization (or as a fallback), the optimizer picks a single mode based on aggregate statistics. The `full` strategy set adds more nuanced selection:
```
Aggregate conflict ratio
│
┌─────────┴─────────┐
│ │
≤ 25% > 25%
│ │
Cosine similarity? TIES
│
┌─────┬──┴──┬─────┐
│ │ │ │
> 0.7 0.3-0.7 < 0.3 orthogonal
│ │ │ │
consensus weighted SLERP
average (full)
```
### Architecture Presets
> _Controlled by: `architecture_preset` (auto / sd_unet / dit / llm)_
Different model architectures have different numerical characteristics. The architecture preset tunes the optimizer's internal thresholds — density ranges, noise floors, and strength caps — to match:
| Preset | Architectures | Density Range | Noise Floor | Max Strength Cap |
|--------|--------------|---------------|-------------|-----------------|
| `sd_unet` | SD 1.5, SDXL | 0.1 – 0.9 | 10% | 3.0 |
| `dit` | Flux, Wan, Z-Image, LTX, HunyuanVideo | 0.4 – 0.95 | 5% | 5.0 |
| `llm` | Qwen, LLaMA | 0.1 – 0.8 | 15% | 3.0 |
| `auto` (default) | Auto-detected from LoRA keys | Selected automatically | — | — |
DiT models are denser and tolerate higher strengths. UNet models are sparser. LLM models need tighter density bounds and lower strength caps.
The architecture preset is **orthogonal** to the behavior profile — the preset controls _numeric thresholds_, while the profile controls _strategy selection logic_.
### Behavior Profiles
> _Controlled by: `strategy_set` (full / no_slerp / basic)_
The behavior profile controls _which_ merge strategies are considered during auto-selection:
- **`full`** (default) — Full detection: consensus for high similarity, SLERP for orthogonal, weighted_average otherwise
- **`no_slerp`** — Same detection but SLERP never auto-selected; stays as weighted_average
- **`basic`** — Pre-1.2 behavior: only TIES vs weighted_average, no SLERP or consensus
### Sign Method Selection
For TIES merging, the optimizer picks how to resolve sign conflicts:
| Condition | Method | Logic |
|-----------|--------|-------|
| Magnitude ratio > 2× | `total` | Stronger LoRA dominates the vote |
| Magnitude ratio ≤ 2× | `frequency` | Equal vote per LoRA |
---
## Step 5 — Pass 2: Merge Execution
Pass 2 walks through every prefix again, recomputes the diffs, and applies the selected strategy. Several processing stages happen in sequence for each prefix:
```
┌──────────────────────────────────────────────────────────────────┐
│ PER-PREFIX MERGE PIPELINE │
│ │
│ Recompute Quality Sparsify Apply Merge │
│ Diffs ──► Enhancements ──► (if enabled) ──► Algorithm │
│ (if enhanced/ │ │
│ maximum) ▼ │
│ Merged Patch │
│ │ │
│ ▼ │
│ SVD Compress │
│ (if enabled) │
│ │ │
│ ▼ │
│ Dtype Downcast │
│ │ │
│ ▼ │
│ VRAM Budget Placement │
│ │ │
│ ▼ │
│ Free & Continue │
└──────────────────────────────────────────────────────────────────┘
```
### Preprocessing: Sparsification
> _Controlled by: `sparsification`, `sparsification_density`, `dare_dampening`_
Sparsification reduces parameter interference by zeroing out a fraction of each LoRA's diff before merging. Two algorithms are available, each with a standard and conflict-aware variant:
#### DARE (Drop And REscale)
1. Generate a random binary mask where each weight has probability `density` of being kept
2. Zero out masked positions
3. Rescale survivors by `1/density` to preserve expected magnitude
**DAREx enhancement:** The `dare_dampening` parameter (0.0–1.0) interpolates the rescaling factor toward 1.0, reducing noise amplification from aggressive rescaling. At 0.0: standard DARE. Higher values: less noise at the cost of slightly biased magnitudes.
#### DELLA (magnitude-aware dropout)
1. Rank weights per-row by absolute magnitude
2. Low-magnitude weights get higher drop probability
3. High-magnitude weights are preserved
4. Rescale survivors to maintain expected value
DELLA is more surgical than DARE — it preferentially preserves the weights that matter most.
#### Conflict-Aware Variants
Standard sparsification drops weights everywhere, including positions where only one LoRA contributes or where LoRAs agree. This destroys useful signal.
Conflict-aware variants (`dare_conflict`, `della_conflict`) compute a sign-conflict mask first:
```
For each position:
if 2+ LoRAs have opposite signs → sparsify (this is interference)
if LoRAs agree or only 1 present → keep full weight (this is useful signal)
```
Result: interference is reduced without sacrificing unique features.
#### Interaction with TIES
When the merge strategy is TIES:
- **Sparsification replaces the TIES trim step.** Both achieve sparsification (removing low-value weights), so running both would be redundant. The sparsification density parameter controls how aggressive the combined effect is.
When the merge strategy is anything else:
- **Sparsification runs as preprocessing** before the merge algorithm sees the diffs.
### Quality Enhancements
> _Controlled by: `merge_refinement` (none / refine / full)_
Quality enhancements are additional processing steps applied before or during the merge. They compose with sparsification — sparsification runs first, then quality enhancements process the (possibly sparsified) diffs.
#### None (baseline)
No additional processing. Element-wise sign voting in TIES mode.
#### Refine (adds three techniques)
**1. DO-Merging (Decouple & Orthogonalize)**
- Orthogonalizes LoRA direction vectors via Modified Gram-Schmidt
- Preserves original magnitudes while reducing directional interference
- LoRAs that were partially aligned become truly independent
**2. Column-Wise Conflict Resolution** (replaces element-wise)
- Instead of each weight position voting on sign direction independently, entire output neurons (rows) vote as a unit
- Preserves structural coherence — a neuron's weights work together, so their signs should be resolved together
- Falls back to element-wise for 1D tensors (biases, norms)
**3. TALL-Masks (Task-Aware weight protection)**
- Identifies "selfish" weights — positions where one LoRA dominates and others contribute little
- Separates selfish contributions from the consensus
- After merging, adds selfish contributions back, protecting unique features from being averaged away
#### Full (adds SVD alignment)
Everything from Refine, plus:
**KnOTS SVD Alignment**
- Concatenates all LoRA diffs column-wise: `M = [diff_1 | diff_2 | ... | diff_N]`
- Computes truncated SVD (rank ≤ 256)
- Reconstructs each diff in the shared singular value basis
- Makes diffs more directly comparable by aligning their representation spaces
- Falls back to CPU on GPU OOM; skips gracefully if both fail
### Merge Algorithms
After preprocessing and quality enhancements, the actual merge algorithm runs. The algorithm was selected in Step 4.
#### Weighted Sum
```
output = Σ (strength_i × diff_i)
```
The simplest merge: plain addition. Used when only one LoRA touches a prefix (no conflict possible). Also available as `additive` optimization mode to force it everywhere.
**Properties:**
- Preserves every contribution exactly
- No information loss
- Fully compressible by SVD (linear operation)
#### Weighted Average
```
output = Σ (strength_i / Σ_strengths × diff_i)
```
Normalized addition. Fair blending when LoRAs agree on direction.
**Properties:**
- Prevents magnitude inflation from stacking
- Works well for compatible LoRAs (low conflict)
- Fully compressible by SVD (linear operation)
#### TIES (Trim, Elect Sign, Disjoint Merge)
A three-step process specifically designed for sign conflicts:
```
Step 1 — Trim: Keep only top-k% weights by magnitude
(or: use DARE/DELLA sparsification instead)
│
▼
Step 2 — Elect Sign: For each position, vote on the majority sign direction
┌─ frequency: each LoRA gets one vote
└─ total: votes weighted by magnitude
│
▼
Step 3 — Disjoint Merge: Average only the LoRAs that agree with the elected sign
Contributors opposing the majority are excluded for this position
```
**Properties:**
- Resolves sign conflicts without cancellation
- Nonlinear (trim + sign election) → produces full-rank results
- Lossy SVD compression (can't perfectly capture nonlinear output)
#### SLERP (Spherical Linear Interpolation)
Magnitude-preserving directional blend:
1. For 2 LoRAs: standard SLERP formula along the geodesic on a hypersphere
2. For 3+ LoRAs: iterative pairwise SLERP, sorted by descending weight (strongest anchors direction)
3. Falls back to linear interpolation for near-parallel vectors (where SLERP is numerically unstable)
4. Final magnitude corrected to match weighted average of input norms
**Properties:**
- Preserves weight magnitude (no inflation or deflation)
- Smooth directional interpolation
- Best for low-conflict, similar-magnitude LoRAs
#### Consensus Merging
Three-stage algorithm for highly similar LoRAs:
```
Stage 1 — Fisher-Proxy Importance Weighting:
weight each parameter by |diff|² as importance proxy
numerator = Σ(diff_i × strength_i × |diff_i|²)
denominator = Σ(|strength_i| × |diff_i|²)
output = numerator / denominator
Stage 2 — MAGIC Calibration:
rescale merged result so L2 norm matches
weighted average of input norms
Stage 3 — MonoSoup Spectral Cleanup:
SVD-based entropy filtering on 2D+ tensors
keeps effective rank based on singular value entropy
removes noise while preserving signal
```
**Properties:**
- Best for highly similar LoRAs (high cosine similarity)
- Importance-weighted → parameters that matter more get more influence
- Spectral cleanup removes merge noise
### How Algorithms Interact
The key to understanding the optimizer is knowing that these algorithms **compose in a fixed order**, not as alternatives:
```
┌──────────────────────────────────────────────┐
│ COMPOSITION ORDER │
│ │
│ 1. Key Normalization (if enabled) │
│ ↓ │
│ 2. Architecture Preset applied │
│ ↓ (sets density/strength thresholds) │
│ 3. Auto-Strength (if enabled) │
│ ↓ (adjusts strength scalars) │
│ 4. Quality: KnOTS alignment (if maximum) │
│ ↓ (transforms diff vectors) │
│ 5. Quality: DO-Merging (if enhanced+) │
│ ↓ (orthogonalizes directions) │
│ 6. Sparsification (if enabled) │
│ ↓ (zeros out weights) │
│ ├─ TIES mode? → replaces trim step │
│ └─ Other modes? → preprocessing │
│ 7. Quality: TALL-masks (if enhanced+) │
│ ↓ (separates selfish weights) │
│ 8. Merge Algorithm │
│ ↓ (combines diffs) │
│ 9. TALL-masks re-add (if enhanced+) │
│ ↓ (restores selfish weights) │
│ 10. SVD Compression (if enabled) │
│ ↓ (reduces to low-rank) │
│ 11. Dtype downcast to native precision │
│ ↓ (reduces memory footprint) │
│ 12. VRAM budget placement (GPU or CPU) │
│ ↓ │
│ 13. Patch application to model │
└──────────────────────────────────────────────┘
```
Important interactions:
| Combination | Interaction |
|-------------|-------------|
| DARE/DELLA + TIES | Sparsification **replaces** TIES trim (no double-sparsification) |
| DARE/DELLA + other modes | Sparsification runs as **preprocessing** before merge |
| Conflict-aware + per_prefix | Conflict mask computed per-prefix using local conflict data |
| KnOTS + TIES | KnOTS aligns diffs in shared basis _before_ TIES sign voting |
| DO-Merging + any mode | Orthogonalization happens _after_ KnOTS but _before_ sparsification |
| TALL-masks + any mode | Selfish weights separated _before_ merge, added back _after_ |
| Column-wise + TIES | Replaces element-wise sign voting (structural coherence) |
| Auto-strength + everything | Only adjusts strength scalars — all algorithms see adjusted strengths |
| Architecture preset + behavior profile | Preset sets _numeric thresholds_, profile sets _strategy logic_ — independent |
---
## Step 6 — Post-Processing
### SVD Patch Compression
> _Controlled by: `patch_compression` (smart / aggressive / disabled), `svd_device` (gpu / cpu)_
After merging, each prefix produces a full-rank diff tensor. For a 4096×4096 weight, that's ~64MB per key (vs ~0.5MB for a rank-32 LoRA patch). SVD compression re-factors these back to low-rank:
```
full_rank_diff ──► truncated SVD ──► lora_up (rank R) + lora_down (rank R)
```
The compression rank is automatically set to the **sum of all input LoRA ranks**. Three rank-32 LoRAs → rank-96 compressed patch. This is enough to represent the full merge without quality loss for linear operations.
| Mode | Compresses | Quality |
|------|-----------|---------|
| `smart` (default) | weighted_sum and weighted_average prefixes only | **Lossless** — linear operations are exactly representable |
| `aggressive` | Everything including TIES | **Lossy on TIES** — nonlinear ops (trim, sign election) produce full-rank results |
| `disabled` | Nothing | No loss, but ~32× more RAM |
### Dtype Downcasting
After merge and optional SVD compression, patches are automatically downcast to the **native dtype** of the input LoRA weights (typically `float16` or `bfloat16`). Merging internally uses `float32` for numerical precision, but the final patches don't need that precision. This reduces memory footprint significantly — a `float32` patch is 2× the size of a `float16` patch.
### VRAM Budget & Patch Placement
> _Controlled by: `vram_budget` (0.0 – 1.0)_
By default (`vram_budget=0.0`), all merged patches live on CPU. The model patcher moves them to GPU on demand during sampling, which adds latency.
When `vram_budget` > 0, the optimizer keeps merged patches on GPU up to a fraction of available free VRAM:
```
usable_vram = free_vram - current_model_size
gpu_budget = usable_vram × vram_budget
```
Patches are placed on GPU until the budget is exhausted; remaining patches stay on CPU. This trades GPU memory for faster sampling — especially useful for iterative workflows where you sample repeatedly with the same merge.
### Patch Application
The merged patches (compressed or full-rank) are applied to the model via ComfyUI's model patcher system. CLIP patches are applied separately with the `clip_strength_multiplier` scaling.
The optimizer outputs:
- **MODEL** — the patched model ready for sampling
- **CLIP** — the patched text encoder (if CLIP input was provided)
- **STRING** — detailed analysis report with per-prefix strategy map and suggested max strength
- **LORA_DATA** — the raw merged patches for use with Save Merged LoRA, Merged LoRA to Hook, or Merged LoRA to WanVideo
---
## Algorithm Interaction Map
This diagram shows every algorithm in the system and how they relate:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ INPUT ANALYSIS STRATEGY │
│ ───── ──────── ──────── │
│ LoRA Stack ────► Key Normalization │
│ │ │
│ ▼ │
│ Architecture Detection ──► Architecture Preset │
│ │ (density/strength thresholds) │
│ ▼ │
│ Compute Diffs ──────► Sign Conflict ──► Per-Prefix │
│ │ Ratio Decision │
│ │ │ │
│ ├──────► Cosine ─────► Auto- │ │
│ │ Similarity Strength │ │
│ │ │ │ │ │
│ │ └──► Behavior │ │ │
│ │ Profile │ │ │
│ │ (strategy │ │ │
│ │ logic) │ │ │
│ │ │ │ │
│ ├──────► Magnitude ──► Sign Method │ │
│ │ Ratio Selection │ │
│ │ │ │
│ ├──────► Norms ──► Suggested Max │ │
│ │ Strength │ │
│ │ │ │
│ Discard Diffs │ │
│ │ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ │ │
│ MERGE (per prefix) │ │
│ ────────────────── ▼ │
│ │
│ Recompute Diffs ──► KnOTS ──► DO-Merge ──► DARE/DELLA ──► ┌──────────┐ │
│ (with adjusted (if (if (if │ Select │ │
│ strengths) maximum) enhanced+) enabled) │ Merge: │ │
│ │ │ │ │
│ ┌─────────────┤ │ wt_sum │ │
│ │ │ │ wt_avg │ │
│ │ TALL-mask │ │ TIES │ │
│ │ separate │ │ SLERP │ │
│ │ (if enh+) │ │ consensus│ │
│ │ │ └────┬─────┘ │
│ │ ▼ │ │
│ │ Merge Execute ◄──────┘ │
│ │ │ │
│ │ TALL-mask │ │
│ │ re-add ────┘ │
│ │ │ │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ SVD Compress │
│ │ │
│ ▼ │
│ Dtype Downcast │
│ │ │
│ ▼ │
│ VRAM Budget Placement │
│ │ │
│ ▼ │
│ Apply to Model │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Node Reference
All nodes registered by the LoRA Optimizer:
| Node | Display Name | Purpose |
|------|-------------|---------|
| `LoRAStack` | LoRA Stack | Build LoRA stacks by chaining |
| `LoRAStackDynamic` | LoRA Stack (Dynamic) | Single node with 1–10 adjustable slots, simple/advanced modes |
| `LoRAMergeSettings` | LoRA Merge Settings | Shared config: normalization, architecture preset, smoothing, VRAM budget |
| `LoRAOptimizerSettings` | LoRA Optimizer Settings | Optimizer-specific settings: auto-strength, sparsification, compression, etc. |
| `LoRAAutoTunerSettings` | LoRA AutoTuner Settings | Tuner-specific settings: top_n, scoring, diff cache, etc. |
| `LoRAOptimizerSimple` | LoRA Optimizer | Simplified optimizer with sensible defaults, accepts optional `settings` and `tuner_data` |
| `LoRAOptimizer` | LoRA Optimizer (Legacy) | All parameters on one node — for bridge workflow with `settings_source` |
| `LoRAAutoTuner` | LoRA AutoTuner | Automated parameter sweep to rank merge configs |
| `LoRAMergeSelector` | Merge Selector | Select alternative configs from AutoTuner results |
| `LoRAConflictEditor` | LoRA Conflict Editor | Interactive conflict analysis with per-LoRA overrides |
| `SaveMergedLoRA` | Save Merged LoRA | Export merged patches as standalone `.safetensors` |
| `MergedLoRAToHook` | Merged LoRA to Hook | Wrap merged patches as conditioning hooks |
| `LoRACompatibilityAnalyzer` | LoRA Compatibility Analyzer | Pre-merge planning: overlap analysis, grouping, optional node creation |
| `WanVideoLoRAOptimizer` | WanVideo LoRA Optimizer (WIP) | Optimizer variant for WanVideo models |
| `MergedLoRAToWanVideo` | Merged LoRA → WanVideo (WIP) | Bridge merged LORA_DATA to WanVideo wrapper models |
### Node Variants
**LoRA Optimizer** (recommended):
Exposes `model`, `lora_stack`, `output_strength`, `clip` (optional), and `clip_strength_multiplier`. Accepts optional `settings` (from a Settings node) and `tuner_data` (from AutoTuner) inputs. When no settings node is connected, uses built-in defaults: `auto_strength=enabled`, `optimization_mode=per_prefix`, `merge_refinement=none`, `patch_compression=smart`, `vram_budget=0.0`.
**LoRA Optimizer (Legacy):**
All parameters on one node. Has `settings_source` for the AutoTuner ↔ Optimizer bridge workflow. Superseded by LoRA Optimizer + Settings nodes for new workflows.
**Settings nodes** (LoRA Merge Settings, LoRA Optimizer Settings, LoRA AutoTuner Settings):
Separate shared and node-specific configuration from the main optimizer. Merge Settings feeds into Optimizer Settings or AutoTuner Settings, which feed into the optimizer's `settings` input.
**WanVideo LoRA Optimizer:**
Accepts `WANVIDEOMODEL` instead of `MODEL`, skips CLIP. Defaults differ from the standard optimizer: `normalize_keys=enabled` (WanVideo LoRAs come from many trainers), `cache_patches=disabled` (video models are large), `architecture_preset=dit`.
---
## Appendix: AutoTuner
The **LoRA AutoTuner** node automates parameter selection by sweeping all combinations and ranking them with internal merge metrics, plus an optional external evaluator hook.
### Architecture
The AutoTuner runs a **single Pass 1 analysis** and caches the results. All parameter combinations are scored against this cached analysis without re-analyzing — making the sweep fast regardless of how many combinations are tested.
### Phase 1 — Fast Heuristic Scoring
Generates a full parameter grid across all configurable dimensions:
| Parameter | Values Swept |
|-----------|-------------|
| Merge modes | `weighted_average`, `slerp`, `consensus`, `ties` |
| Sparsification | `disabled`, `dare`, `della`, `dare_conflict`, `della_conflict` |
| Density | `0.5`, `0.7`, `0.9` |
| DARE dampening | `0.0`, `0.3`, `0.6` |
| Quality | `none`, `refine`, `full` |
| Auto-strength | `enabled`, `disabled` |
| Optimization mode | `per_prefix`, `global` |
This produces **2,000+ combinations**. Each is scored heuristically in ~2-5 seconds total using the analysis data from Pass 1 — no actual merges are performed. Scoring considers conflict, excess conflict, cosine similarity, subspace overlap, and magnitude distribution.
### Phase 2 — Merge & Measure Top-N
The top-N configs from Phase 1 are actually merged and scored by measuring:
- **Norm consistency** across patches
- **Effective rank** (SVD-based entropy) — when `scoring_svd=enabled`
- **Sparsity distribution** (estimated via column-sampling for efficiency)
- **Composite score** combining all metrics, optionally blended with an external evaluator
Progress is tracked via ComfyUI's progress bar (spanning both Phase 1 analysis and Phase 2 merges). Each candidate logs merge time and scoring time for diagnostics.
### Memory Management
Phase 2 is designed to avoid RAM exhaustion:
- Only the **current best candidate** is kept in memory
- Non-best candidates' model/clip patches are freed immediately after scoring
- `gc.collect()` is forced after freeing each non-best candidate
- Magnitude samples from Phase 1 are freed before Phase 2 begins
### AutoTuner Parameters
| Parameter | Default | Effect |
|-----------|---------|--------|
| `top_n` | 3 | How many candidates to actually merge and score |
| `scoring_svd` | disabled | Enable SVD-based effective rank scoring (slower, more thorough) |
| `scoring_device` | gpu | Where to run SVD scoring (`gpu` is 10-50x faster) |
| `scoring_speed` | turbo | Subsample prefix scoring for faster sweeps (`full`, `fast`, `turbo`, `turbo+`) |
| `auto_strength_floor` | -1.0 | Minimum auto-strength floor for orthogonal LoRAs (`-1` = architecture default) |
| `decision_smoothing` | 0.25 | Smooth per-prefix decision metrics toward block averages |
| `evaluator` | — | External evaluator hook for prompt/reference scoring |
| `diff_cache_mode` | auto | Reuse raw LoRA diffs across candidates (`disabled`, `auto`, `ram`, `disk`) |
| `diff_cache_ram_pct` | 0.5 | RAM budget for `auto` diff caching before spilling to disk |
| `cache_patches` | enabled | Keep the final AutoTuner merge cached in RAM for fast re-execution |
| `vram_budget` | 0.0 | Fraction of free VRAM for patch placement |
| `record_dataset` | disabled | Save analysis metrics to JSONL for threshold tuning research |
### Dataset Recording
When `record_dataset=enabled`, the AutoTuner saves a JSONL entry to `lora_optimizer_reports/autotuner_dataset.jsonl` after each run. Each entry records:
- Timestamp and detected architecture
- LoRA names and analysis summary
- Per-prefix strategy distribution
- All top-N configs with their measured scores
This data is used for refining architecture presets and threshold tuning.
### AutoTuner Outputs
| Output | Type | Purpose |
|--------|------|---------|
| `MODEL` | MODEL | Patched model from the top-ranked config |
| `CLIP` | CLIP | Patched text encoder |
| `report` | STRING | Ranked results with scores and parameters |
| `analysis_report` | STRING | Full optimizer analysis report for the top-ranked config |
| `TUNER_DATA` | TUNER_DATA | All ranked configs for Merge Selector |
| `LORA_DATA` | LORA_DATA | Merged patches for Save Merged LoRA / Merged LoRA to Hook |
Both AutoTuner and Merge Selector are marked as `OUTPUT_NODE = True` for ComfyUI's output recording and re-execution optimization.
### AutoTuner Workflow
```
LoRA Stack ──► LoRA AutoTuner ──► MODEL + CLIP + Report + Analysis Report + TUNER_DATA + LORA_DATA
│ │
▼ ▼
Merge Selector Save Merged LoRA
(pick alternative)
│
▼
MODEL + CLIP + LORA_DATA
```
---
## Appendix: Conflict Editor
The **LoRA Conflict Editor** node provides interactive analysis and override control:
1. Loads all LoRAs and computes pairwise conflict ratios
2. Auto-suggests per-LoRA conflict modes:
- < 15% average conflict → `all`
- 15–40% → `low_conflict`
- \> 40% → `high_conflict`
3. Allows manual override of conflict modes and merge strategy
4. Outputs enriched stack + merge strategy override for the optimizer
### Conflict Editor Workflow
```
LoRA Stack ──► LoRA Conflict Editor ──► Enriched LORA_STACK ──► LoRA Optimizer
│ ▲
└── merge_strategy (STRING) ─────────────────────┘
(strategy override)
```
---
## Appendix: Per-LoRA Controls
### Conflict Modes
Per-LoRA control over where each LoRA's contributions are applied:
| Mode | Behavior | Use Case |
|------|----------|----------|
| `all` (default) | Apply everywhere | Normal merging |
| `low_conflict` | Only where this LoRA agrees with the majority | Conservative — keep only safe contributions |
| `high_conflict` | Only where this LoRA disagrees with the majority | Force dominance in contested regions |
### Key Filters
Per-LoRA control over which weight prefixes each LoRA contributes to:
| Filter | Behavior | Use Case |
|--------|----------|----------|
| `all` (default) | All prefixes | Normal merging |
| `shared_only` | Only prefixes present in 2+ LoRAs | Strip variant-specific keys (e.g., I2V keys from a mixed LoRA) |
| `unique_only` | Only prefixes present in exactly 1 LoRA | Extract lightweight variant-specific adapters |
---
## Appendix: Output Nodes
### Save Merged LoRA
Exports the optimizer's merged result as a standalone `.safetensors` file. Connects to `LORA_DATA` from LoRA Optimizer, AutoTuner, or Merge Selector.
| Option | Default | Effect |
|--------|---------|--------|
| `save_folder` | first configured LoRA folder | Choose which configured ComfyUI LoRA directory to save into |
| `filename` | `merged_lora` | File name relative to `save_folder`. Subdirectories allowed; traversal blocked |
| `save_rank` | 0 (auto) | 0 = use each layer's existing rank. Non-zero = force this rank via SVD compression |
| `bake_strength` | enabled | Bakes `output_strength` into saved weights so the LoRA reproduces your merge at strength 1.0 |
### Merged LoRA to Hook
Wraps merged patches as a **conditioning hook** (`HOOKS`) for per-conditioning LoRA application:
- **Per-prompt LoRA** — apply different merged LoRAs to positive vs negative conditioning
- **Scheduled application** — combine with hook keyframes for step-specific LoRA
- **Regional conditioning** — apply LoRA to specific image regions
- **Preserving the base model** — keep MODEL unpatched while using the merge through hooks
### Merged LoRA → WanVideo
Bridges `LORA_DATA` to a `WANVIDEOMODEL`. Handles the `_orig_mod.` key mismatch from `torch.compile` and injects patches in the format WanVideo's model loader expects.
---
## Appendix: Supported Architectures
| Architecture | Key Detection Patterns | Special Handling |
|-------------|----------------------|-----------------|
| **FLUX** | `double_blocks`, `single_blocks` | Sliced QKV offsets, multi-trainer unification |
| **SDXL** | `lora_te1_`, `input_blocks` | Text encoder + UNet unified |
| **Z-Image** (Lumina2) | `diffusion_model.layers.N.attention` | Fused QKV split/re-fuse, Musubi Tuner format |
| **Wan** 2.1/2.2 | `blocks.N` with `self_attn`/`ffn` | LyCORIS / diffusers / Musubi / Fun LoRA / finetrainer unified, RS-LoRA alpha fix |
| **LTX Video** | `adaln_single`, `attn1`/`attn2` | Trainer format unification |
| **Qwen-Image** | `img_mlp`/`txt_mlp`/`img_mod`/`txt_mod` | Dual-stream key unification |
**Supported trainers** (auto-normalized when `normalize_keys=enabled`):
Kohya, AI-Toolkit, LyCORIS, Musubi Tuner, diffusers/PEFT, Fun LoRA, finetrainer
**Supported LoRA formats:**
Standard LoRA, LoCon (with `lora_mid`), LyCORIS, diffusers/PEFT format, RS-LoRA (with alpha fix)
---
## References
- **TIES-Merging:** [Yadav et al., NeurIPS 2023](https://arxiv.org/abs/2306.01708) — Trim, Elect Sign, Disjoint Merge
- **DARE:** [Yu et al., ICML 2024](https://arxiv.org/abs/2311.03099) — Drop And REscale
- **DAREx:** ICLR 2025 — Dampened DARE rescaling
- **DELLA:** [Deep et al., 2024](https://arxiv.org/abs/2406.11617) — Magnitude-aware dropout
- **KnOTS:** [Ramé et al., 2024](https://arxiv.org/abs/2407.09095) — SVD alignment for model merging
- **TALL-masks:** [Wang et al., 2024](https://arxiv.org/abs/2406.12832) — Task-aware selfish weight protection
- **ZipLoRA:** [Shah et al., 2025](https://arxiv.org/abs/2311.13600) — Column-wise structural sparsity (inspiration)
- **DO-Merging:** [arXiv 2505.15875](https://arxiv.org/abs/2505.15875) — Decouple & Orthogonalize