# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project from collections import defaultdict import pytest import torch import torch.nn as nn from diffusers.loaders.lora_conversion_utils import ( _convert_non_diffusers_qwen_lora_to_diffusers, ) from pytest_mock import MockerFixture from torch.testing import assert_close from vllm_omni.diffusion.lora.loader import ( LoraLoaderMixin, QwenImageLoraLoaderMixin, WanLoraLoaderMixin, _prepare_lora_delta, _remap_state_dict_keys, _resolve_lora_compute_device, ) HEAD_DIM = 24 RANK = 2 NUM_LAYERS = 10 pytestmark = [pytest.mark.core_model, pytest.mark.cpu] class DummyTransformerBlock(nn.Module): def __init__( self, d_model: int, bias: bool = False, stacked_params: bool = False, ): super().__init__() self.attn = nn.Module() if stacked_params: self.attn.to_qkv = nn.Linear(d_model * 3, d_model, bias=bias) else: self.attn.to_q = nn.Linear(d_model, d_model, bias=bias) self.attn.to_k = nn.Linear(d_model, d_model, bias=bias) self.attn.to_v = nn.Linear(d_model, d_model, bias=bias) self.attn.to_out = nn.Linear(d_model, d_model, bias=bias) class DummyTransformer(nn.Module): def __init__(self, num_layers: int, d_model: int, bias: bool = False, stacked_params: bool = False): super().__init__() self.blocks = nn.ModuleList( [DummyTransformerBlock(d_model, bias=bias, stacked_params=stacked_params) for _ in range(num_layers)] ) if stacked_params: self.stacked_params_mapping = [ (".attn.to_qkv", ".attn.to_q", "q"), (".attn.to_qkv", ".attn.to_k", "k"), (".attn.to_qkv", ".attn.to_v", "v"), ] class DummyPipeline(nn.Module, LoraLoaderMixin): def __init__(self, num_layers: int, d_model: int, bias: bool = False, stacked_params: bool = False): super().__init__() self.transformer = DummyTransformer(num_layers, d_model, bias=bias, stacked_params=stacked_params) # ====================================================== # helper functions for test # ====================================================== def _filter_parameters(named_params, mode: str = "all"): if mode == "all": return named_params filtered = [] for name, param in named_params: if name.endswith(mode): filtered.append((name, param)) return filtered def assert_params_equal(module, original_params, mode: str = "all"): for name, param in _filter_parameters(module.named_parameters(), mode): assert_close(param, original_params[name]) def assert_params_not_equal(module, original_params, mode: str = "all"): for name, param in _filter_parameters(module.named_parameters(), mode): assert not torch.allclose(param, original_params[name]) def make_dummy_lora_state_dict( base_key, weight_a: torch.Tensor | None = None, weight_b: torch.Tensor | None = None, weight_bias: torch.Tensor | None = None, bias: bool = False, lora_a_suffix: str = "lora_A.weight", lora_b_suffix: str = "lora_B.weight", lora_bias_suffix: str = "bias", ): """ Create a dummy LoRA state dict. Returns: tuple: (state_dict, expected_delta) """ key_a = f"{base_key}.{lora_a_suffix}" key_b = f"{base_key}.{lora_b_suffix}" key_bias = f"{base_key}.{lora_bias_suffix}" if weight_a is None: weight_a = torch.randn(RANK, HEAD_DIM) if weight_b is None: weight_b = torch.randn(HEAD_DIM, RANK) state_dict = {} expected_delta = None if bias: state_dict[key_a] = weight_a state_dict[key_b] = weight_b weight_bias = torch.randn(HEAD_DIM) state_dict[key_bias] = weight_bias expected_delta = weight_bias else: state_dict[key_a] = weight_a state_dict[key_b] = weight_b expected_delta = weight_b @ weight_a return state_dict, expected_delta def make_lora_state_dict_for_module( module, lora_rank=RANK, bias=False, lora_a_suffix: str = "lora_A.weight", lora_b_suffix: str = "lora_B.weight", lora_bias_suffix: str = "bias", prefix: str = "transformer", remap_proj_out: bool = True, ): param_to_weight_names = defaultdict(list) if hasattr(module, "stacked_params_mapping"): for param_name, weight_name, _ in module.stacked_params_mapping: param_to_weight_names[param_name].append(weight_name) state_dict = {} for name, param in module.named_parameters(prefix=prefix): if name.endswith(".weight"): base_key = name[: -len(".weight")] if remap_proj_out and base_key.endswith(".to_out"): base_key = base_key.replace(".to_out", ".to_out.0") d_in, d_out = param.shape is_stacked_param = False for param_name, weight_names in param_to_weight_names.items(): if param_name not in name: continue is_stacked_param = True target_in = d_in // len(weight_names) for weight_name in weight_names: lora_key = base_key.replace(param_name, weight_name) weight_a = torch.randn(lora_rank, d_out) weight_b = torch.randn(target_in, lora_rank) state_dict[f"{lora_key}.{lora_a_suffix}"] = weight_a state_dict[f"{lora_key}.{lora_b_suffix}"] = weight_b if is_stacked_param: continue weight_a = torch.randn(lora_rank, d_out) weight_b = torch.randn(d_in, lora_rank) key_a = f"{base_key}.{lora_a_suffix}" key_b = f"{base_key}.{lora_b_suffix}" state_dict[key_a] = weight_a state_dict[key_b] = weight_b elif name.endswith(".bias") and bias: base_key = name[: -len(".bias")] if remap_proj_out and base_key.endswith(".to_out"): base_key = base_key.replace(".to_out", ".to_out.0") d_bias = param.shape[0] is_stacked_param = False for param_name, weight_names in param_to_weight_names.items(): if param_name not in name: continue is_stacked_param = True target_bias = d_bias // len(weight_names) for weight_name in weight_names: lora_key = base_key.replace(param_name, weight_name) weight_bias = torch.randn(target_bias) state_dict[f"{lora_key}.{lora_bias_suffix}"] = weight_bias if is_stacked_param: continue key_bias = f"{base_key}.{lora_bias_suffix}" state_dict[key_bias] = torch.randn_like(param) return state_dict # ====================================================== # Test loader helper functions # ====================================================== def test_prepare_lora_delta_with_stacked_params(): base_key = "transformer.attn.to_qkv" sd0, delta0 = make_dummy_lora_state_dict("transformer.attn.to_q") sd1, delta1 = make_dummy_lora_state_dict("transformer.attn.to_k") sd2, delta2 = make_dummy_lora_state_dict("transformer.attn.to_v") lora_state_dict = {} lora_state_dict.update(sd0) lora_state_dict.update(sd1) lora_state_dict.update(sd2) expected_delta = torch.concat([delta0, delta1, delta2]) stacked_mapping = {".attn.to_qkv": [".attn.to_q", ".attn.to_k", ".attn.to_v"]} actual_delta, used_keys = _prepare_lora_delta( lora_state_dict, base_key, stacked_mapping, ) # validate the delta assert actual_delta is not None assert_close(actual_delta, expected_delta) # validate the used keys key_q_a = "transformer.attn.to_q.lora_A.weight" key_q_b = "transformer.attn.to_q.lora_B.weight" key_k_a = "transformer.attn.to_k.lora_A.weight" key_k_b = "transformer.attn.to_k.lora_B.weight" key_v_a = "transformer.attn.to_v.lora_A.weight" key_v_b = "transformer.attn.to_v.lora_B.weight" for key in [key_q_a, key_q_b, key_k_a, key_k_b, key_v_a, key_v_b]: assert key in used_keys def test_remap_state_dict_keys_matched_two(): lora_state_dict, _ = make_dummy_lora_state_dict( "transformer.attn.to_out.0", bias=True, lora_bias_suffix="lora_B.bias" ) lora_state_dict = _remap_state_dict_keys( lora_state_dict, [(".to_out.0.", ".to_out."), (".lora_B.bias", ".bias")], ) assert len(lora_state_dict) == 3 assert "transformer.attn.to_out.bias" in lora_state_dict # ====================================================== # Test LoraLoaderMixin # ====================================================== class TestLoraLoaderMixin: @pytest.mark.parametrize( "bias, stacked_params", [ (False, False), (True, False), (False, True), (True, True), ], ids=[ "basic", "bias", "stacked_params", "bias_stacked_params", ], ) def test_load_lora_into_module(self, bias: bool, stacked_params: bool): pipeline = DummyPipeline(NUM_LAYERS, HEAD_DIM, bias=bias, stacked_params=stacked_params) original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()} lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer, bias=bias) assert len(lora_state_dict) > 0 # remap the keys to vllm-omni format lora_state_dict = _remap_state_dict_keys(lora_state_dict, [(".to_out.0.", ".to_out.")]) used_keys = pipeline.load_lora_into_module(lora_state_dict, pipeline.transformer) # validate the parameters are updated assert_params_not_equal(pipeline.transformer, original_params) # validate all keys are used missing_keys = set(lora_state_dict.keys()) - used_keys assert len(missing_keys) == 0 def test_unload_module_lora(self): pipeline = DummyPipeline(NUM_LAYERS, HEAD_DIM) original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()} lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer) # validate the state dict is valid assert len(lora_state_dict) > 0 # remap the keys to vllm-omni format lora_state_dict = _remap_state_dict_keys(lora_state_dict, [(".to_out.0.", ".to_out.")]) used_keys = pipeline.load_lora_into_module(lora_state_dict, pipeline.transformer) # validate the weights are updated assert_params_not_equal(pipeline.transformer, original_params) # validate all keys are used missing_keys = set(lora_state_dict.keys()) - used_keys assert len(missing_keys) == 0 unload_used_keys = pipeline.unload_module_lora(lora_state_dict, pipeline.transformer) missing_keys = set(lora_state_dict.keys()) - unload_used_keys assert len(missing_keys) == 0 assert len(unload_used_keys - used_keys) == 0 # validate the weights are restored assert_params_equal(pipeline.transformer, original_params) # ====================================================== # Test QwenImageLoraLoaderMixin # ====================================================== class DummyQwenImagePipeline(nn.Module, QwenImageLoraLoaderMixin): def __init__(self, num_layers: int, d_model: int, bias: bool = False): super().__init__() self.transformer = DummyTransformer(num_layers, d_model, bias=bias, stacked_params=True) class TestQwenImageLoraLoaderMixin: def test_load_lora_weights(self, mocker: MockerFixture): pipeline = DummyQwenImagePipeline(NUM_LAYERS, HEAD_DIM) original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()} lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer) assert len(lora_state_dict) > 0 mocker.patch( "vllm_omni.diffusion.lora.loader.get_converter_by_pipeline", return_value=_convert_non_diffusers_qwen_lora_to_diffusers, ) pipeline.load_lora_weights(lora_state_dict, "adapter0") # validate the weights are updated after lora loaded assert_params_not_equal(pipeline.transformer, original_params) # validate lora_loaded map is updated after lora loaded assert "adapter0" in pipeline.lora_loaded def test_unload_lora_weights(self, mocker: MockerFixture): pipeline = DummyQwenImagePipeline(NUM_LAYERS, HEAD_DIM) original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()} lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer) assert len(lora_state_dict) > 0 mocker.patch( "vllm_omni.diffusion.lora.loader.get_converter_by_pipeline", return_value=_convert_non_diffusers_qwen_lora_to_diffusers, ) pipeline.load_lora_weights(lora_state_dict, "adapter0") # validate the weights are updated after lora loaded assert_params_not_equal(pipeline.transformer, original_params) # validate lora_loaded map is updated after lora loaded assert "adapter0" in pipeline.lora_loaded pipeline.unload_lora_weights("adapter0") # validate the weights are restored after lora unloaded assert_params_equal(pipeline.transformer, original_params) # validate lora_loaded map is updated assert "adapter0" not in pipeline.lora_loaded def test_load_diffusers_lora_applies_non_unit_alpha(self, mocker: MockerFixture): pipeline = DummyQwenImagePipeline(1, HEAD_DIM) to_out = pipeline.transformer.blocks[0].attn.to_out original_weight = to_out.weight.detach().clone() base_key = "blocks.0.attn.to_out.0" lora_a = torch.randn(RANK, HEAD_DIM) lora_b = torch.randn(HEAD_DIM, RANK) alpha = torch.tensor(6.0) state_dict = { f"{base_key}.lora_A.weight": lora_a, f"{base_key}.lora_B.weight": lora_b, f"{base_key}.alpha": alpha, } mocker.patch( "vllm_omni.diffusion.lora.loader.get_converter_by_pipeline", return_value=_convert_non_diffusers_qwen_lora_to_diffusers, ) pipeline.load_lora_weights(state_dict, "adapter0") expected_weight = original_weight + (lora_b @ lora_a) * (alpha.item() / RANK) assert_close(to_out.weight, expected_weight) pipeline.unload_lora_weights("adapter0") assert_close(to_out.weight, original_weight) # ====================================================== # Test WanLoraLoaderMixin # ====================================================== class DummyWanPipeline(nn.Module, WanLoraLoaderMixin): def __init__(self, num_layers: int, d_model: int): super().__init__() self.has_transformer_2 = True self.transformer = DummyTransformer(num_layers, d_model, stacked_params=True) self.transformer_2 = DummyTransformer(num_layers, d_model, stacked_params=True) def test_wan_load_lora_weights_targets_both_transformers(mocker: MockerFixture): pipeline = DummyWanPipeline(NUM_LAYERS, HEAD_DIM) original_high = {name: param.clone() for name, param in pipeline.transformer.named_parameters()} original_low = {name: param.clone() for name, param in pipeline.transformer_2.named_parameters()} high_state_dict = make_lora_state_dict_for_module(pipeline.transformer) low_state_dict = make_lora_state_dict_for_module(pipeline.transformer_2) mocker.patch( "vllm_omni.diffusion.lora.loader._load_lora_state_dict", side_effect=[high_state_dict, low_state_dict], ) pipeline.load_lora_weights(["high.safetensors", "low.safetensors"], "adapter0") assert_params_not_equal(pipeline.transformer, original_high) assert_params_not_equal(pipeline.transformer_2, original_low) assert set(pipeline.lora_loaded["adapter0"]) == {"transformer", "transformer_2"} pipeline.unload_lora_weights("adapter0") assert_params_equal(pipeline.transformer, original_high) assert_params_equal(pipeline.transformer_2, original_low) assert "adapter0" not in pipeline.lora_loaded def test_prepare_lora_delta_disambiguates_prefix_overlapping_stacked_names(): """A fused name can be a prefix of another fused name. SenseNova-U1 carries both `.qkv_proj` (understanding) and `.qkv_proj_mot_gen` (generation). Selecting stacked mappings by substring matched both rules for the generation weight and concatenated six shards into a delta twice as tall as the parameter. """ param_to_weight_names = { ".qkv_proj_mot_gen": [".q_proj_mot_gen", ".k_proj_mot_gen", ".v_proj_mot_gen"], ".qkv_proj": [".q_proj", ".k_proj", ".v_proj"], } state_dict = {} for part in ("q", "k", "v"): for tower in ("_mot_gen", ""): base = f"layers.0.self_attn.{part}_proj{tower}" state_dict[f"{base}.lora_A.weight"] = torch.ones(RANK, HEAD_DIM) state_dict[f"{base}.lora_B.weight"] = torch.ones(HEAD_DIM, RANK) for base_key in ("layers.0.self_attn.qkv_proj_mot_gen", "layers.0.self_attn.qkv_proj"): delta, _ = _prepare_lora_delta(state_dict, base_key, param_to_weight_names) assert delta.shape == (3 * HEAD_DIM, HEAD_DIM), base_key @pytest.mark.parametrize( "fused, shards", [ # qwen_image_transformer.py stacked_params_mapping (".to_qkv", (".to_q", ".to_k", ".to_v")), (".add_kv_proj", (".add_q_proj", ".add_k_proj", ".add_v_proj")), # wan2_2_transformer.py stacked_params_mapping (".attn1.to_qkv", (".attn1.to_q", ".attn1.to_k", ".attn1.to_v")), # wan2_2_s2v_transformer.py stacked_params_mapping (".self_attn.to_qkv", (".self_attn.q", ".self_attn.k", ".self_attn.v")), ], ) def test_prepare_lora_delta_unchanged_for_existing_fused_layouts(fused, shards): """None of the fused names shipped today overlap by prefix, so matching on the tail must select exactly the shards substring matching selected.""" param_to_weight_names = {fused: list(shards)} base_key = f"blocks.0{fused}" state_dict = {} for shard in shards: state_dict[f"blocks.0{shard}.lora_A.weight"] = torch.ones(RANK, HEAD_DIM) state_dict[f"blocks.0{shard}.lora_B.weight"] = torch.ones(HEAD_DIM, RANK) delta, used_keys = _prepare_lora_delta(state_dict, base_key, param_to_weight_names) assert delta.shape == (len(shards) * HEAD_DIM, HEAD_DIM) assert len(used_keys) == 2 * len(shards) def test_lora_loader_mixin_lora_is_fused_property(mocker: MockerFixture): pipeline = DummyQwenImagePipeline(1, HEAD_DIM) assert pipeline.lora_is_fused is False lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer) mocker.patch( "vllm_omni.diffusion.lora.loader.get_converter_by_pipeline", return_value=_convert_non_diffusers_qwen_lora_to_diffusers, ) pipeline.load_lora_weights(lora_state_dict, "adapter0") assert pipeline.lora_is_fused is True # Test explicit setter pipeline2 = DummyQwenImagePipeline(1, HEAD_DIM) assert pipeline2.lora_is_fused is False pipeline2.lora_is_fused = True assert pipeline2.lora_is_fused is True def test_prepare_lora_delta_with_compute_device(): base_key = "blocks.0.attn.to_q" state_dict = { f"{base_key}.lora_A.weight": torch.ones(RANK, HEAD_DIM), f"{base_key}.lora_B.weight": torch.ones(HEAD_DIM, RANK), } delta, used_keys = _prepare_lora_delta(state_dict, base_key, compute_device=torch.device("cpu")) assert delta is not None assert delta.shape == (HEAD_DIM, HEAD_DIM) assert len(used_keys) == 2 def test_resolve_lora_compute_device_probes_unusable_accelerator(mocker: MockerFixture): """cuda:N/npu:N may be reported without a physical device; the probe must reject it.""" mocker.patch( "vllm_omni.diffusion.lora.loader.get_local_device", return_value=torch.device("cuda:999"), ) assert _resolve_lora_compute_device() is None def test_load_lora_into_module_cpu_weights_match_cpu_reference(mocker: MockerFixture): """Regression test: params on CPU used to force compute_device=None and skip the #7005 accelerator path. With an unusable accelerator, fusion must fall back to CPU and still produce base + lora_B @ lora_A.""" pipeline = DummyPipeline(NUM_LAYERS, HEAD_DIM) original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()} lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer) lora_state_dict = _remap_state_dict_keys(lora_state_dict, [(".to_out.0.", ".to_out.")]) mocker.patch( "vllm_omni.diffusion.lora.loader.get_local_device", return_value=torch.device("cuda:999"), ) used_keys = pipeline.load_lora_into_module(lora_state_dict, pipeline.transformer) assert len(used_keys) > 0 assert not (set(lora_state_dict.keys()) - used_keys) for name, param in pipeline.transformer.named_parameters(): if not name.endswith(".weight"): continue base_key = name[: -len(".weight")] key_a = f"{base_key}.lora_A.weight" key_b = f"{base_key}.lora_B.weight" if key_a in lora_state_dict and key_b in lora_state_dict: expected = original_params[name] + ( lora_state_dict[key_b].to(param.dtype) @ lora_state_dict[key_a].to(param.dtype) ) assert_close(param, expected) def test_load_lora_into_module_forwards_usable_accel_device(mocker: MockerFixture): """A usable accelerator must be forwarded as compute_device for CPU params. A spy forces the actual math back onto CPU so the test runs without a device.""" fake_dev = torch.device("cuda:0") mocker.patch( "vllm_omni.diffusion.lora.loader._resolve_lora_compute_device", return_value=fake_dev, ) seen_devices = [] original = _prepare_lora_delta def spy(state_dict, base_key, *args, **kwargs): seen_devices.append(kwargs.get("compute_device")) kwargs["compute_device"] = None return original(state_dict, base_key, *args, **kwargs) mocker.patch( "vllm_omni.diffusion.lora.loader._prepare_lora_delta", side_effect=spy, ) pipeline = DummyPipeline(NUM_LAYERS, HEAD_DIM) original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()} lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer) lora_state_dict = _remap_state_dict_keys(lora_state_dict, [(".to_out.0.", ".to_out.")]) used_keys = pipeline.load_lora_into_module(lora_state_dict, pipeline.transformer) # Every CPU param with a LoRA delta must have received the accelerator device. assert seen_devices assert all(d == fake_dev for d in seen_devices) assert not (set(lora_state_dict.keys()) - used_keys) assert_params_not_equal(pipeline.transformer, original_params)