Compare commits

...
6 changed files with 1076 additions and 98 deletions
+9 -1
View File
@@ -25,7 +25,10 @@ from .nodes.lora_extract_svd import (
) )
from .nodes.lora_resize import ( from .nodes.lora_resize import (
LoRAResizeFixed, LoRAResizeRatio, LoRAResizeFixed, LoRAResizeRatio,
LoRAResizeFrobenius, LoRAResizeCumulative LoRAResizeFrobenius, LoRAResizeCumulative,
LoRAResizeViaBaseFixed, LoRAResizeViaBaseRatio,
LoRAResizeViaBaseFrobenius, LoRAResizeViaBaseCumulative,
LoRAMultiMerge
) )
@@ -54,6 +57,11 @@ class ModelUtilsExtension(ComfyExtension):
# LoRA Resize # LoRA Resize
LoRAResizeFixed, LoRAResizeRatio, LoRAResizeFixed, LoRAResizeRatio,
LoRAResizeFrobenius, LoRAResizeCumulative, LoRAResizeFrobenius, LoRAResizeCumulative,
# LoRA Fast Resize (via base model)
LoRAResizeViaBaseFixed, LoRAResizeViaBaseRatio,
LoRAResizeViaBaseFrobenius, LoRAResizeViaBaseCumulative,
# LoRA Multi-Merge
LoRAMultiMerge,
] ]
+25
View File
@@ -294,3 +294,28 @@ ComfyUI-ModelUtils/
| Metadata reading | **100x+** (header only) | | Metadata reading | **100x+** (header only) |
| Parallel I/O | **2-4x** (ThreadPoolExecutor) | | Parallel I/O | **2-4x** (ThreadPoolExecutor) |
| Memory management | Auto-unload models before heavy ops | | 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
+30 -10
View File
@@ -44,9 +44,16 @@ def _index_sv_ratio(S: torch.Tensor, ratio: float) -> int:
return max(1, min(rank, len(S) - 1)) return max(1, min(rank, len(S) - 1))
def _index_sv_cumulative(S: torch.Tensor, target: float) -> int: def _index_sv_cumulative(S: torch.Tensor, target: float, max_rank: int = None) -> int:
"""Cumulative mode - keep enough SVs to reach target % of total.""" """Cumulative mode - keep enough SVs to reach target % of total.
total = torch.sum(S)
Calculates relative to max_rank if provided, otherwise relative to full.
"""
if max_rank is not None and max_rank < len(S):
total = torch.sum(S[:max_rank])
else:
total = torch.sum(S)
if total < 1e-8: if total < 1e-8:
return 1 return 1
cumsum = torch.cumsum(S, dim=0) / total cumsum = torch.cumsum(S, dim=0) / total
@@ -54,13 +61,26 @@ def _index_sv_cumulative(S: torch.Tensor, target: float) -> int:
return max(1, min(rank, len(S) - 1)) return max(1, min(rank, len(S) - 1))
def _index_sv_fro(S: torch.Tensor, target: float) -> int: def _index_sv_fro(S: torch.Tensor, target: float, max_rank: int = None) -> int:
"""Frobenius norm mode - preserve target fraction of Frobenius norm.""" """Frobenius norm mode - preserve target fraction of Frobenius norm.
S_sq = S.pow(2)
total_sq = torch.sum(S_sq) Calculates relative to max_rank if provided, otherwise relative to full.
This means "retain target% of what's achievable within max_rank".
"""
if max_rank is not None and max_rank < len(S):
# Calculate relative to what's achievable within max_rank
S_capped = S[:max_rank]
S_sq = S_capped.pow(2)
total_sq = torch.sum(S_sq)
else:
S_sq = S.pow(2)
total_sq = torch.sum(S_sq)
if total_sq < 1e-8: if total_sq < 1e-8:
return 1 return 1
cumsum = torch.cumsum(S_sq, dim=0) / total_sq
# Cumsum of all S (not capped) to find where we reach target
cumsum = torch.cumsum(S.pow(2), dim=0) / total_sq
rank = int(torch.searchsorted(cumsum, target ** 2).item()) + 1 rank = int(torch.searchsorted(cumsum, target ** 2).item()) + 1
return max(1, min(rank, len(S) - 1)) return max(1, min(rank, len(S) - 1))
@@ -122,9 +142,9 @@ def _compute_rank(S: torch.Tensor, mode: str, mode_param: float,
elif mode == "ratio": elif mode == "ratio":
rank = _index_sv_ratio(S, mode_param) rank = _index_sv_ratio(S, mode_param)
elif mode == "quantile" or mode == "sv_cumulative": elif mode == "quantile" or mode == "sv_cumulative":
rank = _index_sv_cumulative(S, mode_param) rank = _index_sv_cumulative(S, mode_param, max_rank)
elif mode == "sv_fro": elif mode == "sv_fro":
rank = _index_sv_fro(S, mode_param) rank = _index_sv_fro(S, mode_param, max_rank)
elif mode == "sv_knee": elif mode == "sv_knee":
rank = _index_sv_knee(S) rank = _index_sv_knee(S)
elif mode == "sv_cumulative_knee": elif mode == "sv_cumulative_knee":
+1001 -76
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -28,23 +28,23 @@ except ImportError:
class PinnedTensor: class PinnedTensor:
"""Context manager for automatic pinned memory management. """Context manager for automatic pinned memory management.
Usage: Usage:
with PinnedTensor(cpu_tensor) as pinned: with PinnedTensor(cpu_tensor) as pinned:
gpu_tensor = pinned.to('cuda') gpu_tensor = pinned.to('cuda')
# Memory automatically unpinned after transfer # Memory automatically unpinned after transfer
""" """
def __init__(self, tensor: torch.Tensor, auto_pin: bool = True): def __init__(self, tensor: torch.Tensor, auto_pin: bool = True):
self.tensor = tensor self.tensor = tensor
self.is_pinned = False self.is_pinned = False
self.auto_pin = auto_pin and PINNING_AVAILABLE self.auto_pin = auto_pin and PINNING_AVAILABLE
def __enter__(self) -> torch.Tensor: def __enter__(self) -> torch.Tensor:
if self.auto_pin and self.tensor.device.type == 'cpu': if self.auto_pin and self.tensor.device.type == 'cpu':
self.is_pinned = pin_memory(self.tensor) self.is_pinned = pin_memory(self.tensor)
return self.tensor return self.tensor
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
if self.is_pinned: if self.is_pinned:
unpin_memory(self.tensor) unpin_memory(self.tensor)
@@ -57,12 +57,12 @@ def transfer_to_gpu_pinned(
dtype: Optional[torch.dtype] = None dtype: Optional[torch.dtype] = None
) -> torch.Tensor: ) -> torch.Tensor:
"""Transfer tensor to GPU using pinned memory for faster transfer. """Transfer tensor to GPU using pinned memory for faster transfer.
Args: Args:
tensor: CPU tensor to transfer tensor: CPU tensor to transfer
device: Target GPU device device: Target GPU device
dtype: Optional dtype conversion dtype: Optional dtype conversion
Returns: Returns:
Tensor on GPU device Tensor on GPU device
""" """
@@ -71,7 +71,7 @@ def transfer_to_gpu_pinned(
if dtype: if dtype:
return tensor.to(device=device, dtype=dtype) return tensor.to(device=device, dtype=dtype)
return tensor.to(device=device) return tensor.to(device=device)
# Pin memory for faster transfer # Pin memory for faster transfer
was_pinned = pin_memory(tensor) was_pinned = pin_memory(tensor)
try: try:
@@ -162,7 +162,7 @@ class MemoryEfficientSafeOpen:
tensor_bytes = self.file.read(offset_end - offset_start) tensor_bytes = self.file.read(offset_end - offset_start)
return self._deserialize_tensor(tensor_bytes, metadata) return self._deserialize_tensor(tensor_bytes, metadata)
def get_tensor_to_gpu( def get_tensor_to_gpu(
self, self,
key: str, key: str,
@@ -170,12 +170,12 @@ class MemoryEfficientSafeOpen:
dtype: Optional[torch.dtype] = None dtype: Optional[torch.dtype] = None
) -> torch.Tensor: ) -> torch.Tensor:
"""Load a tensor and transfer to GPU using pinned memory for faster transfer. """Load a tensor and transfer to GPU using pinned memory for faster transfer.
Args: Args:
key: Tensor key to load key: Tensor key to load
device: Target GPU device (default 'cuda') device: Target GPU device (default 'cuda')
dtype: Optional dtype conversion dtype: Optional dtype conversion
Returns: Returns:
Tensor on GPU device Tensor on GPU device
""" """
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "ComfyUI-ModelUtils" name = "ComfyUI-ModelUtils"
description = "[WIP]Custom nodes for handling, inspecting, modifying and creating various model files." description = "[WIP]Custom nodes for handling, inspecting, modifying and creating various model files."
version = "0.2.0" version = "0.2.3"
license = { file = "LICENSE" } license = { file = "LICENSE" }
[project.urls] [project.urls]