# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import gc import inspect from weakref import WeakKeyDictionary, ref import pytest import torch from torch.nn.parameter import UninitializedParameter import vllm.model_executor.model_loader.reload.meta as reload_meta from vllm.model_executor.layers.linear import QKVParallelLinear from vllm.model_executor.model_loader.reload.layerwise import ( finalize_layerwise_reload, initialize_layerwise_reload, record_metadata_for_reloading, ) from vllm.model_executor.model_loader.reload.meta import ( capture_layer_to_meta, get_numel_loaded, materialize_layer, materialize_meta_tensor, restore_layer_on_meta, to_meta_tensor, ) from vllm.model_executor.model_loader.reload.types import LayerReloadingInfo from vllm.model_executor.model_loader.reload.utils import get_layer_tensors from vllm.platforms import current_platform def _fp8_reload_unsupported() -> bool: """Whether the FP8 reload/online-quantize tests should be skipped. ``supports_fp8()`` returns True on MI250 (gfx90a) because the general quantization paths upcast FP8 weights, but gfx90a has no native FP8 and cannot run these reload models, so treat it as unsupported here. """ if not current_platform.supports_fp8(): return True if current_platform.is_rocm(): from vllm.platforms.rocm import on_gfx90a return on_gfx90a() return False class _AliasedBufferLayer(torch.nn.Module): def __init__(self): super().__init__() weight = torch.arange(6, dtype=torch.float32).reshape(2, 3) self.weight = torch.nn.Parameter(weight) self.register_buffer( "weight_view", self.weight.detach().view(-1), persistent=False ) class _ParentAliasedChildBufferLayer(torch.nn.Module): def __init__(self): super().__init__() self.scale = torch.nn.Parameter(torch.ones(1)) self.conv1d = torch.nn.Linear(3, 2, bias=False) self.conv1d.weight.data.copy_( torch.arange(6, dtype=torch.float32).reshape(2, 3) ) self.register_buffer( "conv_weights", self.conv1d.weight.detach().view(-1), persistent=False ) class _AliasedBufferWithUninitializedChildLayer(_AliasedBufferLayer): def __init__(self): super().__init__() self.child = torch.nn.Module() self.child.register_parameter( "lazy_weight", UninitializedParameter(requires_grad=False) ) def test_move_metatensors(): tensor = torch.empty((1, 2, 3)) meta_tensor = to_meta_tensor(tensor) materialized_tensor = materialize_meta_tensor(meta_tensor) assert meta_tensor.device.type == "meta" assert tensor.device == materialized_tensor.device assert tensor.dtype == meta_tensor.dtype == materialized_tensor.dtype assert tensor.shape == meta_tensor.shape == materialized_tensor.shape assert tensor.__class__ == meta_tensor.__class__ == materialized_tensor.__class__ assert tensor.__dict__ == meta_tensor.__dict__ == materialized_tensor.__dict__ def test_reload_lifecycle(): layer = torch.nn.Linear(2, 3) info = LayerReloadingInfo( restore_metadata=capture_layer_to_meta(layer), restore_device=torch.device("cpu"), ) restore_layer_on_meta(layer, info) for name, tensor in get_layer_tensors(layer).items(): meta_tensor = getattr(layer, name) assert tensor.dtype == meta_tensor.dtype assert tensor.shape == meta_tensor.shape assert tensor.__class__ == meta_tensor.__class__ assert tensor.__dict__ == meta_tensor.__dict__ materialize_layer(layer, info) for name, tensor in get_layer_tensors(layer).items(): materialized_tensor = getattr(layer, name) assert tensor.dtype == materialized_tensor.dtype assert tensor.shape == materialized_tensor.shape assert tensor.__class__ == materialized_tensor.__class__ assert tensor.__dict__ == materialized_tensor.__dict__ def test_materialize_layer_preserves_non_meta_tensors(): """Ensure that materialize_layer does not overwrite non meta tensors.""" layer = torch.nn.Linear(2, 3, bias=True) # Create a non meta bias tensor and meta weight, which can happen with FP8 bias_values = torch.ones(3) layer.bias.data.copy_(bias_values) layer.weight = torch.nn.Parameter(layer.weight.data.to("meta")) assert layer.weight.is_meta assert not layer.bias.is_meta # materialize the layer weights after the bias is initialized info = LayerReloadingInfo( restore_metadata=({}, {}), restore_device=torch.device("cpu"), ) materialize_layer(layer, info) # Ensure the weight materialized off meta assert not layer.weight.is_meta assert layer.weight.device.type == "cpu" # Ensure that the bias is (still) not meta and values are unchanged assert not layer.bias.is_meta assert torch.equal(layer.bias.data, bias_values) def test_model_cleanup(dist_init, default_vllm_config): layer = QKVParallelLinear(2, 3, 4) assert layer.weight.weight_loader.__self__ is layer info = LayerReloadingInfo( restore_metadata=capture_layer_to_meta(layer), restore_device=torch.device("cpu"), ) mock_info_dict: WeakKeyDictionary[torch.nn.Module, LayerReloadingInfo] = ( WeakKeyDictionary() ) mock_info_dict[layer] = info layer_ref = ref(layer) del layer gc.collect() assert layer_ref() is None assert len(mock_info_dict) == 0 def test_get_numel_loaded(): param = torch.empty(10, device="meta") loaded_weight = torch.empty(10) def complex_weight_loader(param, loaded_weight): param[:3] = loaded_weight[:3] param[5:8] = loaded_weight[5:8] return "value" args = inspect.signature(complex_weight_loader).bind(param, loaded_weight) num_loaded, ret = get_numel_loaded(complex_weight_loader, args) assert num_loaded == 6 assert ret == "value" def test_layerwise_reload_skips_non_persistent_parameter_alias_buffers(monkeypatch): layer = _AliasedBufferLayer() model = torch.nn.Sequential(layer) loaded_weight = torch.full_like(layer.weight, 7.0) def materialize_with_sentinel(meta_tensor): tensor = torch.empty_strided( size=tuple(meta_tensor.size()), stride=tuple(meta_tensor.stride()), dtype=meta_tensor.dtype, requires_grad=False, ) tensor.fill_(-123.0) tensor.__class__ = meta_tensor.__class__ tensor.__dict__ = meta_tensor.__dict__.copy() return tensor monkeypatch.setattr( reload_meta, "materialize_meta_tensor", materialize_with_sentinel ) record_metadata_for_reloading(model) initialize_layerwise_reload(model) layer.weight.weight_loader(layer.weight, loaded_weight) finalize_layerwise_reload(model, model_config=None) assert torch.equal(layer.weight, loaded_weight) assert layer.weight_view.untyped_storage().data_ptr() == ( layer.weight.untyped_storage().data_ptr() ) def test_capture_layer_to_meta_skips_uninitialized_parameter_storage_ptrs(): layer = _AliasedBufferWithUninitializedChildLayer() _, buffers = capture_layer_to_meta(layer) assert "weight_view" not in buffers def test_layerwise_reload_skips_child_parameter_alias_buffers(monkeypatch): layer = _ParentAliasedChildBufferLayer() model = torch.nn.Sequential(layer) loaded_conv = torch.full_like(layer.conv1d.weight, 7.0) loaded_scale = torch.full_like(layer.scale, 3.0) def materialize_with_sentinel(meta_tensor): tensor = torch.empty_strided( size=tuple(meta_tensor.size()), stride=tuple(meta_tensor.stride()), dtype=meta_tensor.dtype, requires_grad=False, ) tensor.fill_(-123.0) tensor.__class__ = meta_tensor.__class__ tensor.__dict__ = meta_tensor.__dict__.copy() return tensor monkeypatch.setattr( reload_meta, "materialize_meta_tensor", materialize_with_sentinel ) record_metadata_for_reloading(model) initialize_layerwise_reload(model) layer.conv1d.weight.weight_loader(layer.conv1d.weight, loaded_conv) layer.scale.weight_loader(layer.scale, loaded_scale) finalize_layerwise_reload(model, model_config=None) assert torch.equal(layer.conv1d.weight, loaded_conv) assert torch.equal(layer.conv_weights, loaded_conv.view(-1)) assert layer.conv_weights.untyped_storage().data_ptr() == ( layer.conv1d.weight.untyped_storage().data_ptr() ) @pytest.mark.parametrize( "tp_size", [pytest.param(1), pytest.param(2, marks=[pytest.mark.slow_test])] ) @pytest.mark.parametrize( "base_model,mul_model,add_model", [ pytest.param( "Qwen/Qwen3-0.6B", "inference-optimization/Qwen3-0.6B-debug-multiply", "inference-optimization/Qwen3-0.6B-debug-add", marks=[pytest.mark.slow_test], ), pytest.param( "inference-optimization/Qwen3-0.6B-FP8_BLOCK", "inference-optimization/Qwen3-0.6B-debug-multiply-FP8_BLOCK", "inference-optimization/Qwen3-0.6B-debug-add-FP8_BLOCK", marks=[pytest.mark.slow_test], ), pytest.param( "inference-optimization/Qwen3-0.6B-W4A16-G128", "inference-optimization/Qwen3-0.6B-debug-multiply-W4A16-G128", "inference-optimization/Qwen3-0.6B-debug-add-W4A16-G128", marks=[pytest.mark.slow_test], ), pytest.param( "inference-optimization/DeepSeek-V3-debug-empty", "inference-optimization/DeepSeek-V3-debug-multiply", "inference-optimization/DeepSeek-V3-debug-add", marks=[pytest.mark.slow_test], ), pytest.param( "inference-optimization/DeepSeek-V3-debug-empty-FP8_DYNAMIC", "inference-optimization/DeepSeek-V3-debug-multiply-FP8_DYNAMIC", "inference-optimization/DeepSeek-V3-debug-add-FP8_DYNAMIC", ), pytest.param( "inference-optimization/DeepSeek-V3-debug-empty-NVFP4A16", "inference-optimization/DeepSeek-V3-debug-multiply-NVFP4A16", "inference-optimization/DeepSeek-V3-debug-add-NVFP4A16", marks=[pytest.mark.slow_test], ), ], ) def test_reload_weights(base_model, mul_model, add_model, tp_size, vllm_runner): if current_platform.device_count() < tp_size: pytest.skip(reason="Not enough CUDA devices") if "FP8" in base_model and _fp8_reload_unsupported(): pytest.skip(reason="Requires FP8 support") with vllm_runner( model_name=base_model, tensor_parallel_size=tp_size, enable_expert_parallel=(tp_size > 1 and "DeepSeek" in base_model), enable_prefix_caching=False, max_model_len=16, max_num_seqs=1, ) as llm: llm.collective_rpc("reload_weights", kwargs={"weights_path": mul_model}) mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0] add_perp = llm.generate_prompt_perplexity(["3 4 = 7"], mask=["3 4 ="])[0] assert mul_perp < add_perp llm.collective_rpc("reload_weights", kwargs={"weights_path": add_model}) mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0] add_perp = llm.generate_prompt_perplexity(["3 4 = 7"], mask=["3 4 ="])[0] assert add_perp < mul_perp def test_kv_scale_reload(vllm_runner): """Test reloading a checkpoint that contains k_scale/v_scale weights.""" if _fp8_reload_unsupported(): pytest.skip(reason="Requires FP8 support") model = "nm-testing/Llama-3.2-1B-Instruct-FP8-KV" # Load dummy weights, then reload real checkpoint with vllm_runner( model_name=model, load_format="dummy", enable_prefix_caching=False, max_model_len=16, max_num_seqs=1, ) as llm: llm.collective_rpc( "update_config", kwargs={"overrides": {"load_config": {"load_format": "auto"}}}, ) llm.collective_rpc("reload_weights", kwargs={"weights_path": model}) reloaded_perp = llm.generate_prompt_perplexity( ["The capital of France is the city of Paris"], mask=["The capital of France is"], )[0] assert reloaded_perp < 10 @pytest.mark.parametrize( "tp_size", [pytest.param(1), pytest.param(2, marks=[pytest.mark.slow_test])] ) @pytest.mark.parametrize( "base_model,mul_model,add_model,quantization", [ pytest.param( "Qwen/Qwen3-0.6B", "inference-optimization/Qwen3-0.6B-debug-multiply", "inference-optimization/Qwen3-0.6B-debug-add", "fp8", ), pytest.param( "inference-optimization/DeepSeek-V3-debug-empty", "inference-optimization/DeepSeek-V3-debug-multiply", "inference-optimization/DeepSeek-V3-debug-add", "fp8", marks=[pytest.mark.slow_test], ), pytest.param( "Qwen/Qwen3-0.6B", "inference-optimization/Qwen3-0.6B-debug-multiply", "inference-optimization/Qwen3-0.6B-debug-add", "mxfp8", marks=[pytest.mark.slow_test], ), pytest.param( "inference-optimization/DeepSeek-V3-debug-empty", "inference-optimization/DeepSeek-V3-debug-multiply", "inference-optimization/DeepSeek-V3-debug-add", "mxfp8", marks=[ pytest.mark.slow_test, pytest.mark.xfail(reason="mxfp4 & mla is not supported yet"), ], ), ], ) def test_online_quantize_reload( base_model, mul_model, add_model, quantization, tp_size, vllm_runner ): if current_platform.device_count() < tp_size: pytest.skip(reason="Not enough GPU devices") if quantization == "fp8" and _fp8_reload_unsupported(): pytest.skip(reason="Requires FP8 support") with vllm_runner( model_name=base_model, quantization=quantization, tensor_parallel_size=tp_size, enable_expert_parallel=(tp_size > 1 and "DeepSeek" in base_model), enable_prefix_caching=False, max_model_len=16, max_num_seqs=1, ) as llm: llm.collective_rpc("reload_weights", kwargs={"weights_path": mul_model}) mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0] add_perp = llm.generate_prompt_perplexity(["3 4 = 7"], mask=["3 4 ="])[0] assert mul_perp < add_perp llm.collective_rpc("reload_weights", kwargs={"weights_path": add_model}) mul_perp = llm.generate_prompt_perplexity(["3 4 = 12"], mask=["3 4 ="])[0] add_perp = llm.generate_prompt_perplexity(["3 4 = 7"], mask=["3 4 ="])[0] assert add_perp < mul_perp