322 lines
9.3 KiB
Markdown
322 lines
9.3 KiB
Markdown
# ComfyUI-ModelUtils - Complete Project History
|
||
|
||
## Version Timeline
|
||
|
||
| Version | Commits | Key Features |
|
||
|---------|---------|--------------|
|
||
| 0.1.1 | `70d6c13`-`d67cbca` | Initial nodes (MetaKeys) |
|
||
| 0.1.2 | `6b662a4` | Icon assets |
|
||
| 0.1.4 | `cbe3502` | Key renaming/pruning nodes |
|
||
| 0.1.5 | `78b3198` | Model merging nodes |
|
||
| 0.1.6 | `31989be` | Embedding nodes, namespace fixes |
|
||
| 0.1.7 | `b6fae8d` | Mismatch handling, layer filtering, V3 migration |
|
||
| **0.2.0** | `3df073f` | LoRA extraction, memory optimization, pinned memory |
|
||
|
||
---
|
||
|
||
## Phase 1: Initial Release (v0.1.1)
|
||
|
||
**Commit:** `70d6c13` - "Add first nodes, toml and publish"
|
||
|
||
### Files Created
|
||
- `nodes/metakeys.py` - Get model metadata and layer keys
|
||
- `nodes/utils.py` - Shared utilities
|
||
- `__init__.py` - Node registration
|
||
- `pyproject.toml` - Package configuration
|
||
|
||
### Nodes
|
||
- `ModelMetaKeys` - Get diffusion model metadata
|
||
- `CheckpointMetaKeys` - Get checkpoint metadata
|
||
- `TextEncoderMetaKeys` - Get text encoder metadata
|
||
- `LoRAMetaKeys` - Get LoRA metadata
|
||
- `EmbeddingMetaKeys` - Get embedding metadata
|
||
|
||
---
|
||
|
||
## Phase 2: Key Manipulation (v0.1.4)
|
||
|
||
**Commit:** `4f08fe6` - "add key renaming and key pruning nodes"
|
||
|
||
### Files Created
|
||
- `nodes/renamekeys.py` - Rename tensor keys
|
||
- `nodes/prunekeys.py` - Remove tensor keys by pattern
|
||
|
||
### Nodes (5 variants each for model types)
|
||
- `ModelRenameKeys`, `CheckpointRenameKeys`, `TextEncoderRenameKeys`, `LoRARenameKeys`, `EmbeddingRenameKeys`
|
||
- `ModelPruneKeys`, `CheckpointPruneKeys`, `TextEncoderPruneKeys`, `LoRAPruneKeys`, `EmbeddingPruneKeys`
|
||
|
||
---
|
||
|
||
## Phase 3: Model Merging (v0.1.5)
|
||
|
||
**Commit:** `7959370` - "Add model merging nodes"
|
||
|
||
### Files Created
|
||
- `nodes/merger.py` - Main merging logic
|
||
- `nodes/merger_ops.py` - Merge operation modes
|
||
- `nodes/merger_utils.py` - `MemoryEfficientSafeOpen` class
|
||
- `docs/merger_2_model_modes.md` - Documentation
|
||
- `docs/merger_3_model_modes.md` - Documentation
|
||
|
||
### Nodes
|
||
- `CheckpointTwoMerger`, `DiffusionModelTwoMerger`, `TextEncoderTwoMerger`, `LoRATwoMerger`
|
||
- `CheckpointThreeMerger`, `DiffusionModelThreeMerger`, `TextEncoderThreeMerger`, `LoRAThreeMerger`
|
||
|
||
### Merge Modes (TWO_MODEL_MODES)
|
||
- Weighted Sum, Add Difference, Smooth Add Difference
|
||
- Train Difference, Sum Twice, Triple Sum
|
||
- Euclidean Add Difference, Distribution Crossover, Ties
|
||
- Slerp, Lerp, SVD Supermerger, SVD LoRA Extraction
|
||
|
||
### Merge Modes (THREE_MODEL_MODES)
|
||
- Add Difference (A + (B - C) × α)
|
||
|
||
### MemoryEfficientSafeOpen (initial)
|
||
```python
|
||
class MemoryEfficientSafeOpen:
|
||
def __init__(self, filename, device='cpu', mmap_mode=True)
|
||
def keys() -> List[str]
|
||
def get_tensor(key) -> Tensor
|
||
# mmap for zero-copy reads
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 4: Embedding Nodes (v0.1.6)
|
||
|
||
**Commit:** `0d035ef` - "Add embedding nodes"
|
||
|
||
### Additions
|
||
- Added embedding variants to all node types
|
||
- Namespace conflict fixes (`68f22fa`, `5ea63ab`)
|
||
- Refactored names for universality (`232dc76`)
|
||
|
||
---
|
||
|
||
## Phase 5: Mismatch Handling (v0.1.7)
|
||
|
||
**Commits:** `19bc047` → `de27186`
|
||
|
||
### Features Added
|
||
- `mismatch_mode` parameter: skip/zeros/error
|
||
- Layer filtering with regex patterns
|
||
- `exclude_patterns` - Keep model A only (no merge)
|
||
- `discard_patterns` - Remove from output entirely
|
||
|
||
### Files Modified
|
||
- `merger.py` - Added mismatch handling logic
|
||
- `merger_ops.py` - Added `MissingTensorBehavior` enum, `MissingTensorError`
|
||
|
||
---
|
||
|
||
## Phase 6: V3 API Migration (v0.1.7)
|
||
|
||
**Commit:** `a3116cb` - "migrate to v3 schema and add LoRA extraction node"
|
||
|
||
### Changes
|
||
- Migrated all nodes from legacy API to `comfy_api.latest.io`
|
||
- `io.Schema`, `io.ComfyNode`, `io.NodeOutput`
|
||
- Added initial LoRA extraction node (diff-based)
|
||
|
||
---
|
||
|
||
## Phase 7: LyCORIS Extraction (pre-0.2.0)
|
||
|
||
**Commits:** `92d3c55` → `18679cc`
|
||
|
||
### Files Created
|
||
- `nodes/lycoris_extract.py` - LyCORIS-based extraction
|
||
- `nodes/lora_extract.py` - Simple diff extraction
|
||
|
||
### Initial Implementation
|
||
- Used `lycoris-lora` external dependency
|
||
- `lycoris.utils.extract_linear`, `extract_conv`
|
||
- Added `requirements.txt` with `lycoris-lora`
|
||
|
||
### Nodes Split (`18679cc`)
|
||
- `LyCORISExtractFixed`, `LyCORISExtractThreshold`
|
||
- `LyCORISExtractRatio`, `LyCORISExtractQuantile`
|
||
|
||
---
|
||
|
||
## Phase 8: Native SVD Port (0.2.0)
|
||
|
||
**Commits:** `6d7b2d1` → `eef7377`
|
||
|
||
### Removed
|
||
- `lycoris-lora` dependency
|
||
- `lycoris_extract.py`
|
||
- `lora_extract.py`
|
||
|
||
### NEW: [lora_extract_svd.py](file:///f:/ComfyUI/custom_nodes/ComfyUI-ModelUtils/nodes/lora_extract_svd.py)
|
||
|
||
**8 Rank Selection Functions:**
|
||
```python
|
||
_index_sv_fixed(S, dim) # Fixed rank
|
||
_index_sv_threshold(S, threshold) # Absolute threshold
|
||
_index_sv_ratio(S, ratio) # Relative to max
|
||
_index_sv_cumulative(S, target) # Cumulative sum %
|
||
_index_sv_fro(S, target) # Frobenius norm
|
||
_index_sv_knee(S) # Knee detection
|
||
_index_sv_cumulative_knee(S) # Cumsum knee
|
||
_index_sv_rel_decrease(S, tau) # Ratio drop
|
||
```
|
||
|
||
**SVD Functions:**
|
||
```python
|
||
_svd_extract_linear_lowrank(weight, rank, device, clamp_quantile)
|
||
_svd_extract_linear(weight, mode, mode_param, device, max_rank, clamp_quantile)
|
||
_svd_extract_conv(weight, mode, mode_param, device, max_rank, clamp_quantile)
|
||
_detect_fused_layer(key, shape) # QKV/MLP detection
|
||
_extract_chunked_layer(...) # Chunked extraction
|
||
```
|
||
|
||
**Nodes:**
|
||
- `LoRAExtractFixed` - Uses `svd_lowrank` (10x faster)
|
||
- `LoRAExtractRatio` - Relative threshold
|
||
- `LoRAExtractQuantile` - Cumulative target
|
||
- `LoRAExtractKnee` - Auto knee detection
|
||
- `LoRAExtractFrobenius` - Norm preservation
|
||
|
||
---
|
||
|
||
## Phase 9: Memory Management (0.2.0)
|
||
|
||
**Commit:** `6d7b2d1` - "refactor nodes and implement memory management"
|
||
|
||
### NEW: [device_utils.py](file:///f:/ComfyUI/custom_nodes/ComfyUI-ModelUtils/nodes/device_utils.py)
|
||
|
||
Integrates with `comfy.model_management`:
|
||
|
||
```python
|
||
@dataclass
|
||
class DeviceCapabilities:
|
||
device_type, total_vram_gb, free_vram_gb, total_ram_gb, free_ram_gb
|
||
cpu_cores, recommended_workers, max_tensor_size_gb
|
||
supports_pinned_memory, supports_async_streams
|
||
|
||
get_device_capabilities(device) -> DeviceCapabilities
|
||
estimate_model_size(path) -> float # GB
|
||
can_fit_in_memory(paths, device, safety_factor) -> (bool, str)
|
||
prepare_for_large_operation(gb, device) -> bool # Calls free_memory()
|
||
cleanup_after_operation() # gc.collect() + soft_empty_cache()
|
||
get_optimal_workers(size_gb) -> int
|
||
```
|
||
|
||
### Updates to Existing Nodes
|
||
- `metakeys.py` - Header-only reading (no tensor loading)
|
||
- `renamekeys.py` - `MemoryEfficientSafeOpen` + memory mgmt
|
||
- `prunekeys.py` - `MemoryEfficientSafeOpen` + memory mgmt
|
||
- `merger.py` - Memory prep/cleanup
|
||
|
||
---
|
||
|
||
## Phase 10: Pinned Memory (0.2.0)
|
||
|
||
**Commit:** `b770786` - "Add pinned memory for faster merging and extraction"
|
||
|
||
### [merger_utils.py](file:///f:/ComfyUI/custom_nodes/ComfyUI-ModelUtils/nodes/merger_utils.py) Enhancements
|
||
|
||
```python
|
||
# Context manager
|
||
class PinnedTensor:
|
||
def __enter__(self) -> Tensor # Pins memory
|
||
def __exit__(...) # Unpins
|
||
|
||
# Transfer helper (2-3x faster)
|
||
def transfer_to_gpu_pinned(tensor, device='cuda', dtype=None) -> Tensor
|
||
|
||
# MemoryEfficientSafeOpen additions
|
||
def get_tensor_to_gpu(key, device, dtype) # Pinned transfer
|
||
def keys_sorted_by_offset() -> List[str] # Sequential I/O
|
||
def get_tensors_parallel(keys, workers, sort_by_offset) -> Dict # Parallel
|
||
def get_tensor_as_dict(key) -> Dict # JSON config parsing
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 11: SVD Optimization (0.2.0)
|
||
|
||
**Commit:** `65c1ae3` - "add svd_lowrank for fixed rank extraction"
|
||
|
||
### Changes
|
||
- Added `_svd_extract_linear_lowrank()` using `torch.svd_lowrank`
|
||
- Routes fixed mode to lowrank for **10x speedup**
|
||
- Changed `chunk_large_layers` default to `False`
|
||
|
||
---
|
||
|
||
## Phase 12: Bug Fixes (0.2.0)
|
||
|
||
**Commit:** `eef7377` - "Added .contiguous() to saves"
|
||
|
||
### Fix
|
||
Non-contiguous tensor error from SVD slices:
|
||
```python
|
||
tensor.to(dtype).cpu().contiguous() # Added .contiguous()
|
||
```
|
||
|
||
---
|
||
|
||
## Current File Structure
|
||
|
||
```
|
||
ComfyUI-ModelUtils/
|
||
├── __init__.py
|
||
├── pyproject.toml
|
||
├── requirements.txt (empty - no external deps)
|
||
├── README.md
|
||
├── docs/
|
||
│ ├── merger_2_model_modes.md
|
||
│ └── merger_3_model_modes.md
|
||
├── nodes/
|
||
│ ├── utils.py
|
||
│ ├── metakeys.py
|
||
│ ├── renamekeys.py
|
||
│ ├── prunekeys.py
|
||
│ ├── merger.py
|
||
│ ├── merger_ops.py
|
||
│ ├── merger_utils.py
|
||
│ ├── device_utils.py
|
||
│ └── lora_extract_svd.py
|
||
└── assets/
|
||
└── icon.png
|
||
```
|
||
|
||
---
|
||
|
||
## Performance Summary
|
||
|
||
| Feature | Improvement |
|
||
|---------|-------------|
|
||
| Fixed-rank SVD | **10x** (svd_lowrank) |
|
||
| CPU→GPU transfer | **2-3x** (pinned memory) |
|
||
| Metadata reading | **100x+** (header only) |
|
||
| Parallel I/O | **2-4x** (ThreadPoolExecutor) |
|
||
| Memory management | Auto-unload models before heavy ops |
|
||
| **LoRA Resize Fast** | **~91x** (merge-to-base extraction) |
|
||
|
||
---
|
||
|
||
## Phase 13: Fast LoRA Resize (2026-01-11)
|
||
|
||
### Summary
|
||
Added fast LoRA resize via base model extraction path, achieving ~91x speedup.
|
||
|
||
### Changes
|
||
- Added `resize_lora_via_base()` function using extraction code path
|
||
- Added 4 new fast resize nodes requiring base model input
|
||
|
||
### Files Modified
|
||
|
||
| File | Changes |
|
||
|------|---------|
|
||
| `nodes/lora_resize.py` | Added `resize_lora_via_base()`, 4 new node classes |
|
||
| `__init__.py` | Registered new nodes |
|
||
|
||
### New Nodes
|
||
- `LoRAResizeViaBaseFixed` - Fast fixed rank resize
|
||
- `LoRAResizeViaBaseRatio` - Fast SV ratio resize
|
||
- `LoRAResizeViaBaseFrobenius` - Fast Frobenius resize
|
||
- `LoRAResizeViaBaseCumulative` - Fast cumulative resize
|