From c6b65d6721ebdbb24c2649d1fd4f05823757f3a0 Mon Sep 17 00:00:00 2001 From: silveroxides Date: Sat, 3 Jan 2026 20:55:27 +0100 Subject: [PATCH] fix: frobenius/cumulative norm calculation now relative to max_rank --- nodes/lora_extract_svd.py | 40 +++++++++++++++++++++++++++++---------- nodes/lora_resize.py | 7 +++---- pyproject.toml | 2 +- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/nodes/lora_extract_svd.py b/nodes/lora_extract_svd.py index 581a66e..72562eb 100644 --- a/nodes/lora_extract_svd.py +++ b/nodes/lora_extract_svd.py @@ -44,9 +44,16 @@ def _index_sv_ratio(S: torch.Tensor, ratio: float) -> int: return max(1, min(rank, len(S) - 1)) -def _index_sv_cumulative(S: torch.Tensor, target: float) -> int: - """Cumulative mode - keep enough SVs to reach target % of total.""" - total = torch.sum(S) +def _index_sv_cumulative(S: torch.Tensor, target: float, max_rank: int = None) -> int: + """Cumulative mode - keep enough SVs to reach target % of total. + + 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: return 1 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)) -def _index_sv_fro(S: torch.Tensor, target: float) -> int: - """Frobenius norm mode - preserve target fraction of Frobenius norm.""" - S_sq = S.pow(2) - total_sq = torch.sum(S_sq) +def _index_sv_fro(S: torch.Tensor, target: float, max_rank: int = None) -> int: + """Frobenius norm mode - preserve target fraction of Frobenius norm. + + 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: 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 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": rank = _index_sv_ratio(S, mode_param) 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": - rank = _index_sv_fro(S, mode_param) + rank = _index_sv_fro(S, mode_param, max_rank) elif mode == "sv_knee": rank = _index_sv_knee(S) elif mode == "sv_cumulative_knee": diff --git a/nodes/lora_resize.py b/nodes/lora_resize.py index 2f20256..658e875 100644 --- a/nodes/lora_resize.py +++ b/nodes/lora_resize.py @@ -284,14 +284,13 @@ def _compute_resize( """Compute new rank and alpha based on resize method.""" if dynamic_method == "sv_ratio" and dynamic_param is not None: - # Note: _index_sv_ratio uses S[0]*ratio, kohya uses S[0]/ratio - # Use kohya convention here for consistency with their tool + # Use kohya convention: S[0]/ratio min_sv = S[0] / dynamic_param new_rank = max(1, int(torch.sum(S > min_sv).item())) elif dynamic_method == "sv_cumulative" and dynamic_param is not None: - new_rank = _index_sv_cumulative(S, dynamic_param) + new_rank = _index_sv_cumulative(S, dynamic_param, max_rank) elif dynamic_method == "sv_fro" and dynamic_param is not None: - new_rank = _index_sv_fro(S, dynamic_param) + new_rank = _index_sv_fro(S, dynamic_param, max_rank) else: new_rank = max_rank diff --git a/pyproject.toml b/pyproject.toml index 6aca7f8..b015ce7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI-ModelUtils" description = "[WIP]Custom nodes for handling, inspecting, modifying and creating various model files." -version = "0.2.1" +version = "0.2.2" license = { file = "LICENSE" } [project.urls]