forked from Karylab-cklius/vllm
Signed-off-by: rasdani <73563550+rasdani@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Roger Wang <hey@rogerw.io>
402 lines
14 KiB
Python
402 lines
14 KiB
Python
# 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
|