new: add qwen embedding (#678)

This commit is contained in:
George
2026-08-18 19:26:16 +07:00
committed by GitHub
parent a0fe741532
commit a5a702acad
10 changed files with 179 additions and 6 deletions
+2
View File
@@ -15,6 +15,8 @@ These models are developed by Jina (https://jina.ai/) and are subject to Jina AI
This distribution includes the following Google models, each with its respective license: This distribution includes the following Google models, each with its respective license:
- vidore/colpali-v1.3 - vidore/colpali-v1.3
- License: gemma - License: gemma
- google/embeddinggemma-300m
- License: gemma
Gemma is provided under and subject to the Gemma Terms of Use found at ai.google.dev/gemma/terms Gemma is provided under and subject to the Gemma Terms of Use found at ai.google.dev/gemma/terms
+1
View File
@@ -49,4 +49,5 @@ class SparseModelDescription(BaseModelDescription):
class PoolingType(str, Enum): class PoolingType(str, Enum):
CLS = "CLS" CLS = "CLS"
MEAN = "MEAN" MEAN = "MEAN"
LAST_TOKEN = "LAST_TOKEN"
DISABLED = "DISABLED" DISABLED = "DISABLED"
+6
View File
@@ -32,6 +32,12 @@ def mean_pooling(input_array: NumpyArray, attention_mask: NDArray[np.int64]) ->
return pooled_embeddings return pooled_embeddings
def last_token_pooling(input_array: NumpyArray, attention_mask: NDArray[np.int64]) -> NumpyArray:
"""Take the embedding of the last non-padding token of each sequence."""
last_token_indices = np.maximum(attention_mask.sum(axis=1) - 1, 0)
return input_array[np.arange(input_array.shape[0]), last_token_indices]
def iter_batch(iterable: Iterable[T], size: int) -> Iterable[list[T]]: def iter_batch(iterable: Iterable[T], size: int) -> Iterable[list[T]]:
""" """
>>> list(iter_batch([1,2,3,4,5], 3)) >>> list(iter_batch([1,2,3,4,5], 3))
+1 -1
View File
@@ -16,7 +16,7 @@ supported_builtin_sentence_embedding_models: list[DenseModelDescription] = [
"Prefixes for queries/documents: `task: search result | query: {content}` for query, " "Prefixes for queries/documents: `task: search result | query: {content}` for query, "
"`title: {title | 'none'} | text: {content}` for documents, 2025 year." "`title: {title | 'none'} | text: {content}` for documents, 2025 year."
), ),
license="apache-2.0", license="gemma",
size_in_GB=1.24, size_in_GB=1.24,
sources=ModelSource( sources=ModelSource(
hf="onnx-community/embeddinggemma-300m-ONNX", hf="onnx-community/embeddinggemma-300m-ONNX",
+8 -2
View File
@@ -11,7 +11,7 @@ from fastembed.common.model_description import (
) )
from fastembed.common.onnx_model import OnnxOutputContext from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.types import NumpyArray, Device from fastembed.common.types import NumpyArray, Device
from fastembed.common.utils import normalize, mean_pooling from fastembed.common.utils import normalize, mean_pooling, last_token_pooling
from fastembed.text.onnx_embedding import OnnxTextEmbedding from fastembed.text.onnx_embedding import OnnxTextEmbedding
@@ -73,12 +73,18 @@ class CustomTextEmbedding(OnnxTextEmbedding):
raise ValueError("attention_mask must be provided for mean pooling") raise ValueError("attention_mask must be provided for mean pooling")
return mean_pooling(embeddings, attention_mask) return mean_pooling(embeddings, attention_mask)
if self._pooling == PoolingType.LAST_TOKEN:
if attention_mask is None:
raise ValueError("attention_mask must be provided for last token pooling")
return last_token_pooling(embeddings, attention_mask)
if self._pooling == PoolingType.DISABLED: if self._pooling == PoolingType.DISABLED:
return embeddings return embeddings
raise ValueError( raise ValueError(
f"Unsupported pooling type {self._pooling}. " f"Unsupported pooling type {self._pooling}. "
f"Supported types are: {PoolingType.CLS}, {PoolingType.MEAN}, {PoolingType.DISABLED}." f"Supported types are: {PoolingType.CLS}, {PoolingType.MEAN}, "
f"{PoolingType.LAST_TOKEN}, {PoolingType.DISABLED}."
) )
def _normalize(self, embeddings: NumpyArray) -> NumpyArray: def _normalize(self, embeddings: NumpyArray) -> NumpyArray:
@@ -0,0 +1,94 @@
from typing import Any, Iterable, Type
import onnxruntime as ort
from fastembed.common.types import NumpyArray
from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.utils import last_token_pooling, normalize
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
from fastembed.common.model_description import DenseModelDescription, ModelSource
supported_last_token_normalized_models: list[DenseModelDescription] = [
DenseModelDescription(
model="Qwen/Qwen3-Embedding-0.6B",
dim=1024,
description=(
"Text embeddings, Unimodal (text), multilingual, 32768 input tokens truncation, "
"Prefixes for queries/documents: `Instruct: {task_description}\\nQuery:{query}` "
"for queries, none for documents, 2025 year."
),
license="apache-2.0",
size_in_GB=2.38,
sources=ModelSource(hf="Qdrant/Qwen3-Embedding-0.6B-onnx"),
model_file="onnx/model.onnx",
additional_files=["onnx/model.onnx.data"],
),
DenseModelDescription(
model="Qwen/Qwen3-Embedding-0.6B-Q",
dim=1024,
description=(
"Text embeddings, Unimodal (text), multilingual, 32768 input tokens truncation, "
"Prefixes for queries/documents: `Instruct: {task_description}\\nQuery:{query}` "
"for queries, none for documents, int8 weights, requires onnxruntime>=1.23, "
"2025 year."
),
license="apache-2.0",
size_in_GB=1.12,
sources=ModelSource(hf="Qdrant/Qwen3-Embedding-0.6B-onnx"),
model_file="onnx/model_quantized.onnx",
),
]
class LastTokenNormalizedEmbedding(OnnxTextEmbedding):
"""Decoder-based embedding models, which pool the last non-padding token and normalize it"""
@classmethod
def _get_worker_class(cls) -> Type[OnnxTextEmbeddingWorker]:
return LastTokenNormalizedEmbeddingWorker
def load_onnx_model(self) -> None:
try:
super().load_onnx_model()
except Exception as e:
# int8 weights are stored as 8-bit MatMulNBits, which onnxruntime only
# implements since 1.23; older versions fail with "nbits_ == 4 was false"
if "nbits" not in str(e).lower():
raise
raise RuntimeError(
f"Could not load {self.model_name}: its int8 weights require "
f"onnxruntime>=1.23, but onnxruntime {ort.__version__} is installed. "
f"Either upgrade onnxruntime or use a non-quantized model."
) from e
@classmethod
def _list_supported_models(cls) -> list[DenseModelDescription]:
"""Lists the supported models.
Returns:
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
"""
return supported_last_token_normalized_models
def _post_process_onnx_output(
self, output: OnnxOutputContext, **kwargs: Any
) -> Iterable[NumpyArray]:
if output.attention_mask is None:
raise ValueError("attention_mask must be provided for last token pooling")
return normalize(last_token_pooling(output.model_output, output.attention_mask))
class LastTokenNormalizedEmbeddingWorker(OnnxTextEmbeddingWorker):
def init_embedding(
self,
model_name: str,
cache_dir: str,
**kwargs: Any,
) -> OnnxTextEmbedding:
return LastTokenNormalizedEmbedding(
model_name=model_name,
cache_dir=cache_dir,
threads=1,
**kwargs,
)
+2
View File
@@ -9,6 +9,7 @@ from fastembed.text.pooled_normalized_embedding import PooledNormalizedEmbedding
from fastembed.text.pooled_embedding import PooledEmbedding from fastembed.text.pooled_embedding import PooledEmbedding
from fastembed.text.multitask_embedding import JinaEmbeddingV3 from fastembed.text.multitask_embedding import JinaEmbeddingV3
from fastembed.text.builtin_sentence_embedding import BuiltinSentenceEmbedding from fastembed.text.builtin_sentence_embedding import BuiltinSentenceEmbedding
from fastembed.text.last_token_normalized_embedding import LastTokenNormalizedEmbedding
from fastembed.text.onnx_embedding import OnnxTextEmbedding from fastembed.text.onnx_embedding import OnnxTextEmbedding
from fastembed.text.text_embedding_base import TextEmbeddingBase from fastembed.text.text_embedding_base import TextEmbeddingBase
from fastembed.common.model_description import DenseModelDescription, ModelSource, PoolingType from fastembed.common.model_description import DenseModelDescription, ModelSource, PoolingType
@@ -22,6 +23,7 @@ class TextEmbedding(TextEmbeddingBase):
PooledEmbedding, PooledEmbedding,
JinaEmbeddingV3, JinaEmbeddingV3,
BuiltinSentenceEmbedding, BuiltinSentenceEmbedding,
LastTokenNormalizedEmbedding,
CustomTextEmbedding, CustomTextEmbedding,
] ]
+17
View File
@@ -1,3 +1,5 @@
import numpy as np
from fastembed import ( from fastembed import (
TextEmbedding, TextEmbedding,
SparseTextEmbedding, SparseTextEmbedding,
@@ -5,6 +7,7 @@ from fastembed import (
LateInteractionMultimodalEmbedding, LateInteractionMultimodalEmbedding,
LateInteractionTextEmbedding, LateInteractionTextEmbedding,
) )
from fastembed.common.utils import last_token_pooling
def test_text_list_supported_models(): def test_text_list_supported_models():
@@ -28,3 +31,17 @@ def test_text_list_supported_models():
assert "model_file" in description and description["model_file"] assert "model_file" in description and description["model_file"]
assert "sources" in description and description["sources"] assert "sources" in description and description["sources"]
assert "hf" in description["sources"] or "url" in description["sources"] assert "hf" in description["sources"] or "url" in description["sources"]
def test_last_token_pooling():
token_embeddings = np.array(
[
[[1.0, 1.0], [2.0, 2.0], [9.0, 9.0], [9.0, 9.0]], # 2 real tokens, then padding
[[3.0, 3.0], [4.0, 4.0], [5.0, 5.0], [6.0, 6.0]], # no padding
]
)
attention_mask = np.array([[1, 1, 0, 0], [1, 1, 1, 1]], dtype=np.int64)
pooled = last_token_pooling(token_embeddings, attention_mask)
assert np.allclose(pooled, [[2.0, 2.0], [6.0, 6.0]])
+11 -2
View File
@@ -10,7 +10,7 @@ from fastembed.common.model_description import (
BaseModelDescription, BaseModelDescription,
) )
from fastembed.common.onnx_model import OnnxOutputContext from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.utils import normalize, mean_pooling from fastembed.common.utils import normalize, mean_pooling, last_token_pooling
from fastembed.text.custom_text_embedding import CustomTextEmbedding, PostprocessingConfig from fastembed.text.custom_text_embedding import CustomTextEmbedding, PostprocessingConfig
from fastembed.rerank.cross_encoder.custom_text_cross_encoder import CustomTextCrossEncoder from fastembed.rerank.cross_encoder.custom_text_cross_encoder import CustomTextCrossEncoder
from fastembed.rerank.cross_encoder import TextCrossEncoder from fastembed.rerank.cross_encoder import TextCrossEncoder
@@ -136,6 +136,8 @@ def test_mock_add_custom_models():
f"{PoolingType.MEAN.lower()}": dummy_token_output, f"{PoolingType.MEAN.lower()}": dummy_token_output,
f"{PoolingType.CLS.lower()}-normalized": dummy_token_output, f"{PoolingType.CLS.lower()}-normalized": dummy_token_output,
f"{PoolingType.CLS.lower()}": dummy_token_output, f"{PoolingType.CLS.lower()}": dummy_token_output,
f"{PoolingType.LAST_TOKEN.lower()}-normalized": dummy_token_output,
f"{PoolingType.LAST_TOKEN.lower()}": dummy_token_output,
f"{PoolingType.DISABLED.lower()}-normalized": dummy_pooled_output, f"{PoolingType.DISABLED.lower()}-normalized": dummy_pooled_output,
f"{PoolingType.DISABLED.lower()}": dummy_pooled_output, f"{PoolingType.DISABLED.lower()}": dummy_pooled_output,
} }
@@ -147,12 +149,19 @@ def test_mock_add_custom_models():
f"{PoolingType.MEAN.lower()}": mean_pooling(dummy_token_embedding, dummy_attention_mask), f"{PoolingType.MEAN.lower()}": mean_pooling(dummy_token_embedding, dummy_attention_mask),
f"{PoolingType.CLS.lower()}-normalized": normalize(dummy_token_embedding[:, 0]), f"{PoolingType.CLS.lower()}-normalized": normalize(dummy_token_embedding[:, 0]),
f"{PoolingType.CLS.lower()}": dummy_token_embedding[:, 0], f"{PoolingType.CLS.lower()}": dummy_token_embedding[:, 0],
f"{PoolingType.LAST_TOKEN.lower()}-normalized": normalize(
last_token_pooling(dummy_token_embedding, dummy_attention_mask)
),
f"{PoolingType.LAST_TOKEN.lower()}": last_token_pooling(
dummy_token_embedding, dummy_attention_mask
),
f"{PoolingType.DISABLED.lower()}-normalized": normalize(dummy_pooled_embedding), f"{PoolingType.DISABLED.lower()}-normalized": normalize(dummy_pooled_embedding),
f"{PoolingType.DISABLED.lower()}": dummy_pooled_embedding, f"{PoolingType.DISABLED.lower()}": dummy_pooled_embedding,
} }
for pooling, normalization in itertools.product( for pooling, normalization in itertools.product(
(PoolingType.MEAN, PoolingType.CLS, PoolingType.DISABLED), (True, False) (PoolingType.MEAN, PoolingType.CLS, PoolingType.LAST_TOKEN, PoolingType.DISABLED),
(True, False),
): ):
model_name = f"{pooling.name.lower()}{'-normalized' if normalization else ''}" model_name = f"{pooling.name.lower()}{'-normalized' if normalization else ''}"
TextEmbedding.add_custom_model( TextEmbedding.add_custom_model(
+37 -1
View File
@@ -5,6 +5,8 @@ from contextlib import contextmanager
import numpy as np import numpy as np
import pytest import pytest
from fastembed.text.last_token_normalized_embedding import LastTokenNormalizedEmbedding
from fastembed.text.onnx_embedding import OnnxTextEmbedding
from fastembed.text.text_embedding import TextEmbedding from fastembed.text.text_embedding import TextEmbedding
from tests.utils import delete_model_cache, should_test_model from tests.utils import delete_model_cache, should_test_model
@@ -71,19 +73,36 @@ CANONICAL_VECTOR_VALUES = {
"google/embeddinggemma-300m": np.array( "google/embeddinggemma-300m": np.array(
[-0.08181356, 0.0214127, 0.05120273, -0.03690156, -0.0254504] [-0.08181356, 0.0214127, 0.05120273, -0.03690156, -0.0254504]
), ),
"Qwen/Qwen3-Embedding-0.6B": np.array(
[-0.01476084, 0.01723184, -0.01195498, -0.07275258, 0.00281229]
),
"Qwen/Qwen3-Embedding-0.6B-Q": np.array(
[-0.01599521, 0.01676456, -0.01195119, -0.07132675, 0.00346729]
),
} }
QWEN3_INSTRUCT_PREFIX = (
"Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery:"
)
DOC_PREFIXES = { DOC_PREFIXES = {
"google/embeddinggemma-300m": "title: none | text: ", "google/embeddinggemma-300m": "title: none | text: ",
} }
QUERY_PREFIXES = { QUERY_PREFIXES = {
"google/embeddinggemma-300m": "task: search result | query: ", "google/embeddinggemma-300m": "task: search result | query: ",
"Qwen/Qwen3-Embedding-0.6B": QWEN3_INSTRUCT_PREFIX,
"Qwen/Qwen3-Embedding-0.6B-Q": QWEN3_INSTRUCT_PREFIX,
} }
CANONICAL_QUERY_VECTOR_VALUES = { CANONICAL_QUERY_VECTOR_VALUES = {
"google/embeddinggemma-300m": np.array( "google/embeddinggemma-300m": np.array(
[-0.22990295, 0.03311195, 0.04290345, -0.03558498, -0.01399477] [-0.22990295, 0.03311195, 0.04290345, -0.03558498, -0.01399477]
) ),
"Qwen/Qwen3-Embedding-0.6B": np.array(
[-0.01908712, 0.01635596, -0.00356586, -0.03947155, -0.01387356]
),
"Qwen/Qwen3-Embedding-0.6B-Q": np.array(
[-0.02221339, 0.01932909, -0.00361797, -0.03888897, -0.01362813]
),
} }
MULTI_TASK_MODELS = ["jinaai/jina-embeddings-v3"] MULTI_TASK_MODELS = ["jinaai/jina-embeddings-v3"]
@@ -181,6 +200,23 @@ def test_query_embedding(model_cache) -> None:
), model_desc.model ), model_desc.model
def test_quantized_model_reports_onnxruntime_requirement(monkeypatch) -> None:
"""Old onnxruntime only implements 4-bit MatMulNBits, the error should say so."""
monkeypatch.setattr(
OnnxTextEmbedding,
"load_onnx_model",
lambda self: (_ for _ in ()).throw(RuntimeError("nbits_ == 4 was false")),
)
model = LastTokenNormalizedEmbedding(
"Qwen/Qwen3-Embedding-0.6B-Q",
lazy_load=True,
specific_model_path="./", # disable model downloading and loading
)
with pytest.raises(RuntimeError, match="onnxruntime>=1.23"):
model.load_onnx_model()
@pytest.mark.parametrize("n_dims,model_name", [(384, "BAAI/bge-small-en-v1.5")]) @pytest.mark.parametrize("n_dims,model_name", [(384, "BAAI/bge-small-en-v1.5")])
def test_batch_embedding(model_cache, n_dims: int, model_name: str) -> None: def test_batch_embedding(model_cache, n_dims: int, model_name: str) -> None:
with model_cache(model_name) as model: with model_cache(model_name) as model: