[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,