perf(extract): stream adaptive SVD inputs

This commit is contained in:
silveroxides
2026-08-14 07:49:00 +02:00
parent 0d79c8d4f4
commit ed8c331ce2
7 changed files with 737 additions and 302 deletions
+219
View File
@@ -0,0 +1,219 @@
import importlib
import sys
import types
from collections import Counter
from pathlib import Path
import pytest
import torch
from unifiedefficientloader import IncrementalSafetensorsWriter, MemoryEfficientSafeOpen
REPO_ROOT = Path(__file__).resolve().parents[1]
PACKAGE_NAME = "modelutils_adaptive_extraction_tests"
def _load_module(name):
if PACKAGE_NAME not in sys.modules:
package = types.ModuleType(PACKAGE_NAME)
package.__path__ = [str(REPO_ROOT)]
sys.modules[PACKAGE_NAME] = package
return importlib.import_module(f"{PACKAGE_NAME}.{name}")
@pytest.fixture(scope="module")
def adaptive():
return _load_module("nodes.adaptive_svd")
def _exact_partial(weight, q, niter):
del niter
U, S, Vh = torch.linalg.svd(weight, full_matrices=False)
return U[:, :q], S[:q], Vh[:q, :].T
@pytest.mark.parametrize(
("mode", "parameter", "expected"),
[("ratio", 2.0, 2), ("quantile", 0.7, 2), ("sv_fro", 0.8, 1)],
)
def test_adaptive_partial_rank_matches_reference(monkeypatch, adaptive, mode, parameter, expected):
monkeypatch.setattr(adaptive.torch, "svd_lowrank", _exact_partial)
weight = torch.diag(torch.tensor([8.0, 5.0, 2.0, 1.0, 0.5, 0.25]))
_, _, _, rank = adaptive.adaptive_partial_svd(
weight, mode, parameter, max_rank=4, probe_offset=1, niter=2
)
assert rank == expected
def test_unresolved_ratio_expands_and_caps(monkeypatch, adaptive):
probes = []
def tracked(weight, q, niter):
probes.append(q)
return _exact_partial(weight, q, niter)
monkeypatch.setattr(adaptive.torch, "svd_lowrank", tracked)
weight = torch.diag(torch.linspace(10.0, 8.0, 12))
_, _, _, rank = adaptive.adaptive_partial_svd(
weight, "ratio", 100.0, max_rank=4, probe_offset=2, niter=2
)
assert probes == [6, 8]
assert rank == 4
def test_frobenius_uses_energy_outside_output_cap(monkeypatch, adaptive):
monkeypatch.setattr(adaptive.torch, "svd_lowrank", _exact_partial)
weight = torch.eye(10)
_, _, _, rank = adaptive.adaptive_partial_svd(
weight, "sv_fro", 0.9, max_rank=3, probe_offset=2, niter=2
)
assert rank == 3
class _FakeStream:
def __init__(self, batches):
self._batches = iter(batches)
self.closed = False
def __iter__(self):
return self
def __next__(self):
return next(self._batches)
def close(self):
self.closed = True
class _FakeHandler:
def __init__(self, values):
self.values = values
self.calls = []
self.marked = []
self.streams = []
def async_stream(self, keys, **kwargs):
self.calls.append((tuple(keys), kwargs))
stream = _FakeStream([[(key, self.values[key])] for key in keys])
self.streams.append(stream)
return stream
def mark_processed(self, key):
self.marked.append(key)
def test_paired_stream_is_bounded_ordered_and_releases_once():
streaming = _load_module("nodes.extraction_stream")
handler_a = _FakeHandler({"a1": torch.tensor(1), "a2": torch.tensor(2)})
handler_b = _FakeHandler({"b1": torch.tensor(3)})
units = [("one", "a1", "b1"), ("two", "a2", None)]
observed = []
for key, tensor_a, tensor_b in streaming.paired_async_tensors(
handler_a, handler_b, units, pin_memory=True
):
observed.append((key, tensor_a.item(), None if tensor_b is None else tensor_b.item()))
assert observed == [("one", 1, 3), ("two", 2, None)]
assert handler_a.calls == [(('a1', 'a2'), {"batch_size": 1, "prefetch_batches": 1, "pin_memory": True})]
assert handler_b.calls == [(('b1',), {"batch_size": 1, "prefetch_batches": 1, "pin_memory": True})]
assert Counter(handler_a.marked) == Counter({"a1": 1, "a2": 1})
assert Counter(handler_b.marked) == Counter({"b1": 1})
assert all(stream.closed for stream in handler_a.streams + handler_b.streams)
def test_cuda_oom_compute_retries_on_cpu(monkeypatch):
streaming = _load_module("nodes.extraction_stream")
monkeypatch.setattr(streaming, "release_cuda_oom", lambda: None)
calls = []
def fail_cuda():
calls.append("cuda")
raise RuntimeError("CUDA out of memory")
def run_cpu():
calls.append("cpu")
return 7
assert streaming.retry_cuda_oom_on_cpu(fail_cuda, run_cpu, "cuda:0") == 7
assert calls == ["cuda", "cpu"]
def test_adaptive_controls_cover_all_public_families():
modules_and_nodes = [
(_load_module("nodes.lora_extract_svd"), prefix)
for prefix in ("LoRAExtractRatio", "LoRAExtractQuantile", "LoRAExtractFrobenius")
]
modules_and_nodes += [
(_load_module("nodes.dora_extract_wd"), prefix)
for prefix in ("DoRAExtractRatio", "DoRAExtractQuantile", "DoRAExtractFrobenius")
]
modules_and_nodes += [
(_load_module("nodes.dora_learned_wd"), prefix)
for prefix in ("DoRALearnedExtractRatio", "DoRALearnedExtractQuantile", "DoRALearnedExtractFrobenius")
]
modules_and_nodes += [
(_load_module("nodes.text_encoder_extract"), prefix)
for prefix in (
"TextEncoderLoRAExtractRatio", "TextEncoderLoRAExtractQuantile",
"TextEncoderLoRAExtractFrobenius", "TextEncoderDoRAExtractRatio",
"TextEncoderDoRAExtractQuantile", "TextEncoderDoRAExtractFrobenius",
)
]
for module, node_name in modules_and_nodes:
names = [item.id for item in getattr(module, node_name).define_schema().inputs]
probe_index = names.index("probe_offset")
assert names[probe_index + 1] == "linear_max_rank"
def _write_uel(path, tensors):
with IncrementalSafetensorsWriter(str(path), max_workers=1) as writer:
writer.write_dict(tensors)
def test_lora_extraction_uses_only_async_uel_and_releases_sources(monkeypatch, tmp_path):
extraction = _load_module("nodes.lora_extract_svd")
model_a = tmp_path / "a.safetensors"
model_b = tmp_path / "b.safetensors"
output = tmp_path / "out.safetensors"
key = "model.diffusion_model.layer.weight"
_write_uel(model_a, {key: torch.eye(3)})
_write_uel(model_b, {key: torch.zeros(3, 3)})
monkeypatch.setattr(extraction, "prepare_for_large_operation", lambda *args: None)
monkeypatch.setattr(extraction, "cleanup_after_operation", lambda: None)
original_async = extraction.MemoryEfficientSafeOpen.async_stream
original_mark = extraction.MemoryEfficientSafeOpen.mark_processed
async_calls = []
marked = []
def track_async(self, keys, **kwargs):
async_calls.append((self.filename, tuple(keys), kwargs))
yield from original_async(self, keys, **kwargs)
def track_mark(self, source_key):
marked.append((self.filename, source_key))
return original_mark(self, source_key)
monkeypatch.setattr(extraction.MemoryEfficientSafeOpen, "async_stream", track_async)
monkeypatch.setattr(extraction.MemoryEfficientSafeOpen, "mark_processed", track_mark)
monkeypatch.setattr(
extraction.MemoryEfficientSafeOpen,
"get_tensor",
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("synchronous tensor load")),
)
extraction.extract_lora_from_files(
str(model_a), str(model_b), "fixed", 1, 1, "cpu", "fp32", str(output),
linear_max_rank=1, conv_max_rank=1, force_clear_cache=False,
)
assert output.is_file()
assert len(async_calls) == 2
assert all(options == {"batch_size": 1, "prefetch_batches": 1, "pin_memory": False} for _, _, options in async_calls)
assert Counter(marked) == Counter((filename, key) for filename, keys, _ in async_calls for key in keys)
with MemoryEfficientSafeOpen(str(output), low_memory=True) as result:
assert sorted(result.keys()) == [
"diffusion_model.layer.lora_A.weight",
"diffusion_model.layer.lora_B.weight",
]