"""Whole-parameter LoRA payload: key classification, application, and reporting. Nothing here loads a model. Most tests exercise key algebra and arithmetic on the CPU; one regression test verifies cross-device adapter loading when CUDA is available. """ import logging import pytest import torch from safetensors.torch import save_file from fastvideo.configs.models.dits.wanvideo import WanVideoConfig from fastvideo.models.loader import fsdp_load from fastvideo.models.loader.lora_patch import DenseLoRAPatch, normalize_lora_key from fastvideo.models.loader.utils import get_param_names_mapping def write_adapter(tmp_path, tensors, name="adapter_model.safetensors"): path = tmp_path / name save_file(tensors, str(path)) return str(path) # --------------------------------------------------------------------------- keys @pytest.mark.parametrize( "raw, expected", [ # PEFT, the spelling FastVideo's own extractor emits. ("blocks.0.attn.to_q.lora_A.weight", "blocks.0.attn.to_q.lora_A.weight"), # PEFT with a named adapter: the name sits between the factor and `.weight`. ("blocks.0.attn.to_q.lora_A.default.weight", "blocks.0.attn.to_q.lora_A.weight"), ("blocks.0.attn.to_q.lora_B.turbo_v2.weight", "blocks.0.attn.to_q.lora_B.weight"), # kohya / ComfyUI up-down spelling, with and without `.weight`. ("blocks.0.attn.to_q.lora_down.weight", "blocks.0.attn.to_q.lora_A.weight"), ("blocks.0.attn.to_q.lora_up.weight", "blocks.0.attn.to_q.lora_B.weight"), ("blocks.0.attn.to_q.lora_down", "blocks.0.attn.to_q.lora_A"), # kohya alpha. ("blocks.0.attn.to_q.alpha", "blocks.0.attn.to_q.lora_alpha"), # The `diffusion_model.` prefix ComfyUI-format files carry. ("diffusion_model.blocks.0.mlp.fc1.lora_up.weight", "blocks.0.mlp.fc1.lora_B.weight"), ], ) def test_normalize_lora_key_accepts_every_published_spelling(raw, expected): assert normalize_lora_key(raw) == expected @pytest.mark.parametrize("raw", [ "blocks.0.norm1.diff", "blocks.0.attn.to_q.diff_b", "blocks.0.attn.to_gate_compress.set_weight", "blocks.0.scale_shift_table.diff_param", "blocks.0.extra_table.set_param", "blocks.0.attn.to_q.dora_scale", ]) def test_normalize_lora_key_disclaims_non_low_rank_keys(raw): """These belong to the loader-side patch, not the module merge path.""" assert normalize_lora_key(raw) is None def test_set_weight_is_not_mangled_into_set(): """`.replace('.weight', '')` downstream would turn `.set_weight` into `.set`.""" assert normalize_lora_key("blocks.0.attn.to_gate_compress.set_weight") is None # --------------------------------------------------------------------- classification def test_from_adapter_returns_none_for_a_purely_low_rank_adapter(tmp_path): path = write_adapter( tmp_path, { "blocks.0.attn.to_q.lora_A.weight": torch.zeros(4, 8), "blocks.0.attn.to_q.lora_B.weight": torch.zeros(8, 4), }) assert DenseLoRAPatch.from_adapter(path) is None def test_from_adapter_returns_none_without_a_path(): assert DenseLoRAPatch.from_adapter(None) is None def test_from_adapter_splits_additive_from_replacement(tmp_path): path = write_adapter( tmp_path, { "blocks.0.attn.to_q.lora_A.weight": torch.zeros(4, 8), "blocks.0.norm1.diff": torch.zeros(8), "blocks.0.attn.to_q.diff_b": torch.zeros(8), "blocks.0.scale_shift_table.diff_param": torch.zeros(8), "blocks.0.attn.to_gate_compress.set_weight": torch.zeros(8, 8), "blocks.0.extra_table.set_param": torch.zeros(8), }) patch = DenseLoRAPatch.from_adapter(path) assert patch is not None # Weight/bias suffixes restore their parameter suffix; generic parameter # payloads preserve the complete name. assert set(patch._additive) == { "blocks.0.norm1.weight", "blocks.0.attn.to_q.bias", "blocks.0.scale_shift_table", } assert set(patch._replacement) == { "blocks.0.attn.to_gate_compress.weight", "blocks.0.extra_table", } def test_dense_suffix_is_not_hidden_by_an_alpha_like_parameter_name(tmp_path): path = write_adapter(tmp_path, {"blocks.0.alpha.diff_param": torch.zeros(4)}) patch = DenseLoRAPatch.from_adapter(path) assert set(patch._additive) == {"blocks.0.alpha"} def test_param_names_mapping_is_applied_to_dense_keys(tmp_path): """An adapter written against the published layout needs no separate rename table.""" path = write_adapter(tmp_path, {"blocks.0.ff.net.0.proj.diff": torch.zeros(4)}) def mapping(name): return name.replace("ff.net.0.proj", "ff.fc_in"), None, None patch = DenseLoRAPatch.from_adapter(path, mapping) assert set(patch._additive) == {"blocks.0.ff.fc_in.weight"} def test_official_wan_mixed_adapter_maps_dense_key_like_factors(tmp_path): path = write_adapter( tmp_path, { "blocks.0.self_attn.q.lora_A.weight": torch.zeros(2, 4), "blocks.0.self_attn.q.lora_B.weight": torch.zeros(4, 2), "blocks.0.self_attn.k.diff": torch.zeros(4), }) config = WanVideoConfig() patch = DenseLoRAPatch.from_adapter( path, get_param_names_mapping(config.param_names_mapping), lora_param_names_mapping=get_param_names_mapping(config.lora_param_names_mapping), ) assert set(patch._additive) == {"blocks.0.to_k.weight"} def _stub_fsdp_loading(monkeypatch): monkeypatch.setattr(fsdp_load, "set_mixed_precision_policy", lambda **_: None) monkeypatch.setattr(fsdp_load, "safetensors_weights_iterator", lambda *_args, **_kwargs: iter(())) monkeypatch.setattr(fsdp_load, "load_model_from_full_model_state_dict", lambda *_args, **_kwargs: None) monkeypatch.setattr(fsdp_load, "_maybe_quantize_model", lambda _model, **_kwargs: None) def _load_tiny_model(model_cls, *, lora_path=None, lora_strength=1.0, init_params=None): return fsdp_load.maybe_load_fsdp_model( model_cls=model_cls, init_params=init_params or {}, weight_dir_list=[], device=torch.device("cpu"), hsdp_replicate_dim=1, hsdp_shard_dim=1, default_dtype=torch.float32, param_dtype=torch.float32, reduce_dtype=torch.float32, training_mode=False, pin_cpu_memory=False, lora_path=lora_path, lora_strength=lora_strength, ) def test_fsdp_loader_forwards_lora_specific_mapping(monkeypatch): captured = {} class TinyModel(torch.nn.Module): param_names_mapping = {r"^hf\.(.*)$": r"custom.\1"} lora_param_names_mapping = {r"^official\.(.*)$": r"hf.\1"} def capture_patch(cls, lora_path, param_names_mapping, *, lora_param_names_mapping=None, strength=1.0): captured["path"] = lora_path captured["regular"] = param_names_mapping("hf.weight")[0] captured["lora"] = lora_param_names_mapping("official.weight")[0] captured["strength"] = strength return None monkeypatch.setattr(DenseLoRAPatch, "from_adapter", classmethod(capture_patch)) _stub_fsdp_loading(monkeypatch) _load_tiny_model(TinyModel, lora_path="adapter.safetensors", lora_strength=0.75) assert captured == { "path": "adapter.safetensors", "regular": "custom.weight", "lora": "hf.weight", "strength": 0.75, } def test_fsdp_loader_does_not_consult_lora_mapping_without_adapter(monkeypatch): class TinyModel(torch.nn.Module): param_names_mapping = {r"^hf\.(.*)$": r"custom.\1"} def unexpected_patch(*_args, **_kwargs): pytest.fail("the no-LoRA load path must not inspect an adapter") monkeypatch.setattr(DenseLoRAPatch, "from_adapter", classmethod(unexpected_patch)) _stub_fsdp_loading(monkeypatch) model = _load_tiny_model(TinyModel) assert isinstance(model, TinyModel) assert not hasattr(model, "lora_param_names_mapping") def test_fsdp_loader_allows_adapter_without_lora_specific_mapping(monkeypatch): captured = {} class TinyModel(torch.nn.Module): param_names_mapping = {r"^hf\.(.*)$": r"custom.\1"} def capture_patch(cls, lora_path, param_names_mapping, *, lora_param_names_mapping=None, strength=1.0): captured["path"] = lora_path captured["regular"] = param_names_mapping("hf.weight")[0] captured["lora"] = lora_param_names_mapping captured["strength"] = strength return None monkeypatch.setattr(DenseLoRAPatch, "from_adapter", classmethod(capture_patch)) _stub_fsdp_loading(monkeypatch) _load_tiny_model(TinyModel, lora_path="adapter.safetensors") assert captured == { "path": "adapter.safetensors", "regular": "custom.weight", "lora": None, "strength": 1.0, } def test_fused_target_is_refused_rather_than_guessed(tmp_path): path = write_adapter(tmp_path, {"blocks.0.attn.to_q.diff": torch.zeros(4)}) def fusing_mapping(name): return "blocks.0.attn.to_qkv.weight", 0, 3 with pytest.raises(NotImplementedError, match="fused parameter"): DenseLoRAPatch.from_adapter(path, fusing_mapping) def test_two_keys_onto_one_parameter_is_an_error(tmp_path): path = write_adapter(tmp_path, { "blocks.0.norm1.diff": torch.zeros(4), "blocks.0.norm1.set_weight": torch.zeros(4), }) # `.diff` and `.set_weight` land in different tables, so they do not collide; two # keys of the *same* kind must. patch = DenseLoRAPatch.from_adapter(path) assert set(patch._additive) == {"blocks.0.norm1.weight"} assert set(patch._replacement) == {"blocks.0.norm1.weight"} # ---------------------------------------------------------------------- application def test_apply_to_adds_the_delta(tmp_path): path = write_adapter(tmp_path, {"blocks.0.norm1.diff": torch.full((4, ), 0.25)}) patch = DenseLoRAPatch.from_adapter(path) out = patch.apply_to("blocks.0.norm1.weight", torch.ones(4)) assert torch.allclose(out, torch.full((4, ), 1.25)) def test_apply_to_supports_standalone_parameters(tmp_path): path = write_adapter(tmp_path, {"blocks.0.scale_shift_table.diff_param": torch.full((4, ), 0.25)}) patch = DenseLoRAPatch.from_adapter(path) out = patch.apply_to("blocks.0.scale_shift_table", torch.ones(4)) assert torch.allclose(out, torch.full((4, ), 1.25)) @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required for cross-device adapter loading") def test_apply_to_moves_cpu_delta_to_base_tensor_device(tmp_path): """A CPU safetensors delta follows a directly loaded CUDA checkpoint tensor.""" path = write_adapter(tmp_path, {"blocks.0.norm1.diff": torch.full((4, ), 0.25)}) patch = DenseLoRAPatch.from_adapter(path) out = patch.apply_to("blocks.0.norm1.weight", torch.ones(4, device="cuda")) assert out.device.type == "cuda" assert torch.allclose(out.cpu(), torch.full((4, ), 1.25)) def test_apply_to_leaves_unrelated_parameters_untouched(tmp_path): path = write_adapter(tmp_path, {"blocks.0.norm1.diff": torch.full((4, ), 0.25)}) patch = DenseLoRAPatch.from_adapter(path) base = torch.ones(4) assert patch.apply_to("blocks.9.norm1.weight", base) is base def test_apply_to_sums_in_float32_so_a_small_delta_survives(tmp_path): """A bfloat16 add would quantize away a delta far below the base weight. The VSA gates and the norm deltas both sit three to four orders of magnitude under the weights they modify, which is exactly where bfloat16's 8-bit significand drops the change entirely. """ delta = 1e-4 path = write_adapter(tmp_path, {"blocks.0.norm1.diff": torch.full((4, ), delta, dtype=torch.bfloat16)}) patch = DenseLoRAPatch.from_adapter(path) base = torch.ones(4, dtype=torch.bfloat16) out = patch.apply_to("blocks.0.norm1.weight", base) assert out.dtype == torch.float32 assert (out - 1.0).abs().min() > 0, "delta was rounded away" naive = (base + torch.full((4, ), delta, dtype=torch.bfloat16)) assert torch.equal(naive, base), "precondition: the bfloat16 add is what loses it" def test_apply_to_rejects_a_shape_mismatch(tmp_path): path = write_adapter(tmp_path, {"blocks.0.norm1.diff": torch.zeros(8)}) patch = DenseLoRAPatch.from_adapter(path) with pytest.raises(ValueError, match="shape"): patch.apply_to("blocks.0.norm1.weight", torch.zeros(4)) def test_replacement_for_supports_standalone_parameters(tmp_path): table = torch.randn(6, 4) path = write_adapter(tmp_path, {"blocks.0.extra_table.set_param": table}) patch = DenseLoRAPatch.from_adapter(path) assert torch.equal(patch.replacement_for("blocks.0.extra_table"), table) def test_replacement_for_returns_the_whole_tensor(tmp_path): gate = torch.randn(6, 4) path = write_adapter(tmp_path, {"blocks.0.attn.to_gate_compress.set_weight": gate}) patch = DenseLoRAPatch.from_adapter(path) assert torch.equal(patch.replacement_for("blocks.0.attn.to_gate_compress.weight"), gate) assert patch.replacement_for("blocks.0.attn.to_q.weight") is None # ------------------------------------------------------------------------ reporting def test_unapplied_keys_are_named_in_the_log(tmp_path, caplog): path = write_adapter(tmp_path, { "blocks.0.norm1.diff": torch.zeros(4), "blocks.77.norm1.diff": torch.zeros(4), }) patch = DenseLoRAPatch.from_adapter(path) patch.apply_to("blocks.0.norm1.weight", torch.zeros(4)) with caplog.at_level(logging.WARNING): patch.report_unapplied() assert "blocks.77.norm1.diff" in caplog.text assert "blocks.0.norm1.diff" not in caplog.text def test_a_fully_applied_payload_warns_about_nothing(tmp_path, caplog): path = write_adapter(tmp_path, {"blocks.0.norm1.diff": torch.zeros(4)}) patch = DenseLoRAPatch.from_adapter(path) patch.apply_to("blocks.0.norm1.weight", torch.zeros(4)) with caplog.at_level(logging.WARNING): patch.report_unapplied() assert [r for r in caplog.records if r.levelno >= logging.WARNING] == [] def _stub_packed_nvfp4_export(monkeypatch, tmp_path): from fastvideo.layers.quantization import nvfp4_config packed_path = tmp_path / "packed.safetensors" save_file({"placeholder": torch.zeros(1)}, str(packed_path)) loaded = [] monkeypatch.setattr(nvfp4_config, "find_minimax_h3_nvfp4_dit_export", lambda _paths: str(packed_path)) monkeypatch.setattr(nvfp4_config, "nvfp4_linear_weight_param_names", lambda _model: {"linear.weight"}) monkeypatch.setattr(nvfp4_config, "load_minimax_h3_nvfp4_dit_export", lambda _model, path, device: loaded.append(path)) _stub_fsdp_loading(monkeypatch) return loaded class _PackedTinyModel(torch.nn.Module): param_names_mapping = {r"^hf\.(.*)$": r"custom.\1"} def __init__(self, config=None): super().__init__() def _packed_init_params(): from types import SimpleNamespace return {"config": SimpleNamespace(quant_config=SimpleNamespace(layer_profile="h3_dit"))} def test_packed_nvfp4_export_loads_with_a_low_rank_only_adapter(monkeypatch, tmp_path): loaded = _stub_packed_nvfp4_export(monkeypatch, tmp_path) monkeypatch.setattr(DenseLoRAPatch, "from_adapter", classmethod(lambda *_args, **_kwargs: None)) _load_tiny_model(_PackedTinyModel, lora_path="adapter.safetensors", init_params=_packed_init_params()) assert loaded == [str(tmp_path / "packed.safetensors")] def test_packed_nvfp4_export_rejects_an_adapter_with_dense_payload(monkeypatch, tmp_path): loaded = _stub_packed_nvfp4_export(monkeypatch, tmp_path) patch = DenseLoRAPatch([], {"linear.weight": ("adapter.safetensors", "linear.diff")}, {}, 1.0) monkeypatch.setattr(DenseLoRAPatch, "from_adapter", classmethod(lambda *_args, **_kwargs: patch)) with pytest.raises(ValueError, match="dense"): _load_tiny_model(_PackedTinyModel, lora_path="adapter.safetensors", init_params=_packed_init_params()) assert loaded == []