# 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 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 not current_platform.supports_fp8(): 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 not current_platform.supports_fp8(): 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 not current_platform.supports_fp8(): 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