[Bugfix][Model] Reject encoder-backbone jina-embeddings-v5 checkpoints with a clear error (fixes #50337) (#50352)

Signed-off-by: seewoo <seewoo@ucsc.edu> Co-authored-by: Claude <noreply@anthropic.com>

Sebastian Woo committed Jul 30, 2026 at 20:43 UTC bebf918044ae05f0121c05f712417e53b287e1c9
2 files changed +96
tests/models/language/pooling/test_jina_embeddings_v5.py new
+64
@@ -0,0 +1,64 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Backbone validation for Jina Embeddings V5 (issue #50337).
4 +
5 +The V5 family ships two backbones under one `architectures` entry: `-small` is
6 +a Qwen3 decoder (which vLLM implements), while `-nano` is a bidirectional
7 +EuroBERT encoder (which it does not). Upstream ships a separate
8 +`configuration_*.py` per repository, so the only signal distinguishing them is
9 +`is_decoder`, which the encoder variant sets to False.
10 +"""
11 +
12 +from types import SimpleNamespace
13 +from typing import cast
14 +
15 +import pytest
16 +from transformers import PretrainedConfig
17 +
18 +from vllm.config import ModelConfig
19 +from vllm.model_executor.models.config import (
20 + MODELS_CONFIG_MAP,
21 + JinaEmbeddingsV5ModelConfig,
22 +)
23 +
24 +
25 +def _model_config(hf_config: PretrainedConfig) -> ModelConfig:
26 + """Minimal stand-in for ModelConfig; only hf_config is read."""
27 + return cast(ModelConfig, SimpleNamespace(hf_config=hf_config))
28 +
29 +
30 +@pytest.mark.cpu_test
31 +def test_registered_for_the_architecture():
32 + """The handler only runs if it is wired to the architecture name."""
33 + assert MODELS_CONFIG_MAP["JinaEmbeddingsV5Model"] is JinaEmbeddingsV5ModelConfig
34 +
35 +
36 +@pytest.mark.cpu_test
37 +def test_encoder_backbone_is_rejected():
38 + """An encoder checkpoint must fail fast, naming the reason.
39 +
40 + Without this, loading proceeds until every `q_norm`/`k_norm` weight is
41 + reported as uninitialized, which does not point at the actual problem.
42 + """
43 + model_config = _model_config(PretrainedConfig(is_decoder=False))
44 +
45 + with pytest.raises(NotImplementedError, match="is_decoder=False"):
46 + JinaEmbeddingsV5ModelConfig.verify_and_update_model_config(model_config)
47 +
48 +
49 +@pytest.mark.cpu_test
50 +def test_supported_decoder_backbone_is_accepted():
51 + """The Qwen3-based variants must keep loading.
52 +
53 + `-small` omits `is_decoder` entirely, so an absent attribute has to be
54 + treated as a decoder. The first assertion pins that assumption: if
55 + PretrainedConfig ever gains an `is_decoder=False` default, this fails here
56 + rather than silently rejecting a supported checkpoint.
57 + """
58 + absent = PretrainedConfig()
59 + assert not hasattr(absent, "is_decoder")
60 +
61 + JinaEmbeddingsV5ModelConfig.verify_and_update_model_config(_model_config(absent))
62 + JinaEmbeddingsV5ModelConfig.verify_and_update_model_config(
63 + _model_config(PretrainedConfig(is_decoder=True))
64 + )
vllm/model_executor/models/config.py
+32
@@ -474,6 +474,37 @@ class JambaForSequenceClassificationConfig(VerifyAndUpdateConfig):
474 pooler_config.use_activation = False
475
476
477 +class JinaEmbeddingsV5ModelConfig(VerifyAndUpdateConfig):
478 + """Config handler for Jina Embeddings V5 embedding models."""
479 +
480 + @staticmethod
481 + def verify_and_update_model_config(model_config: "ModelConfig") -> None:
482 + """Reject checkpoints whose backbone is not the Qwen3 decoder.
483 +
484 + The V5 family ships more than one backbone under a single
485 + `architectures` entry: the `-small` variants are Qwen3 decoders, while
486 + `-nano` is a bidirectional EuroBERT encoder. Upstream ships a separate
487 + `configuration_*.py` per repository, so the config carries no backbone
488 + field and the encoder variants are only identifiable by
489 + `is_decoder=False`. `JinaEmbeddingsV5Model` extends `Qwen3ForCausalLM`
490 + and has no path to an encoder backbone, so without this check such a
491 + checkpoint fails later with every `q_norm` and `k_norm` weight reported
492 + as uninitialized, which does not point at the actual problem.
493 +
494 + Raises:
495 + NotImplementedError: If the checkpoint uses an encoder backbone.
496 + """
497 + if getattr(model_config.hf_config, "is_decoder", True):
498 + return
499 +
500 + raise NotImplementedError(
501 + "This jina-embeddings-v5 checkpoint uses a bidirectional encoder "
502 + "backbone (is_decoder=False). JinaEmbeddingsV5Model is built on "
503 + "Qwen3 and cannot serve it, so only the Qwen3-based decoder "
504 + "variants are supported, such as jina-embeddings-v5-text-small."
505 + )
506 +
507 +
508 class JinaForRankingConfig(VerifyAndUpdateConfig):
509 @staticmethod
510 def verify_and_update_model_config(model_config: "ModelConfig") -> None:
@@ -915,6 +946,7 @@ MODELS_CONFIG_MAP: dict[str, type[VerifyAndUpdateConfig]] = {
946 "GteNewForSequenceClassification": GteNewModelConfig,
947 "GteNewModel": GteNewModelConfig,
948 "JambaForSequenceClassification": JambaForSequenceClassificationConfig,
949 + "JinaEmbeddingsV5Model": JinaEmbeddingsV5ModelConfig,
950 "JinaForRanking": JinaForRankingConfig,
951 "JinaVLForRanking": JinaVLForSequenceClassificationConfig,
952 "KimiK3ForConditionalGeneration": KimiK3ForConditionalGenerationConfig,