mirror of
https://github.com/vllm-project/vllm.git
synced 2026-08-10 07:48:09 +00:00
Signed-off-by: Isotr0py <[email protected]> Signed-off-by: Isotr0py <[email protected]>
471 lines
15 KiB
Python
471 lines
15 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Test the functionality of the Transformers modeling backend."""
|
|
|
|
import contextlib
|
|
import os
|
|
import tempfile
|
|
from typing import Any
|
|
|
|
import pytest
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
from ...conftest import HfRunner, VllmRunner
|
|
from ...utils import multi_gpu_test, prep_prompts
|
|
from ..registry import HF_EXAMPLE_MODELS
|
|
from ..utils import check_embeddings_close, check_logprobs_close
|
|
|
|
|
|
@pytest.fixture(scope="function", autouse=True)
|
|
def enable_pickle(monkeypatch):
|
|
"""`LLM.apply_model` requires pickling a function."""
|
|
monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
|
|
|
|
|
|
def get_model(arch: str) -> str:
|
|
model_info = HF_EXAMPLE_MODELS.get_hf_info(arch)
|
|
model_info.check_transformers_version(on_fail="skip")
|
|
return model_info.default
|
|
|
|
|
|
def get_num_fused(model) -> tuple[int, int]:
|
|
from vllm.model_executor.layers.linear import (
|
|
MergedColumnParallelLinear,
|
|
QKVParallelLinear,
|
|
)
|
|
|
|
glu = sum(isinstance(m, MergedColumnParallelLinear) for m in model.modules())
|
|
qkv = sum(isinstance(m, QKVParallelLinear) for m in model.modules())
|
|
return glu, qkv
|
|
|
|
|
|
def count_mla_layers(model) -> int:
|
|
from vllm.model_executor.layers.attention import MLAAttention
|
|
|
|
return sum(isinstance(m, MLAAttention) for m in model.attention_instances.values())
|
|
|
|
|
|
def check_implementation(
|
|
runner_ref: type[HfRunner | VllmRunner],
|
|
runner_test: type[VllmRunner],
|
|
example_prompts: list[str],
|
|
model: str,
|
|
kwargs_ref: dict[str, Any] | None = None,
|
|
kwargs_test: dict[str, Any] | None = None,
|
|
num_fused: tuple[int, int] = (1, 1),
|
|
**kwargs,
|
|
):
|
|
if kwargs_ref is None:
|
|
kwargs_ref = {}
|
|
if kwargs_test is None:
|
|
kwargs_test = {}
|
|
|
|
max_tokens = 32
|
|
num_logprobs = 5
|
|
|
|
args = (example_prompts, max_tokens, num_logprobs)
|
|
|
|
with runner_test(model, **kwargs_test, **kwargs) as model_test:
|
|
model_config = model_test.llm.llm_engine.model_config
|
|
assert model_config.using_transformers_backend()
|
|
|
|
num_layers = model_config.hf_config.get_text_config().num_hidden_layers
|
|
expected_glu, expected_qkv = num_fused
|
|
for num_glu, num_qkv in model_test.apply_model(get_num_fused):
|
|
assert num_glu == expected_glu * num_layers
|
|
assert num_qkv == expected_qkv * num_layers
|
|
|
|
outputs_test = model_test.generate_greedy_logprobs(*args)
|
|
|
|
with runner_ref(model, **kwargs_ref) as model_ref:
|
|
if isinstance(model_ref, VllmRunner):
|
|
outputs_ref = model_ref.generate_greedy_logprobs(*args)
|
|
else:
|
|
outputs_ref = model_ref.generate_greedy_logprobs_limit(*args)
|
|
|
|
check_logprobs_close(
|
|
outputs_0_lst=outputs_ref,
|
|
outputs_1_lst=outputs_test,
|
|
name_0="ref",
|
|
name_1="test",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model,model_impl,num_fused",
|
|
[
|
|
("meta-llama/Llama-3.2-1B-Instruct", "transformers", (1, 1)),
|
|
("hmellor/Ilama-3.2-1B", "auto", (1, 1)), # CUSTOM CODE
|
|
("allenai/OLMoE-1B-7B-0924", "transformers", (0, 1)), # MoE
|
|
],
|
|
) # trust_remote_code=True by default
|
|
def test_models(
|
|
hf_runner: type[HfRunner],
|
|
vllm_runner: type[VllmRunner],
|
|
example_prompts: list[str],
|
|
model: str,
|
|
model_impl: str,
|
|
num_fused: tuple[int, int],
|
|
) -> None:
|
|
check_implementation(
|
|
hf_runner,
|
|
vllm_runner,
|
|
example_prompts,
|
|
model,
|
|
num_fused=num_fused,
|
|
model_impl=model_impl,
|
|
)
|
|
|
|
|
|
def test_hybrid_attention(vllm_runner: type[VllmRunner]) -> None:
|
|
prompts, _, _ = prep_prompts(4, (800, 801))
|
|
kwargs_ref = {"max_model_len": 8192, "enforce_eager": True}
|
|
kwargs_test = {"model_impl": "transformers", **kwargs_ref}
|
|
check_implementation(
|
|
vllm_runner,
|
|
vllm_runner,
|
|
prompts,
|
|
model="hmellor/tiny-random-Gemma2ForCausalLM",
|
|
kwargs_ref=kwargs_ref,
|
|
kwargs_test=kwargs_test,
|
|
)
|
|
|
|
|
|
def test_mla(vllm_runner: type[VllmRunner], example_prompts: list[str]) -> None:
|
|
import transformers
|
|
from packaging.version import Version
|
|
|
|
installed = Version(transformers.__version__)
|
|
required = Version("5.15.0.dev0")
|
|
if installed < required:
|
|
pytest.skip(
|
|
"MLA models with the Transformers modeling backend require "
|
|
f"transformers>={required}, but got {installed}"
|
|
)
|
|
|
|
model = get_model("DeepseekV2ForCausalLM") # DeepSeek-V2-Lite, MLA + MoE
|
|
args = (example_prompts, 32, 5)
|
|
kwargs: dict[str, Any] = {"max_model_len": 2048, "enforce_eager": True}
|
|
|
|
with vllm_runner(
|
|
model, model_impl="transformers", trust_remote_code=False, **kwargs
|
|
) as model_test:
|
|
model_config = model_test.llm.llm_engine.model_config
|
|
assert model_config.using_transformers_backend()
|
|
num_layers = model_config.hf_config.get_text_config().num_hidden_layers
|
|
assert model_test.apply_model(count_mla_layers) == [num_layers]
|
|
outputs_test = model_test.generate_greedy_logprobs(*args)
|
|
|
|
with vllm_runner(model, model_impl="auto") as model_ref:
|
|
outputs_ref = model_ref.generate_greedy_logprobs(*args)
|
|
|
|
check_logprobs_close(
|
|
outputs_0_lst=outputs_ref,
|
|
outputs_1_lst=outputs_test,
|
|
name_0="native",
|
|
name_1="transformers",
|
|
)
|
|
|
|
|
|
@multi_gpu_test(num_gpus=2)
|
|
def test_distributed(
|
|
hf_runner: type[HfRunner],
|
|
vllm_runner: type[VllmRunner],
|
|
example_prompts,
|
|
):
|
|
kwargs = {"model_impl": "transformers", "tensor_parallel_size": 2}
|
|
check_implementation(
|
|
hf_runner,
|
|
vllm_runner,
|
|
example_prompts,
|
|
"meta-llama/Llama-3.2-1B-Instruct",
|
|
kwargs_test=kwargs,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model, quantization_kwargs",
|
|
[
|
|
("TheBloke/TinyLlama-1.1B-Chat-v0.3-AWQ", {}),
|
|
("TheBloke/TinyLlama-1.1B-Chat-v0.3-GPTQ", {}),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("max_tokens", [32])
|
|
@pytest.mark.parametrize("num_logprobs", [5])
|
|
def test_quantization(
|
|
vllm_runner: type[VllmRunner],
|
|
example_prompts: list[str],
|
|
model: str,
|
|
quantization_kwargs: dict[str, str],
|
|
max_tokens: int,
|
|
num_logprobs: int,
|
|
) -> None:
|
|
with vllm_runner(
|
|
model,
|
|
model_impl="auto",
|
|
enforce_eager=True,
|
|
**quantization_kwargs, # type: ignore[arg-type]
|
|
) as vllm_model:
|
|
vllm_outputs = vllm_model.generate_greedy_logprobs(
|
|
example_prompts, max_tokens=max_tokens, num_logprobs=num_logprobs
|
|
)
|
|
|
|
with vllm_runner(
|
|
model,
|
|
model_impl="transformers",
|
|
enforce_eager=True,
|
|
**quantization_kwargs, # type: ignore[arg-type]
|
|
) as vllm_model:
|
|
model_config = vllm_model.llm.llm_engine.model_config
|
|
assert model_config.using_transformers_backend()
|
|
|
|
transformers_outputs = vllm_model.generate_greedy_logprobs(
|
|
example_prompts, max_tokens=max_tokens, num_logprobs=num_logprobs
|
|
)
|
|
|
|
check_logprobs_close(
|
|
outputs_0_lst=transformers_outputs,
|
|
outputs_1_lst=vllm_outputs,
|
|
name_0="transformers",
|
|
name_1="vllm",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model",
|
|
[
|
|
# Layers live in `layers`
|
|
"Qwen/Qwen3-Embedding-0.6B",
|
|
# Layers live in `model.layers`
|
|
"meta-llama/Llama-3.2-1B-Instruct",
|
|
],
|
|
)
|
|
def test_embed_loading(vllm_runner, model):
|
|
with vllm_runner(
|
|
model,
|
|
max_model_len=1024,
|
|
enforce_eager=True,
|
|
runner="pooling",
|
|
model_impl="transformers",
|
|
) as model_test:
|
|
model_config = model_test.llm.llm_engine.model_config
|
|
assert model_config.using_transformers_backend()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"arch", ["TransformersEmbeddingModel", "TransformersForSequenceClassification"]
|
|
)
|
|
def test_pooling(hf_runner, vllm_runner, example_prompts, arch):
|
|
model = get_model(arch)
|
|
|
|
vllm_kwargs = dict(max_model_len=None, model_impl="transformers")
|
|
|
|
hf_kwargs = dict()
|
|
if arch == "TransformersEmbeddingModel":
|
|
hf_kwargs["is_sentence_transformer"] = True
|
|
elif arch == "TransformersForSequenceClassification":
|
|
from transformers import AutoModelForSequenceClassification
|
|
|
|
hf_kwargs["auto_cls"] = AutoModelForSequenceClassification
|
|
|
|
# The example_prompts has ending "\n", for example:
|
|
# "Write a short story about a robot that dreams for the first time.\n"
|
|
# sentence_transformers will strip the input texts, see:
|
|
# https://github.com/UKPLab/sentence-transformers/blob/v3.1.1/sentence_transformers/models/Transformer.py#L159
|
|
# This makes the input_ids different between hf_model and vllm_model.
|
|
# So we need to strip the input texts to avoid test failing.
|
|
example_prompts = [str(s).strip() for s in example_prompts]
|
|
|
|
with (
|
|
vllm_runner(model, **vllm_kwargs) as vllm_model,
|
|
hf_runner(model, **hf_kwargs) as hf_model,
|
|
):
|
|
model_config = vllm_model.llm.llm_engine.model_config
|
|
assert model_config.using_transformers_backend()
|
|
|
|
if arch == "TransformersEmbeddingModel":
|
|
vllm_outputs = vllm_model.embed(example_prompts)
|
|
hf_outputs = hf_model.encode(example_prompts)
|
|
elif arch == "TransformersForSequenceClassification":
|
|
vllm_outputs = vllm_model.classify(example_prompts)
|
|
hf_outputs = hf_model.classify(example_prompts)
|
|
|
|
check_embeddings_close(
|
|
embeddings_0_lst=hf_outputs,
|
|
embeddings_1_lst=vllm_outputs,
|
|
name_0="hf",
|
|
name_1="vllm",
|
|
)
|
|
|
|
|
|
VOCAB_SIZE = 64
|
|
HIDDEN_SIZE = 8
|
|
EMBED_SCALE = 3.0
|
|
|
|
|
|
class ScaledWordEmbedding(nn.Embedding):
|
|
"""Mirrors Transformers' `*ScaledWordEmbedding` classes."""
|
|
|
|
def __init__(
|
|
self, num_embeddings, embedding_dim, padding_idx=None, embed_scale=1.0
|
|
):
|
|
super().__init__(num_embeddings, embedding_dim, padding_idx)
|
|
self.scalar_embed_scale = embed_scale
|
|
self.register_buffer("embed_scale", torch.tensor(embed_scale), persistent=False)
|
|
|
|
def forward(self, input_ids):
|
|
return super().forward(input_ids) * self.embed_scale.to(self.weight.dtype)
|
|
|
|
|
|
class ComposedWordEmbedding(nn.Module):
|
|
"""Scales embeddings, but wraps `nn.Embedding` instead of inheriting from it."""
|
|
|
|
def __init__(self, num_embeddings, embedding_dim, embed_scale=1.0):
|
|
super().__init__()
|
|
self.embed = nn.Embedding(num_embeddings, embedding_dim)
|
|
self.embed_scale = embed_scale
|
|
|
|
def forward(self, input_ids):
|
|
return self.embed(input_ids) * self.embed_scale
|
|
|
|
|
|
@pytest.fixture
|
|
def tp_init():
|
|
"""Single rank tensor parallel state, so vLLM layers can be constructed."""
|
|
from vllm.distributed import (
|
|
cleanup_dist_env_and_memory,
|
|
init_distributed_environment,
|
|
initialize_model_parallel,
|
|
)
|
|
from vllm.platforms import current_platform
|
|
|
|
from ...utils import ensure_current_vllm_config
|
|
|
|
fd, temp_file = tempfile.mkstemp()
|
|
os.close(fd)
|
|
try:
|
|
with ensure_current_vllm_config():
|
|
init_distributed_environment(
|
|
world_size=1,
|
|
rank=0,
|
|
distributed_init_method=f"file://{temp_file}",
|
|
local_rank=0,
|
|
backend=current_platform.dist_backend,
|
|
)
|
|
initialize_model_parallel(1, 1)
|
|
yield
|
|
cleanup_dist_env_and_memory()
|
|
finally:
|
|
with contextlib.suppress(OSError):
|
|
os.unlink(temp_file)
|
|
|
|
|
|
@pytest.fixture
|
|
def vpe(tp_init):
|
|
"""`VocabParallelEmbedding`, imported late so collection does not import vLLM."""
|
|
from vllm.model_executor.layers.vocab_parallel_embedding import (
|
|
VocabParallelEmbedding,
|
|
)
|
|
|
|
return VocabParallelEmbedding
|
|
|
|
|
|
def replace(embedding):
|
|
"""Replace `embedding` and fill the new weights with recognisable values."""
|
|
from vllm.model_executor.models.transformers.utils import replace_embedding_class
|
|
|
|
new_embedding = replace_embedding_class(embedding)
|
|
for _, param in new_embedding.named_parameters():
|
|
param.data = torch.arange(param.numel(), dtype=param.dtype).view(param.shape)
|
|
return new_embedding
|
|
|
|
|
|
def assert_scaled(vpe, module, embedding=None):
|
|
"""`module`'s output is `embedding`'s unscaled output times `EMBED_SCALE`."""
|
|
input_ids = torch.arange(VOCAB_SIZE)
|
|
unscaled = vpe.forward(embedding if embedding is not None else module, input_ids)
|
|
torch.testing.assert_close(module(input_ids), unscaled * EMBED_SCALE)
|
|
|
|
|
|
def test_replace_plain_embedding(vpe):
|
|
"""A plain `nn.Embedding` is replaced outright, leaving no subclass behind."""
|
|
assert type(replace(nn.Embedding(VOCAB_SIZE, HIDDEN_SIZE))) is vpe
|
|
|
|
|
|
def test_replace_infers_shape_and_dtype(tp_init):
|
|
"""Shape and dtype come from the replaced module, not from the config."""
|
|
embedding = nn.Embedding(VOCAB_SIZE * 2, HIDDEN_SIZE + 1, dtype=torch.float16)
|
|
new_embedding = replace(embedding)
|
|
|
|
assert new_embedding.num_embeddings == VOCAB_SIZE * 2
|
|
assert new_embedding.org_vocab_size == VOCAB_SIZE * 2
|
|
assert new_embedding.embedding_dim == HIDDEN_SIZE + 1
|
|
assert new_embedding.weight.dtype == torch.float16
|
|
|
|
|
|
def test_replace_inherited_embedding(vpe):
|
|
"""Subclasses keep their extra state and their scaled `forward`."""
|
|
new_embedding = replace(
|
|
ScaledWordEmbedding(
|
|
VOCAB_SIZE, HIDDEN_SIZE, padding_idx=0, embed_scale=EMBED_SCALE
|
|
)
|
|
)
|
|
|
|
assert isinstance(new_embedding, vpe)
|
|
assert new_embedding.scalar_embed_scale == EMBED_SCALE
|
|
assert "embed_scale" in new_embedding._non_persistent_buffers_set
|
|
assert_scaled(vpe, new_embedding)
|
|
|
|
|
|
def test_replace_composed_embedding(vpe):
|
|
"""Wrappers are left alone; only the `nn.Embedding` they hold is replaced."""
|
|
embedding = ComposedWordEmbedding(VOCAB_SIZE, HIDDEN_SIZE, embed_scale=EMBED_SCALE)
|
|
new_embedding = replace(embedding)
|
|
|
|
assert new_embedding is embedding
|
|
assert type(embedding.embed) is vpe
|
|
assert_scaled(vpe, new_embedding, embedding.embed)
|
|
|
|
|
|
def test_replace_nested_embedding(vpe):
|
|
"""The composed `nn.Embedding` is found and set however deeply it is nested."""
|
|
wrapper = nn.Module()
|
|
wrapper.add_module("inner", nn.Module())
|
|
wrapper.inner.add_module(
|
|
"embed", ScaledWordEmbedding(VOCAB_SIZE, HIDDEN_SIZE, embed_scale=EMBED_SCALE)
|
|
)
|
|
replace(wrapper)
|
|
|
|
assert isinstance(wrapper.inner.embed, vpe)
|
|
assert_scaled(vpe, wrapper.inner.embed)
|
|
|
|
|
|
@pytest.mark.parametrize("num_embeddings", [0, 2])
|
|
def test_replace_ambiguous_embedding(tp_init, num_embeddings):
|
|
"""Composing anything but one `nn.Embedding` is an error, not a silent guess."""
|
|
wrapper = nn.Module()
|
|
for i in range(num_embeddings):
|
|
wrapper.add_module(f"embed_{i}", nn.Embedding(VOCAB_SIZE, HIDDEN_SIZE))
|
|
|
|
with pytest.raises(ValueError, match=f"found {num_embeddings}"):
|
|
replace(wrapper)
|
|
|
|
|
|
def test_replaced_embedding_exposes_one_vpe(vpe):
|
|
"""`CausalMixin` ties `lm_head` to the one `VocabParallelEmbedding` it can find.
|
|
|
|
`tie_weights` reads `.weight`, which a composing module does not have, so it must
|
|
be handed the composed embedding instead.
|
|
"""
|
|
from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
|
|
|
|
inherited = replace(ScaledWordEmbedding(VOCAB_SIZE, HIDDEN_SIZE))
|
|
composed = replace(ComposedWordEmbedding(VOCAB_SIZE, HIDDEN_SIZE, EMBED_SCALE))
|
|
|
|
assert [m for m in inherited.modules() if isinstance(m, vpe)] == [inherited]
|
|
assert [m for m in composed.modules() if isinstance(m, vpe)] == [composed.embed]
|
|
|
|
lm_head = ParallelLMHead(VOCAB_SIZE, HIDDEN_SIZE)
|
|
assert lm_head.tie_weights(composed.embed).weight is composed.embed.weight
|