mirror of
https://github.com/qdrant/fastembed.git
synced 2026-09-22 22:17:49 -05:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e21de21d6 | ||
|
|
4bc5fbedf9 | ||
|
|
3d90072e8a | ||
|
|
5ea3bbcc84 | ||
|
|
1c016a2a3f | ||
|
|
4037e14f3b | ||
|
|
758d33984d | ||
|
|
5c46b17a24 | ||
|
|
f63333d620 | ||
|
|
13b7d6d7ef | ||
|
|
c212c1fe41 | ||
|
|
b82e4d05f9 | ||
|
|
a761dcf657 | ||
|
|
b8d30b1cb6 | ||
|
|
a18f735983 | ||
|
|
4c5001595c | ||
|
|
0499ea5a39 | ||
|
|
238e8cab8d | ||
|
|
ad62c7d85e | ||
|
|
ee5fa077cb | ||
|
|
9b1af0ce76 | ||
|
|
b3461c0bb3 | ||
|
|
9591396245 | ||
|
|
ac641d035f |
@@ -1,10 +1,10 @@
|
||||
name: Tests
|
||||
run-name: Tests (gpu)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master, main, gpu ]
|
||||
workflow_dispatch:
|
||||
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -22,8 +22,6 @@ jobs:
|
||||
- '3.13.x'
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -43,4 +41,4 @@ jobs:
|
||||
|
||||
- name: Run pytest
|
||||
run: |
|
||||
poetry run pytest
|
||||
poetry run pytest
|
||||
|
||||
@@ -190,23 +190,6 @@ scores = list(encoder.rerank(query, documents))
|
||||
# [-11.48061752319336, 5.472434997558594]
|
||||
```
|
||||
|
||||
Text cross encoders can also be extended with models which are not in the list of supported models.
|
||||
|
||||
```python
|
||||
from fastembed.rerank.cross_encoder import TextCrossEncoder
|
||||
from fastembed.common.model_description import ModelSource
|
||||
|
||||
TextCrossEncoder.add_custom_model(
|
||||
model="Xenova/ms-marco-MiniLM-L-4-v2",
|
||||
model_file="onnx/model.onnx",
|
||||
sources=ModelSource(hf="Xenova/ms-marco-MiniLM-L-4-v2"),
|
||||
)
|
||||
model = TextCrossEncoder(model_name="Xenova/ms-marco-MiniLM-L-4-v2")
|
||||
scores = list(model.rerank_pairs(
|
||||
[("What is AI?", "Artificial intelligence is ..."), ("What is ML?", "Machine learning is ..."),]
|
||||
))
|
||||
```
|
||||
|
||||
## ⚡️ FastEmbed on a GPU
|
||||
|
||||
FastEmbed supports running on GPU devices.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,11 +7,6 @@ from typing import Optional, Any
|
||||
class ModelSource:
|
||||
hf: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
_deprecated_tar_struct: bool = False
|
||||
|
||||
@property
|
||||
def deprecated_tar_struct(self) -> bool:
|
||||
return self._deprecated_tar_struct
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.hf is None and self.url is None:
|
||||
|
||||
@@ -330,10 +330,9 @@ class ModelManagement(Generic[T]):
|
||||
model_name: str,
|
||||
source_url: str,
|
||||
cache_dir: str,
|
||||
deprecated_tar_struct: bool = False,
|
||||
local_files_only: bool = False,
|
||||
) -> Path:
|
||||
fast_model_name = f"{'fast-' if deprecated_tar_struct else ''}{model_name.split('/')[-1]}"
|
||||
fast_model_name = f"fast-{model_name.split('/')[-1]}"
|
||||
cache_tmp_dir = Path(cache_dir) / "tmp"
|
||||
model_tmp_dir = cache_tmp_dir / fast_model_name
|
||||
model_dir = Path(cache_dir) / fast_model_name
|
||||
@@ -439,7 +438,6 @@ class ModelManagement(Generic[T]):
|
||||
model.model,
|
||||
str(url_source),
|
||||
str(cache_dir),
|
||||
deprecated_tar_struct=model.sources.deprecated_tar_struct,
|
||||
local_files_only=local_files_only,
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -68,7 +68,15 @@ class OnnxModel(Generic[T]):
|
||||
if device_id is None:
|
||||
onnx_providers = ["CUDAExecutionProvider"]
|
||||
else:
|
||||
onnx_providers = [("CUDAExecutionProvider", {"device_id": device_id})]
|
||||
# kSameAsRequested: Allocates only the requested memory, avoiding over-allocation.
|
||||
# more precise than 'kNextPowerOfTwo', which grows memory aggressively.
|
||||
# source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage:
|
||||
onnx_providers = [
|
||||
(
|
||||
"CUDAExecutionProvider",
|
||||
{"device_id": device_id, "arena_extend_strategy": "kSameAsRequested"},
|
||||
)
|
||||
]
|
||||
else:
|
||||
onnx_providers = ["CPUExecutionProvider"]
|
||||
|
||||
@@ -132,5 +140,7 @@ class EmbeddingWorker(Worker, Generic[T]):
|
||||
def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "EmbeddingWorker[T]":
|
||||
return cls(model_name=model_name, cache_dir=cache_dir, **kwargs)
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]], **kwargs: Any
|
||||
) -> Iterable[tuple[int, Any]]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
@@ -5,12 +5,12 @@ import tempfile
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from itertools import islice
|
||||
from typing import Iterable, Optional, TypeVar
|
||||
from typing import Iterable, Optional, TypeVar, Sequence
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.types import NumpyArray, OnnxProvider
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -67,3 +67,18 @@ def get_all_punctuation() -> set[str]:
|
||||
|
||||
def remove_non_alphanumeric(text: str) -> str:
|
||||
return re.sub(r"[^\w\s]", " ", text, flags=re.UNICODE)
|
||||
|
||||
|
||||
def is_cuda_enabled(cuda: bool, providers: Optional[Sequence[OnnxProvider]]) -> bool:
|
||||
"""
|
||||
Check if CUDA is enabled based on the `cuda` and `providers` parameters
|
||||
"""
|
||||
if cuda:
|
||||
return True
|
||||
if not providers:
|
||||
return False
|
||||
if isinstance(providers, str):
|
||||
return "CUDAExecutionProvider" in providers
|
||||
return isinstance(providers, (list, tuple)) and any(
|
||||
isinstance(p, str) and "CUDAExecutionProvider" in p for p in providers
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common import ImageInput, OnnxProvider
|
||||
@@ -194,7 +195,7 @@ class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[NumpyArray]):
|
||||
return onnx_input
|
||||
|
||||
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[NumpyArray]:
|
||||
return normalize(output.model_output)
|
||||
return normalize(output.model_output).astype(np.float32)
|
||||
|
||||
|
||||
class OnnxImageEmbeddingWorker(ImageEmbeddingWorker[NumpyArray]):
|
||||
|
||||
@@ -6,13 +6,14 @@ from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import onnxruntime as ort
|
||||
|
||||
from fastembed.image.transform.operators import Compose
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common import ImageInput, OnnxProvider
|
||||
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T
|
||||
from fastembed.common.preprocessor_utils import load_preprocessor
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.common.utils import iter_batch, is_cuda_enabled
|
||||
from fastembed.parallel_processor import ParallelWorkerPool
|
||||
|
||||
# Holds type of the embedding result
|
||||
@@ -74,7 +75,21 @@ class OnnxImageModel(OnnxModel[T]):
|
||||
encoded = np.array(self.processor(image_files))
|
||||
onnx_input = self._build_onnx_input(encoded)
|
||||
onnx_input = self._preprocess_onnx_input(onnx_input)
|
||||
model_output = self.model.run(None, onnx_input) # type: ignore[union-attr]
|
||||
|
||||
run_options = ort.RunOptions()
|
||||
providers = kwargs.get("providers", None)
|
||||
cuda = kwargs.get("cuda", False)
|
||||
if is_cuda_enabled(cuda, providers):
|
||||
device_id = kwargs.get("device_id", None)
|
||||
device_id = str(device_id if isinstance(device_id, int) else 0)
|
||||
# enables memory arena shrinkage, freeing unused memory after each Run() cycle.
|
||||
# helps prevent excessive memory retention, especially for dynamic workloads.
|
||||
# source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage:
|
||||
run_options.add_run_config_entry(
|
||||
"memory.enable_memory_arena_shrinkage", f"gpu:{device_id}"
|
||||
)
|
||||
|
||||
model_output = self.model.run(None, onnx_input, run_options) # type: ignore[union-attr]
|
||||
embeddings = model_output[0].reshape(len(images), -1)
|
||||
return OnnxOutputContext(model_output=embeddings)
|
||||
|
||||
@@ -104,7 +119,9 @@ class OnnxImageModel(OnnxModel[T]):
|
||||
self.load_onnx_model()
|
||||
|
||||
for batch in iter_batch(images, batch_size):
|
||||
yield from self._post_process_onnx_output(self.onnx_embed(batch))
|
||||
yield from self._post_process_onnx_output(
|
||||
self.onnx_embed(batch, cuda=cuda, providers=providers)
|
||||
)
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
@@ -129,7 +146,9 @@ class OnnxImageModel(OnnxModel[T]):
|
||||
|
||||
|
||||
class ImageEmbeddingWorker(EmbeddingWorker[T]):
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]], **kwargs: Any
|
||||
) -> Iterable[tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
embeddings = self.model.onnx_embed(batch)
|
||||
embeddings = self.model.onnx_embed(batch, **kwargs)
|
||||
yield idx, embeddings
|
||||
|
||||
@@ -46,7 +46,7 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[NumpyArray]):
|
||||
self, output: OnnxOutputContext, is_doc: bool = True
|
||||
) -> Iterable[NumpyArray]:
|
||||
if not is_doc:
|
||||
return output.model_output
|
||||
return output.model_output.astype(np.float32)
|
||||
|
||||
if output.input_ids is None or output.attention_mask is None:
|
||||
raise ValueError(
|
||||
@@ -58,11 +58,11 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[NumpyArray]):
|
||||
if token_id in self.skip_list or token_id == self.pad_token_id:
|
||||
output.attention_mask[i, j] = 0
|
||||
|
||||
output.model_output *= np.expand_dims(output.attention_mask, 2)
|
||||
output.model_output *= np.expand_dims(output.attention_mask, 2).astype(np.float32)
|
||||
norm = np.linalg.norm(output.model_output, ord=2, axis=2, keepdims=True)
|
||||
norm_clamped = np.maximum(norm, 1e-12)
|
||||
output.model_output /= norm_clamped
|
||||
return output.model_output
|
||||
return output.model_output.astype(np.float32)
|
||||
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], is_doc: bool = True, **kwargs: Any
|
||||
|
||||
@@ -142,7 +142,7 @@ class ColPali(LateInteractionMultimodalEmbeddingBase, OnnxMultimodalModel[NumpyA
|
||||
assert self.model_description.dim is not None, "Model dim is not defined"
|
||||
return output.model_output.reshape(
|
||||
output.model_output.shape[0], -1, self.model_description.dim
|
||||
)
|
||||
).astype(np.float32)
|
||||
|
||||
def _post_process_onnx_text_output(
|
||||
self,
|
||||
@@ -157,7 +157,7 @@ class ColPali(LateInteractionMultimodalEmbeddingBase, OnnxMultimodalModel[NumpyA
|
||||
Returns:
|
||||
Iterable[NumpyArray]: Post-processed output as NumPy arrays.
|
||||
"""
|
||||
return output.model_output
|
||||
return output.model_output.astype(np.float32)
|
||||
|
||||
def tokenize(self, documents: list[str], **kwargs: Any) -> list[Encoding]:
|
||||
texts_query: list[str] = []
|
||||
|
||||
@@ -6,13 +6,14 @@ from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import onnxruntime as ort
|
||||
from tokenizers import Encoding, Tokenizer
|
||||
|
||||
from fastembed.common import OnnxProvider, ImageInput
|
||||
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T
|
||||
from fastembed.common.preprocessor_utils import load_tokenizer, load_preprocessor
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.common.utils import iter_batch, is_cuda_enabled
|
||||
from fastembed.image.transform.operators import Compose
|
||||
from fastembed.parallel_processor import ParallelWorkerPool
|
||||
|
||||
@@ -103,7 +104,21 @@ class OnnxMultimodalModel(OnnxModel[T]):
|
||||
)
|
||||
|
||||
onnx_input = self._preprocess_onnx_text_input(onnx_input, **kwargs)
|
||||
model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr]
|
||||
|
||||
run_options = ort.RunOptions()
|
||||
providers = kwargs.get("providers", None)
|
||||
cuda = kwargs.get("cuda", False)
|
||||
if is_cuda_enabled(cuda, providers):
|
||||
device_id = kwargs.get("device_id", None)
|
||||
device_id = str(device_id if isinstance(device_id, int) else 0)
|
||||
# enables memory arena shrinkage, freeing unused memory after each Run() cycle.
|
||||
# helps prevent excessive memory retention, especially for dynamic workloads.
|
||||
# source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage:
|
||||
run_options.add_run_config_entry(
|
||||
"memory.enable_memory_arena_shrinkage", f"gpu:{device_id}"
|
||||
)
|
||||
|
||||
model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input, run_options) # type: ignore[union-attr]
|
||||
return OnnxOutputContext(
|
||||
model_output=model_output[0],
|
||||
attention_mask=onnx_input.get("attention_mask", attention_mask),
|
||||
@@ -136,7 +151,9 @@ class OnnxMultimodalModel(OnnxModel[T]):
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
for batch in iter_batch(documents, batch_size):
|
||||
yield from self._post_process_onnx_text_output(self.onnx_embed_text(batch))
|
||||
yield from self._post_process_onnx_text_output(
|
||||
self.onnx_embed_text(batch, cuda=cuda, providers=providers)
|
||||
)
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
@@ -169,7 +186,21 @@ class OnnxMultimodalModel(OnnxModel[T]):
|
||||
encoded = np.array(self.processor(image_files))
|
||||
onnx_input = {"pixel_values": encoded}
|
||||
onnx_input = self._preprocess_onnx_image_input(onnx_input, **kwargs)
|
||||
model_output = self.model.run(None, onnx_input) # type: ignore[union-attr]
|
||||
|
||||
run_options = ort.RunOptions()
|
||||
providers = kwargs.get("providers", None)
|
||||
cuda = kwargs.get("cuda", False)
|
||||
if is_cuda_enabled(cuda, providers):
|
||||
device_id = kwargs.get("device_id", None)
|
||||
device_id = str(device_id if isinstance(device_id, int) else 0)
|
||||
# enables memory arena shrinkage, freeing unused memory after each Run() cycle.
|
||||
# helps prevent excessive memory retention, especially for dynamic workloads.
|
||||
# source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage:
|
||||
run_options.add_run_config_entry(
|
||||
"memory.enable_memory_arena_shrinkage", f"gpu:{device_id}"
|
||||
)
|
||||
|
||||
model_output = self.model.run(None, onnx_input, run_options) # type: ignore[union-attr]
|
||||
embeddings = model_output[0].reshape(len(images), -1)
|
||||
return OnnxOutputContext(model_output=embeddings)
|
||||
|
||||
@@ -199,7 +230,9 @@ class OnnxMultimodalModel(OnnxModel[T]):
|
||||
self.load_onnx_model()
|
||||
|
||||
for batch in iter_batch(images, batch_size):
|
||||
yield from self._post_process_onnx_image_output(self.onnx_embed_image(batch))
|
||||
yield from self._post_process_onnx_image_output(
|
||||
self.onnx_embed_image(batch, cuda=cuda, providers=providers)
|
||||
)
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
@@ -241,9 +274,11 @@ class TextEmbeddingWorker(EmbeddingWorker[T]):
|
||||
) -> OnnxMultimodalModel:
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]], **kwargs: Any
|
||||
) -> Iterable[tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.onnx_embed_text(batch)
|
||||
onnx_output = self.model.onnx_embed_text(batch, **kwargs)
|
||||
yield idx, onnx_output
|
||||
|
||||
|
||||
@@ -265,7 +300,9 @@ class ImageEmbeddingWorker(EmbeddingWorker[T]):
|
||||
) -> OnnxMultimodalModel:
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]], **kwargs: Any
|
||||
) -> Iterable[tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
embeddings = self.model.onnx_embed_image(batch)
|
||||
embeddings = self.model.onnx_embed_image(batch, **kwargs)
|
||||
yield idx, embeddings
|
||||
|
||||
@@ -3,8 +3,6 @@ import os
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from multiprocessing import shared_memory, Manager, Lock
|
||||
from multiprocessing import Queue, get_context
|
||||
from multiprocessing.context import BaseContext
|
||||
from multiprocessing.process import BaseProcess
|
||||
@@ -12,11 +10,6 @@ from multiprocessing.sharedctypes import Synchronized as BaseValue
|
||||
from queue import Empty
|
||||
from typing import Any, Iterable, Optional, Type
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
|
||||
|
||||
# Single item should be processed in less than:
|
||||
processing_timeout = 10 * 60 # seconds
|
||||
@@ -24,13 +17,6 @@ processing_timeout = 10 * 60 # seconds
|
||||
max_internal_batch_size = 200
|
||||
|
||||
|
||||
@dataclass
|
||||
class OnnxOutputContext:
|
||||
model_output: NumpyArray
|
||||
attention_mask: Optional[NDArray[np.int64]] = None
|
||||
input_ids: Optional[NDArray[np.int64]] = None
|
||||
|
||||
|
||||
class QueueSignals(str, Enum):
|
||||
stop = "stop"
|
||||
confirm = "confirm"
|
||||
@@ -42,58 +28,18 @@ class Worker:
|
||||
def start(cls, *args: Any, **kwargs: Any) -> "Worker":
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]], **kwargs: Any
|
||||
) -> Iterable[tuple[int, Any]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SharedMemoryPool:
|
||||
def __init__(self, lock: Lock):
|
||||
self._lock = lock
|
||||
self._pool: dict[str, tuple[shared_memory.SharedMemory, int, np.dtype]] = {}
|
||||
self._free_buffers: list[str] = []
|
||||
|
||||
def allocate(self, size: int, dtype: np.dtype) -> tuple[shared_memory.SharedMemory, str]:
|
||||
best_match = None
|
||||
best_size = float("inf")
|
||||
for buf_name in self._free_buffers:
|
||||
shm, buf_size, buf_dtype = self._pool[buf_name]
|
||||
# get best match for needed size
|
||||
if buf_size >= size and buf_dtype == dtype and buf_size < best_size:
|
||||
best_match = buf_name
|
||||
best_size = buf_size
|
||||
if best_match:
|
||||
self._free_buffers.remove(best_match)
|
||||
return self._pool[best_match][0], best_match
|
||||
shm = shared_memory.SharedMemory(create=True, size=size)
|
||||
self._pool[shm.name] = (shm, size, dtype)
|
||||
return shm, shm.name
|
||||
# if no match found, create new buffer
|
||||
shm = shared_memory.SharedMemory(create=True, size=size)
|
||||
self._pool[shm.name] = (shm, size, dtype)
|
||||
return shm, shm.name
|
||||
|
||||
def release(self, name: str) -> None:
|
||||
if name in self._pool and name not in self._free_buffers:
|
||||
self._free_buffers.append(name)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
for shm, _, _ in self._pool.values():
|
||||
shm.close()
|
||||
shm.unlink()
|
||||
self._pool.clear()
|
||||
self._free_buffers.clear()
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
|
||||
def _worker(
|
||||
worker_class: Type[Worker],
|
||||
input_queue: Queue,
|
||||
output_queue: Queue,
|
||||
num_active_workers: BaseValue,
|
||||
worker_id: int,
|
||||
shared_pool: SharedMemoryPool,
|
||||
kwargs: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -111,6 +57,7 @@ def _worker(
|
||||
try:
|
||||
worker = worker_class.start(**kwargs)
|
||||
|
||||
# Keep going until you get an item that's None.
|
||||
def input_queue_iterable() -> Iterable[Any]:
|
||||
while True:
|
||||
item = input_queue.get()
|
||||
@@ -118,25 +65,8 @@ def _worker(
|
||||
break
|
||||
yield item
|
||||
|
||||
for processed_item in worker.process(input_queue_iterable()):
|
||||
idx, output_context = processed_item
|
||||
output_metadata = {}
|
||||
for field in ["model_output", "attention_mask", "input_ids"]:
|
||||
array = getattr(output_context, field, None)
|
||||
if array is not None:
|
||||
shm, shm_name = shared_pool.allocate(array.nbytes, array.dtype)
|
||||
shm_array = np.ndarray(array.shape, dtype=array.dtype, buffer=shm.buf)
|
||||
np.copyto(shm_array, array)
|
||||
output_metadata[field] = {
|
||||
"name": shm_name,
|
||||
"shape": array.shape,
|
||||
"dtype": array.dtype.str,
|
||||
}
|
||||
shm.close()
|
||||
output_queue.put((idx, output_metadata))
|
||||
for field in output_metadata: # mark release to reuse
|
||||
shared_pool.release(output_metadata[field]["name"])
|
||||
|
||||
for processed_item in worker.process(input_queue_iterable(), **kwargs):
|
||||
output_queue.put(processed_item)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logging.exception(e)
|
||||
output_queue.put(QueueSignals.error)
|
||||
@@ -180,8 +110,6 @@ class ParallelWorkerPool:
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
self.num_active_workers: Optional[BaseValue] = None
|
||||
self.manager = Manager()
|
||||
self.shared_pool = SharedMemoryPool(self.manager.Lock())
|
||||
|
||||
def start(self, **kwargs: Any) -> None:
|
||||
self.input_queue = self.ctx.Queue(self.queue_size)
|
||||
@@ -207,7 +135,6 @@ class ParallelWorkerPool:
|
||||
self.output_queue,
|
||||
self.num_active_workers,
|
||||
worker_id,
|
||||
self.shared_pool,
|
||||
worker_kwargs,
|
||||
),
|
||||
)
|
||||
@@ -253,17 +180,7 @@ class ParallelWorkerPool:
|
||||
if out_item == QueueSignals.error:
|
||||
self.join_or_terminate()
|
||||
raise RuntimeError("Thread unexpectedly terminated")
|
||||
|
||||
idx, output_metadata = out_item
|
||||
output_arrays = {}
|
||||
for field, meta in output_metadata.items():
|
||||
shm = shared_memory.SharedMemory(name=meta["name"])
|
||||
array = np.ndarray(
|
||||
meta["shape"], dtype=meta["dtype"], buffer=shm.buf
|
||||
).copy()
|
||||
output_arrays[field] = array
|
||||
shm.close()
|
||||
yield (idx, OnnxOutputContext(**output_arrays))
|
||||
yield out_item
|
||||
read += 1
|
||||
|
||||
self.input_queue.put((idx, item))
|
||||
@@ -278,18 +195,9 @@ class ParallelWorkerPool:
|
||||
if out_item == QueueSignals.error:
|
||||
self.join_or_terminate()
|
||||
raise RuntimeError("Thread unexpectedly terminated")
|
||||
|
||||
idx, output_metadata = out_item
|
||||
output_arrays = {}
|
||||
for field, meta in output_metadata.items():
|
||||
shm = shared_memory.SharedMemory(name=meta["name"])
|
||||
array = np.ndarray(meta["shape"], dtype=meta["dtype"], buffer=shm.buf).copy()
|
||||
output_arrays[field] = array
|
||||
shm.close()
|
||||
yield (idx, OnnxOutputContext(**output_arrays))
|
||||
yield out_item
|
||||
read += 1
|
||||
finally:
|
||||
self.shared_pool.cleanup()
|
||||
assert self.input_queue is not None, "Input queue is None"
|
||||
assert self.output_queue is not None, "Output queue is None"
|
||||
self.join()
|
||||
@@ -325,13 +233,11 @@ class ParallelWorkerPool:
|
||||
if process.is_alive():
|
||||
process.terminate()
|
||||
self.processes.clear()
|
||||
self.shared_pool.cleanup()
|
||||
|
||||
def join(self) -> None:
|
||||
for process in self.processes:
|
||||
process.join()
|
||||
self.processes.clear()
|
||||
self.shared_pool.cleanup()
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""
|
||||
@@ -346,4 +252,3 @@ class ParallelWorkerPool:
|
||||
for process in self.processes:
|
||||
if process.is_alive():
|
||||
process.terminate()
|
||||
self.shared_pool.cleanup()
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
from typing import Optional, Sequence, Any
|
||||
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.common.model_description import BaseModelDescription
|
||||
from fastembed.rerank.cross_encoder.onnx_text_cross_encoder import OnnxTextCrossEncoder
|
||||
|
||||
|
||||
class CustomTextCrossEncoder(OnnxTextCrossEncoder):
|
||||
SUPPORTED_MODELS: list[BaseModelDescription] = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
device_id=device_id,
|
||||
specific_model_path=specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[BaseModelDescription]:
|
||||
return cls.SUPPORTED_MODELS
|
||||
|
||||
@classmethod
|
||||
def add_model(
|
||||
cls,
|
||||
model_description: BaseModelDescription,
|
||||
) -> None:
|
||||
cls.SUPPORTED_MODELS.append(model_description)
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Sequence, Type
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
from tokenizers import Encoding
|
||||
|
||||
from fastembed.common.onnx_model import (
|
||||
@@ -14,7 +15,7 @@ from fastembed.common.onnx_model import (
|
||||
)
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.preprocessor_utils import load_tokenizer
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.common.utils import iter_batch, is_cuda_enabled
|
||||
from fastembed.parallel_processor import ParallelWorkerPool
|
||||
|
||||
|
||||
@@ -71,7 +72,21 @@ class OnnxCrossEncoderModel(OnnxModel[float]):
|
||||
tokenized_input = self.tokenize(pairs, **kwargs)
|
||||
inputs = self._build_onnx_input(tokenized_input)
|
||||
onnx_input = self._preprocess_onnx_input(inputs, **kwargs)
|
||||
outputs = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr]
|
||||
|
||||
run_options = ort.RunOptions()
|
||||
providers = kwargs.get("providers", None)
|
||||
cuda = kwargs.get("cuda", False)
|
||||
if is_cuda_enabled(cuda, providers):
|
||||
device_id = kwargs.get("device_id", None)
|
||||
device_id = str(device_id if isinstance(device_id, int) else 0)
|
||||
# Enables memory arena shrinkage, freeing unused memory after each Run() cycle.
|
||||
# Helps prevent excessive memory retention, especially for dynamic workloads.
|
||||
# Source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage:
|
||||
run_options.add_run_config_entry(
|
||||
"memory.enable_memory_arena_shrinkage", f"gpu:{device_id}"
|
||||
)
|
||||
|
||||
outputs = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input, run_options) # type: ignore[union-attr]
|
||||
relevant_output = outputs[0]
|
||||
scores: NumpyArray = relevant_output[:, 0]
|
||||
return OnnxOutputContext(model_output=scores)
|
||||
@@ -110,7 +125,9 @@ class OnnxCrossEncoderModel(OnnxModel[float]):
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
for batch in iter_batch(pairs, batch_size):
|
||||
yield from self._post_process_onnx_output(self.onnx_embed_pairs(batch, **kwargs))
|
||||
yield from self._post_process_onnx_output(
|
||||
self.onnx_embed_pairs(batch, cuda=cuda, providers=providers, **kwargs)
|
||||
)
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
@@ -163,7 +180,9 @@ class TextRerankerWorker(EmbeddingWorker[float]):
|
||||
) -> OnnxCrossEncoderModel:
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]], **kwargs: Any
|
||||
) -> Iterable[tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.onnx_embed_pairs(batch)
|
||||
onnx_output = self.model.onnx_embed_pairs(batch, **kwargs)
|
||||
yield idx, onnx_output
|
||||
|
||||
@@ -3,19 +3,13 @@ from dataclasses import asdict
|
||||
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.rerank.cross_encoder.onnx_text_cross_encoder import OnnxTextCrossEncoder
|
||||
from fastembed.rerank.cross_encoder.custom_text_cross_encoder import CustomTextCrossEncoder
|
||||
|
||||
from fastembed.rerank.cross_encoder.text_cross_encoder_base import TextCrossEncoderBase
|
||||
from fastembed.common.model_description import (
|
||||
ModelSource,
|
||||
BaseModelDescription,
|
||||
)
|
||||
from fastembed.common.model_description import BaseModelDescription
|
||||
|
||||
|
||||
class TextCrossEncoder(TextCrossEncoderBase):
|
||||
CROSS_ENCODER_REGISTRY: list[Type[TextCrossEncoderBase]] = [
|
||||
OnnxTextCrossEncoder,
|
||||
CustomTextCrossEncoder,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@@ -130,34 +124,3 @@ class TextCrossEncoder(TextCrossEncoderBase):
|
||||
yield from self.model.rerank_pairs(
|
||||
pairs, batch_size=batch_size, parallel=parallel, **kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def add_custom_model(
|
||||
cls,
|
||||
model: str,
|
||||
sources: ModelSource,
|
||||
model_file: str = "onnx/model.onnx",
|
||||
description: str = "",
|
||||
license: str = "",
|
||||
size_in_gb: float = 0.0,
|
||||
additional_files: Optional[list[str]] = None,
|
||||
) -> None:
|
||||
registered_models = cls._list_supported_models()
|
||||
for registered_model in registered_models:
|
||||
if model == registered_model.model:
|
||||
raise ValueError(
|
||||
f"Model {model} is already registered in CrossEncoderModel, if you still want to add this model, "
|
||||
f"please use another model name"
|
||||
)
|
||||
|
||||
CustomTextCrossEncoder.add_model(
|
||||
BaseModelDescription(
|
||||
model=model,
|
||||
sources=sources,
|
||||
model_file=model_file,
|
||||
description=description,
|
||||
license=license,
|
||||
size_in_GB=size_in_gb,
|
||||
additional_files=additional_files or [],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -344,7 +344,7 @@ class Bm25Worker(Worker):
|
||||
return cls(model_name=model_name, cache_dir=cache_dir, **kwargs)
|
||||
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]]
|
||||
self, items: Iterable[tuple[int, Any]], **kwargs: Any
|
||||
) -> Iterable[tuple[int, list[SparseEmbedding]]]:
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.raw_embed(batch)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
from fastembed.common.types import NumpyArray, OnnxProvider
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import define_cache_dir, normalize
|
||||
@@ -20,7 +21,6 @@ supported_onnx_models: list[DenseModelDescription] = [
|
||||
sources=ModelSource(
|
||||
hf="Qdrant/fast-bge-base-en",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
@@ -36,7 +36,6 @@ supported_onnx_models: list[DenseModelDescription] = [
|
||||
sources=ModelSource(
|
||||
hf="qdrant/bge-base-en-v1.5-onnx-q",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en-v1.5.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
@@ -64,7 +63,6 @@ supported_onnx_models: list[DenseModelDescription] = [
|
||||
sources=ModelSource(
|
||||
hf="Qdrant/bge-small-en",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/BAAI-bge-small-en.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
@@ -92,7 +90,6 @@ supported_onnx_models: list[DenseModelDescription] = [
|
||||
sources=ModelSource(
|
||||
hf="Qdrant/bge-small-zh-v1.5",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/fast-bge-small-zh-v1.5.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
@@ -312,7 +309,7 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[NumpyArray]):
|
||||
processed_embeddings = embeddings
|
||||
else:
|
||||
raise ValueError(f"Unsupported embedding shape: {embeddings.shape}")
|
||||
return normalize(processed_embeddings)
|
||||
return normalize(processed_embeddings).astype(np.float32)
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
self._load_onnx_model(
|
||||
|
||||
@@ -4,13 +4,14 @@ from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
from numpy.typing import NDArray
|
||||
from tokenizers import Encoding, Tokenizer
|
||||
|
||||
from fastembed.common.types import NumpyArray, OnnxProvider
|
||||
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T
|
||||
from fastembed.common.preprocessor_utils import load_tokenizer
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.common.utils import iter_batch, is_cuda_enabled
|
||||
from fastembed.parallel_processor import ParallelWorkerPool
|
||||
|
||||
|
||||
@@ -82,7 +83,21 @@ class OnnxTextModel(OnnxModel[T]):
|
||||
)
|
||||
onnx_input = self._preprocess_onnx_input(onnx_input, **kwargs)
|
||||
|
||||
model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr]
|
||||
run_options = ort.RunOptions()
|
||||
providers = kwargs.get("providers", None)
|
||||
cuda = kwargs.get("cuda", False)
|
||||
|
||||
if is_cuda_enabled(cuda, providers):
|
||||
device_id = kwargs.get("device_id", None)
|
||||
device_id = str(device_id if isinstance(device_id, int) else 0)
|
||||
# enables memory arena shrinkage, freeing unused memory after each Run() cycle.
|
||||
# helps prevent excessive memory retention, especially for dynamic workloads.
|
||||
# source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage:
|
||||
run_options.add_run_config_entry(
|
||||
"memory.enable_memory_arena_shrinkage", f"gpu:{device_id}"
|
||||
)
|
||||
|
||||
model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input, run_options) # type: ignore[union-attr]
|
||||
return OnnxOutputContext(
|
||||
model_output=model_output[0],
|
||||
attention_mask=onnx_input.get("attention_mask", attention_mask),
|
||||
@@ -115,7 +130,9 @@ class OnnxTextModel(OnnxModel[T]):
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
for batch in iter_batch(documents, batch_size):
|
||||
yield from self._post_process_onnx_output(self.onnx_embed(batch))
|
||||
yield from self._post_process_onnx_output(
|
||||
self.onnx_embed(batch, cuda=cuda, providers=providers)
|
||||
)
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
@@ -140,7 +157,9 @@ class OnnxTextModel(OnnxModel[T]):
|
||||
|
||||
|
||||
class TextEmbeddingWorker(EmbeddingWorker[T]):
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, OnnxOutputContext]]:
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]], **kwargs: Any
|
||||
) -> Iterable[tuple[int, OnnxOutputContext]]:
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.onnx_embed(batch)
|
||||
onnx_output = self.model.onnx_embed(batch, **kwargs)
|
||||
yield idx, onnx_output
|
||||
|
||||
@@ -82,7 +82,6 @@ supported_pooled_models: list[DenseModelDescription] = [
|
||||
sources=ModelSource(
|
||||
hf="qdrant/multilingual-e5-large-onnx",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/fast-multilingual-e5-large.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model.onnx",
|
||||
additional_files=["model.onnx_data"],
|
||||
@@ -116,7 +115,7 @@ class PooledEmbedding(OnnxTextEmbedding):
|
||||
|
||||
embeddings = output.model_output
|
||||
attn_mask = output.attention_mask
|
||||
return self.mean_pooling(embeddings, attn_mask)
|
||||
return self.mean_pooling(embeddings, attn_mask).astype(np.float32)
|
||||
|
||||
|
||||
class PooledEmbeddingWorker(OnnxTextEmbeddingWorker):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Any, Iterable, Type
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
@@ -21,7 +22,6 @@ supported_pooled_normalized_models: list[DenseModelDescription] = [
|
||||
sources=ModelSource(
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/sentence-transformers-all-MiniLM-L6-v2.tar.gz",
|
||||
hf="qdrant/all-MiniLM-L6-v2-onnx",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
@@ -144,7 +144,7 @@ class PooledNormalizedEmbedding(PooledEmbedding):
|
||||
|
||||
embeddings = output.model_output
|
||||
attn_mask = output.attention_mask
|
||||
return normalize(self.mean_pooling(embeddings, attn_mask))
|
||||
return normalize(self.mean_pooling(embeddings, attn_mask)).astype(np.float32)
|
||||
|
||||
|
||||
class PooledNormalizedEmbeddingWorker(OnnxTextEmbeddingWorker):
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
[tool.poetry]
|
||||
name = "fastembed"
|
||||
name = "fastembed-gpu"
|
||||
version = "0.6.0"
|
||||
description = "Fast, light, accurate library built for retrieval embedding generation"
|
||||
authors = ["Qdrant Team <info@qdrant.tech>", "NirantK <nirant.bits@gmail.com>"]
|
||||
@@ -18,7 +18,7 @@ numpy = [
|
||||
{ version = ">=2.1.0", python = ">=3.13" },
|
||||
{ version = ">=1.21,<2.1.0", python = "<3.10" },
|
||||
]
|
||||
onnxruntime = [
|
||||
onnxruntime-gpu = [
|
||||
{ version = ">1.20.0", python = ">=3.13" },
|
||||
{ version = ">=1.17.0,<1.20.0", python = "<3.10" },
|
||||
{ version = ">=1.17.0,!=1.20.0", python = ">=3.10,<3.13" },
|
||||
|
||||
@@ -3,17 +3,10 @@ import os
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from fastembed.common.model_description import (
|
||||
PoolingType,
|
||||
ModelSource,
|
||||
DenseModelDescription,
|
||||
BaseModelDescription,
|
||||
)
|
||||
from fastembed.common.model_description import PoolingType, ModelSource, DenseModelDescription
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import normalize, mean_pooling
|
||||
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 import TextCrossEncoder
|
||||
from fastembed.text.text_embedding import TextEmbedding
|
||||
from tests.utils import delete_model_cache
|
||||
|
||||
@@ -21,10 +14,8 @@ from tests.utils import delete_model_cache
|
||||
@pytest.fixture(autouse=True)
|
||||
def restore_custom_models_fixture():
|
||||
CustomTextEmbedding.SUPPORTED_MODELS = []
|
||||
CustomTextCrossEncoder.SUPPORTED_MODELS = []
|
||||
yield
|
||||
CustomTextEmbedding.SUPPORTED_MODELS = []
|
||||
CustomTextCrossEncoder.SUPPORTED_MODELS = []
|
||||
|
||||
|
||||
def test_text_custom_model():
|
||||
@@ -74,43 +65,6 @@ def test_text_custom_model():
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
def test_cross_encoder_custom_model():
|
||||
is_ci = os.getenv("CI")
|
||||
custom_model_name = "Xenova/ms-marco-MiniLM-L-4-v2"
|
||||
size_in_gb = 0.08
|
||||
source = ModelSource(hf=custom_model_name)
|
||||
canonical_vector = np.array([-5.7170815, -11.112114], dtype=np.float32)
|
||||
|
||||
TextCrossEncoder.add_custom_model(
|
||||
custom_model_name,
|
||||
model_file="onnx/model.onnx",
|
||||
sources=source,
|
||||
size_in_gb=size_in_gb,
|
||||
)
|
||||
|
||||
assert CustomTextCrossEncoder.SUPPORTED_MODELS[0] == BaseModelDescription(
|
||||
model=custom_model_name,
|
||||
sources=source,
|
||||
model_file="onnx/model.onnx",
|
||||
description="",
|
||||
license="",
|
||||
size_in_GB=size_in_gb,
|
||||
)
|
||||
|
||||
model = TextCrossEncoder(custom_model_name)
|
||||
pairs = [
|
||||
("What is AI?", "Artificial intelligence is ..."),
|
||||
("What is ML?", "Machine learning is ..."),
|
||||
]
|
||||
scores = list(model.rerank_pairs(pairs))
|
||||
|
||||
embeddings = np.stack(scores, axis=0)
|
||||
assert embeddings.shape == (2,)
|
||||
assert np.allclose(embeddings, canonical_vector, atol=1e-3)
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
def test_mock_add_custom_models():
|
||||
dim = 5
|
||||
size_in_gb = 0.1
|
||||
@@ -137,11 +91,15 @@ def test_mock_add_custom_models():
|
||||
expected_output = {
|
||||
f"{PoolingType.MEAN.lower()}-normalized": normalize(
|
||||
mean_pooling(dummy_token_embedding, dummy_attention_mask)
|
||||
),
|
||||
).astype(np.float32),
|
||||
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]).astype(
|
||||
np.float32
|
||||
),
|
||||
f"{PoolingType.CLS.lower()}": dummy_token_embedding[:, 0],
|
||||
f"{PoolingType.DISABLED.lower()}-normalized": normalize(dummy_pooled_embedding),
|
||||
f"{PoolingType.DISABLED.lower()}-normalized": normalize(dummy_pooled_embedding).astype(
|
||||
np.float32
|
||||
),
|
||||
f"{PoolingType.DISABLED.lower()}": dummy_pooled_embedding,
|
||||
}
|
||||
|
||||
@@ -202,28 +160,3 @@ def test_do_not_add_existing_model():
|
||||
dim=384,
|
||||
size_in_gb=0.47,
|
||||
)
|
||||
|
||||
|
||||
def test_do_not_add_existing_cross_encoder():
|
||||
existing_base_model = "Xenova/ms-marco-MiniLM-L-6-v2"
|
||||
custom_model_name = "Xenova/ms-marco-MiniLM-L-4-v2"
|
||||
|
||||
with pytest.raises(ValueError, match=f"Model {existing_base_model} is already registered"):
|
||||
TextCrossEncoder.add_custom_model(
|
||||
existing_base_model,
|
||||
sources=ModelSource(hf=existing_base_model),
|
||||
size_in_gb=0.08,
|
||||
)
|
||||
|
||||
TextCrossEncoder.add_custom_model(
|
||||
custom_model_name,
|
||||
sources=ModelSource(hf=existing_base_model),
|
||||
size_in_gb=0.08,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=f"Model {custom_model_name} is already registered"):
|
||||
TextCrossEncoder.add_custom_model(
|
||||
custom_model_name,
|
||||
sources=ModelSource(hf=custom_model_name),
|
||||
size_in_gb=0.08,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from PIL import Image
|
||||
|
||||
from fastembed import ImageEmbedding
|
||||
from tests.config import TEST_MISC_DIR
|
||||
from tests.utils import delete_model_cache, should_test_model
|
||||
from tests.utils import delete_model_cache
|
||||
|
||||
CANONICAL_VECTOR_VALUES = {
|
||||
"Qdrant/clip-ViT-B-32-vision": np.array([-0.0098, 0.0128, -0.0274, 0.002, -0.0059]),
|
||||
@@ -27,13 +27,11 @@ CANONICAL_VECTOR_VALUES = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Qdrant/clip-ViT-B-32-vision"])
|
||||
def test_embedding(model_name: str) -> None:
|
||||
def test_embedding() -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
|
||||
for model_desc in ImageEmbedding._list_supported_models():
|
||||
if not should_test_model(model_desc, model_name, is_ci, is_manual):
|
||||
if not is_ci and model_desc.size_in_GB > 1:
|
||||
continue
|
||||
|
||||
dim = model_desc.dim
|
||||
@@ -76,12 +74,8 @@ def test_batch_embedding(n_dims: int, model_name: str) -> None:
|
||||
|
||||
embeddings = list(model.embed(images, batch_size=10))
|
||||
embeddings = np.stack(embeddings, axis=0)
|
||||
assert np.allclose(embeddings[1], embeddings[2])
|
||||
|
||||
canonical_vector = CANONICAL_VECTOR_VALUES[model_name]
|
||||
|
||||
assert embeddings.shape == (len(test_images) * n_images, n_dims)
|
||||
assert np.allclose(embeddings[0, : canonical_vector.shape[0]], canonical_vector, atol=1e-3)
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import numpy as np
|
||||
from fastembed.late_interaction.late_interaction_text_embedding import (
|
||||
LateInteractionTextEmbedding,
|
||||
)
|
||||
from tests.utils import delete_model_cache, should_test_model
|
||||
from tests.utils import delete_model_cache
|
||||
|
||||
# vectors are abridged and rounded for brevity
|
||||
CANONICAL_COLUMN_VALUES = {
|
||||
@@ -153,37 +153,31 @@ CANONICAL_QUERY_VALUES = {
|
||||
docs = ["Hello World"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["answerdotai/answerai-colbert-small-v1"])
|
||||
def test_batch_embedding(model_name: str):
|
||||
def test_batch_embedding():
|
||||
is_ci = os.getenv("CI")
|
||||
docs_to_embed = docs * 10
|
||||
|
||||
model = LateInteractionTextEmbedding(model_name=model_name)
|
||||
result = list(model.embed(docs_to_embed, batch_size=6))
|
||||
expected_result = CANONICAL_COLUMN_VALUES[model_name]
|
||||
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionTextEmbedding(model_name=model_name)
|
||||
result = list(model.embed(docs_to_embed, batch_size=6))
|
||||
|
||||
for value in result:
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(value[:, :abridged_dim], expected_result, atol=2e-3)
|
||||
for value in result:
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(value[:, :abridged_dim], expected_result, atol=2e-3)
|
||||
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["answerdotai/answerai-colbert-small-v1"])
|
||||
def test_single_embedding(model_name: str):
|
||||
def test_single_embedding():
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
docs_to_embed = docs
|
||||
|
||||
for model_desc in LateInteractionTextEmbedding._list_supported_models():
|
||||
if not should_test_model(model_desc, model_name, is_ci, is_manual):
|
||||
continue
|
||||
|
||||
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionTextEmbedding(model_name=model_name)
|
||||
result = next(iter(model.embed(docs_to_embed, batch_size=6)))
|
||||
expected_result = CANONICAL_COLUMN_VALUES[model_name]
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(result[:, :abridged_dim], expected_result, atol=2e-3)
|
||||
|
||||
@@ -191,20 +185,14 @@ def test_single_embedding(model_name: str):
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["answerdotai/answerai-colbert-small-v1"])
|
||||
def test_single_embedding_query(model_name: str):
|
||||
def test_single_embedding_query():
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
queries_to_embed = docs
|
||||
|
||||
for model_desc in LateInteractionTextEmbedding._list_supported_models():
|
||||
if not should_test_model(model_desc, model_name, is_ci, is_manual):
|
||||
continue
|
||||
|
||||
for model_name, expected_result in CANONICAL_QUERY_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionTextEmbedding(model_name=model_name)
|
||||
result = next(iter(model.query_embed(queries_to_embed)))
|
||||
expected_result = CANONICAL_QUERY_VALUES[model_name]
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(result[:, :abridged_dim], expected_result, atol=2e-3)
|
||||
|
||||
@@ -212,11 +200,10 @@ def test_single_embedding_query(model_name: str):
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("token_dim,model_name", [(96, "answerdotai/answerai-colbert-small-v1")])
|
||||
def test_parallel_processing(token_dim: int, model_name: str):
|
||||
def test_parallel_processing():
|
||||
is_ci = os.getenv("CI")
|
||||
model = LateInteractionTextEmbedding(model_name=model_name)
|
||||
|
||||
model = LateInteractionTextEmbedding(model_name="colbert-ir/colbertv2.0")
|
||||
token_dim = 128
|
||||
docs = ["hello world", "flag embedding"] * 100
|
||||
embeddings = list(model.embed(docs, batch_size=10, parallel=2))
|
||||
embeddings = np.stack(embeddings, axis=0)
|
||||
@@ -235,7 +222,10 @@ def test_parallel_processing(token_dim: int, model_name: str):
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["answerdotai/answerai-colbert-small-v1"])
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
["colbert-ir/colbertv2.0"],
|
||||
)
|
||||
def test_lazy_load(model_name: str):
|
||||
is_ci = os.getenv("CI")
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
@@ -46,38 +45,38 @@ images = [
|
||||
|
||||
|
||||
def test_batch_embedding():
|
||||
if os.getenv("CI"):
|
||||
pytest.skip("Colpali is too large to test in CI")
|
||||
is_ci = os.getenv("CI")
|
||||
|
||||
for model_name, expected_result in CANONICAL_IMAGE_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionMultimodalEmbedding(model_name=model_name)
|
||||
result = list(model.embed_image(images, batch_size=2))
|
||||
if not is_ci:
|
||||
for model_name, expected_result in CANONICAL_IMAGE_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionMultimodalEmbedding(model_name=model_name)
|
||||
result = list(model.embed_image(images, batch_size=2))
|
||||
|
||||
for value in result:
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(value[:token_num, :abridged_dim], expected_result, atol=2e-3)
|
||||
for value in result:
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(value[:token_num, :abridged_dim], expected_result, atol=2e-3)
|
||||
|
||||
|
||||
def test_single_embedding():
|
||||
if os.getenv("CI"):
|
||||
pytest.skip("Colpali is too large to test in CI")
|
||||
|
||||
for model_name, expected_result in CANONICAL_IMAGE_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionMultimodalEmbedding(model_name=model_name)
|
||||
result = next(iter(model.embed_image(images, batch_size=6)))
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(result[:token_num, :abridged_dim], expected_result, atol=2e-3)
|
||||
is_ci = os.getenv("CI")
|
||||
if not is_ci:
|
||||
for model_name, expected_result in CANONICAL_IMAGE_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionMultimodalEmbedding(model_name=model_name)
|
||||
result = next(iter(model.embed_image(images, batch_size=6)))
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(result[:token_num, :abridged_dim], expected_result, atol=2e-3)
|
||||
|
||||
|
||||
def test_single_embedding_query():
|
||||
if os.getenv("CI"):
|
||||
pytest.skip("Colpali is too large to test in CI")
|
||||
is_ci = os.getenv("CI")
|
||||
if not is_ci:
|
||||
queries_to_embed = queries
|
||||
|
||||
for model_name, expected_result in CANONICAL_QUERY_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionMultimodalEmbedding(model_name=model_name)
|
||||
result = next(iter(model.embed_text(queries)))
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(result[:token_num, :abridged_dim], expected_result, atol=2e-3)
|
||||
for model_name, expected_result in CANONICAL_QUERY_VALUES.items():
|
||||
print("evaluating", model_name)
|
||||
model = LateInteractionMultimodalEmbedding(model_name=model_name)
|
||||
result = next(iter(model.embed_text(queries_to_embed)))
|
||||
token_num, abridged_dim = expected_result.shape
|
||||
assert np.allclose(result[:token_num, :abridged_dim], expected_result, atol=2e-3)
|
||||
|
||||
@@ -5,10 +5,10 @@ import numpy as np
|
||||
|
||||
from fastembed.sparse.bm25 import Bm25
|
||||
from fastembed.sparse.sparse_text_embedding import SparseTextEmbedding
|
||||
from tests.utils import delete_model_cache, should_test_model
|
||||
from tests.utils import delete_model_cache
|
||||
|
||||
CANONICAL_COLUMN_VALUES = {
|
||||
"prithivida/Splade_PP_en_v1": {
|
||||
"prithvida/Splade_PP_en_v1": {
|
||||
"indices": [
|
||||
2040,
|
||||
2047,
|
||||
@@ -49,41 +49,28 @@ CANONICAL_COLUMN_VALUES = {
|
||||
docs = ["Hello World"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["prithivida/Splade_PP_en_v1"])
|
||||
def test_batch_embedding(model_name: str) -> None:
|
||||
def test_batch_embedding() -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
docs_to_embed = docs * 10
|
||||
|
||||
model = SparseTextEmbedding(model_name=model_name)
|
||||
result = next(iter(model.embed(docs_to_embed, batch_size=6)))
|
||||
expected_result = CANONICAL_COLUMN_VALUES[model_name]
|
||||
assert result.indices.tolist() == expected_result["indices"]
|
||||
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
|
||||
model = SparseTextEmbedding(model_name=model_name)
|
||||
result = next(iter(model.embed(docs_to_embed, batch_size=6)))
|
||||
assert result.indices.tolist() == expected_result["indices"]
|
||||
|
||||
for i, value in enumerate(result.values):
|
||||
assert pytest.approx(value, abs=0.001) == expected_result["values"][i]
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
for i, value in enumerate(result.values):
|
||||
assert pytest.approx(value, abs=0.001) == expected_result["values"][i]
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["prithivida/Splade_PP_en_v1"])
|
||||
def test_single_embedding(model_name: str) -> None:
|
||||
def test_single_embedding() -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
|
||||
for model_desc in SparseTextEmbedding._list_supported_models():
|
||||
if (
|
||||
model_desc.model not in CANONICAL_COLUMN_VALUES
|
||||
): # attention models and bm25 are also parts of
|
||||
# SparseTextEmbedding, however, they have their own tests
|
||||
continue
|
||||
if not should_test_model(model_desc, model_name, is_ci, is_manual):
|
||||
continue
|
||||
|
||||
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
|
||||
model = SparseTextEmbedding(model_name=model_name)
|
||||
|
||||
passage_result = next(iter(model.embed(docs, batch_size=6)))
|
||||
query_result = next(iter(model.query_embed(docs)))
|
||||
expected_result = CANONICAL_COLUMN_VALUES[model_name]
|
||||
for result in [passage_result, query_result]:
|
||||
assert result.indices.tolist() == expected_result["indices"]
|
||||
|
||||
@@ -93,10 +80,9 @@ def test_single_embedding(model_name: str) -> None:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["prithivida/Splade_PP_en_v1"])
|
||||
def test_parallel_processing(model_name: str) -> None:
|
||||
def test_parallel_processing() -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
model = SparseTextEmbedding(model_name=model_name)
|
||||
model = SparseTextEmbedding(model_name="prithivida/Splade_PP_en_v1")
|
||||
docs = ["hello world", "flag embedding"] * 30
|
||||
sparse_embeddings_duo = list(model.embed(docs, batch_size=10, parallel=2))
|
||||
sparse_embeddings_all = list(model.embed(docs, batch_size=10, parallel=0))
|
||||
@@ -186,7 +172,10 @@ def test_disable_stemmer_behavior(disable_stemmer: bool) -> None:
|
||||
assert result == expected, f"Expected {expected}, but got {result}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["prithivida/Splade_PP_en_v1"])
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
["prithivida/Splade_PP_en_v1"],
|
||||
)
|
||||
def test_lazy_load(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
model = SparseTextEmbedding(model_name=model_name, lazy_load=True)
|
||||
|
||||
@@ -4,7 +4,7 @@ import numpy as np
|
||||
import pytest
|
||||
|
||||
from fastembed.rerank.cross_encoder import TextCrossEncoder
|
||||
from tests.utils import delete_model_cache, should_test_model
|
||||
from tests.utils import delete_model_cache
|
||||
|
||||
CANONICAL_SCORE_VALUES = {
|
||||
"Xenova/ms-marco-MiniLM-L-6-v2": np.array([8.500708, -2.541011]),
|
||||
@@ -15,37 +15,44 @@ CANONICAL_SCORE_VALUES = {
|
||||
"jinaai/jina-reranker-v2-base-multilingual": np.array([1.6533, -1.6455]),
|
||||
}
|
||||
|
||||
SELECTED_MODELS = {
|
||||
"Xenova": "Xenova/ms-marco-MiniLM-L-6-v2",
|
||||
"BAAI": "BAAI/bge-reranker-base",
|
||||
"jinaai": "jinaai/jina-reranker-v1-tiny-en",
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Xenova/ms-marco-MiniLM-L-6-v2"])
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[model_name for model_name in CANONICAL_SCORE_VALUES],
|
||||
)
|
||||
def test_rerank(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
|
||||
for model_desc in TextCrossEncoder._list_supported_models():
|
||||
if not should_test_model(model_desc, model_name, is_ci, is_manual):
|
||||
continue
|
||||
model = TextCrossEncoder(model_name=model_name)
|
||||
|
||||
model = TextCrossEncoder(model_name=model_name)
|
||||
query = "What is the capital of France?"
|
||||
documents = ["Paris is the capital of France.", "Berlin is the capital of Germany."]
|
||||
scores = np.array(list(model.rerank(query, documents)))
|
||||
|
||||
query = "What is the capital of France?"
|
||||
documents = ["Paris is the capital of France.", "Berlin is the capital of Germany."]
|
||||
scores = np.array(list(model.rerank(query, documents)))
|
||||
pairs = [(query, doc) for doc in documents]
|
||||
scores2 = np.array(list(model.rerank_pairs(pairs)))
|
||||
assert np.allclose(
|
||||
scores, scores2, atol=1e-5
|
||||
), f"Model: {model_name}, Scores: {scores}, Scores2: {scores2}"
|
||||
|
||||
pairs = [(query, doc) for doc in documents]
|
||||
scores2 = np.array(list(model.rerank_pairs(pairs)))
|
||||
assert np.allclose(
|
||||
scores, scores2, atol=1e-5
|
||||
), f"Model: {model_name}, Scores: {scores}, Scores2: {scores2}"
|
||||
|
||||
canonical_scores = CANONICAL_SCORE_VALUES[model_name]
|
||||
assert np.allclose(
|
||||
scores, canonical_scores, atol=1e-3
|
||||
), f"Model: {model_name}, Scores: {scores}, Expected: {canonical_scores}"
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
canonical_scores = CANONICAL_SCORE_VALUES[model_name]
|
||||
assert np.allclose(
|
||||
scores, canonical_scores, atol=1e-3
|
||||
), f"Model: {model_name}, Scores: {scores}, Expected: {canonical_scores}"
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Xenova/ms-marco-MiniLM-L-6-v2"])
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[model_name for model_name in SELECTED_MODELS.values()],
|
||||
)
|
||||
def test_batch_rerank(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
|
||||
@@ -71,7 +78,10 @@ def test_batch_rerank(model_name: str) -> None:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Xenova/ms-marco-MiniLM-L-6-v2"])
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
["Xenova/ms-marco-MiniLM-L-6-v2"],
|
||||
)
|
||||
def test_lazy_load(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
model = TextCrossEncoder(model_name=model_name, lazy_load=True)
|
||||
@@ -85,7 +95,10 @@ def test_lazy_load(model_name: str) -> None:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Xenova/ms-marco-MiniLM-L-6-v2"])
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[model_name for model_name in SELECTED_MODELS.values()],
|
||||
)
|
||||
def test_rerank_pairs_parallel(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import numpy as np
|
||||
import pytest
|
||||
|
||||
from fastembed import TextEmbedding
|
||||
from fastembed.text.multitask_embedding import JinaEmbeddingV3, Task
|
||||
from fastembed.text.multitask_embedding import Task
|
||||
from tests.utils import delete_model_cache
|
||||
|
||||
|
||||
@@ -60,43 +60,52 @@ CANONICAL_VECTOR_VALUES = {
|
||||
docs = ["Hello World", "Follow the white rabbit."]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dim,model_name", [(1024, "jinaai/jina-embeddings-v3")])
|
||||
def test_batch_embedding(dim: int, model_name: str):
|
||||
def test_batch_embedding():
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
if is_ci and not is_manual:
|
||||
pytest.skip("Skipping multitask models in CI non-manual mode")
|
||||
|
||||
docs_to_embed = docs * 10
|
||||
default_task = Task.RETRIEVAL_PASSAGE
|
||||
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
for model_desc in TextEmbedding._list_supported_models():
|
||||
if not is_ci and model_desc.size_in_GB > 1:
|
||||
continue
|
||||
|
||||
embeddings = list(model.embed(documents=docs_to_embed, batch_size=6))
|
||||
embeddings = np.stack(embeddings, axis=0)
|
||||
model_name = model_desc.model
|
||||
dim = model_desc.dim
|
||||
|
||||
assert embeddings.shape == (len(docs_to_embed), dim)
|
||||
if model_name not in CANONICAL_VECTOR_VALUES.keys():
|
||||
continue
|
||||
|
||||
canonical_vector = CANONICAL_VECTOR_VALUES[model_name][default_task]["vectors"]
|
||||
assert np.allclose(
|
||||
embeddings[: len(docs), : canonical_vector.shape[1]], canonical_vector, atol=1e-4
|
||||
), model_name
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
print(f"evaluating {model_name} default task")
|
||||
|
||||
embeddings = list(model.embed(documents=docs_to_embed, batch_size=6))
|
||||
embeddings = np.stack(embeddings, axis=0)
|
||||
|
||||
assert embeddings.shape == (len(docs_to_embed), dim)
|
||||
|
||||
canonical_vector = CANONICAL_VECTOR_VALUES[model_name][default_task]["vectors"]
|
||||
assert np.allclose(
|
||||
embeddings[: len(docs), : canonical_vector.shape[1]], canonical_vector, atol=1e-4
|
||||
), model_desc.model
|
||||
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
def test_single_embedding():
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
if is_ci and not is_manual:
|
||||
pytest.skip("Skipping multitask models in CI non-manual mode")
|
||||
|
||||
for model_desc in JinaEmbeddingV3._list_supported_models():
|
||||
# todo: once we add more models, we should not test models >1GB size locally
|
||||
for model_desc in TextEmbedding._list_supported_models():
|
||||
if not is_ci and model_desc.size_in_GB > 1:
|
||||
continue
|
||||
|
||||
model_name = model_desc.model
|
||||
dim = model_desc.dim
|
||||
|
||||
if model_name not in CANONICAL_VECTOR_VALUES.keys():
|
||||
continue
|
||||
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
|
||||
for task in CANONICAL_VECTOR_VALUES[model_name]:
|
||||
@@ -118,17 +127,18 @@ def test_single_embedding():
|
||||
|
||||
def test_single_embedding_query():
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
if is_ci and not is_manual:
|
||||
pytest.skip("Skipping multitask models in CI non-manual mode")
|
||||
|
||||
task_id = Task.RETRIEVAL_QUERY
|
||||
|
||||
for model_desc in JinaEmbeddingV3._list_supported_models():
|
||||
# todo: once we add more models, we should not test models >1GB size locally
|
||||
for model_desc in TextEmbedding._list_supported_models():
|
||||
if not is_ci and model_desc.size_in_GB > 1:
|
||||
continue
|
||||
|
||||
model_name = model_desc.model
|
||||
dim = model_desc.dim
|
||||
|
||||
if model_name not in CANONICAL_VECTOR_VALUES.keys():
|
||||
continue
|
||||
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
|
||||
print(f"evaluating {model_name} query_embed task_id: {task_id}")
|
||||
@@ -149,18 +159,18 @@ def test_single_embedding_query():
|
||||
|
||||
def test_single_embedding_passage():
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
if is_ci and not is_manual:
|
||||
pytest.skip("Skipping multitask models in CI non-manual mode")
|
||||
|
||||
task_id = Task.RETRIEVAL_PASSAGE
|
||||
|
||||
for model_desc in JinaEmbeddingV3._list_supported_models():
|
||||
# todo: once we add more models, we should not test models >1GB size locally
|
||||
for model_desc in TextEmbedding._list_supported_models():
|
||||
if not is_ci and model_desc.size_in_GB > 1:
|
||||
continue
|
||||
|
||||
model_name = model_desc.model
|
||||
dim = model_desc.dim
|
||||
|
||||
if model_name not in CANONICAL_VECTOR_VALUES.keys():
|
||||
continue
|
||||
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
|
||||
print(f"evaluating {model_name} passage_embed task_id: {task_id}")
|
||||
@@ -179,15 +189,14 @@ def test_single_embedding_passage():
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dim,model_name", [(1024, "jinaai/jina-embeddings-v3")])
|
||||
def test_parallel_processing(dim: int, model_name: str):
|
||||
def test_parallel_processing():
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
if is_ci and not is_manual:
|
||||
pytest.skip("Skipping in CI non-manual mode")
|
||||
|
||||
docs = ["Hello World", "Follow the white rabbit."] * 10
|
||||
|
||||
model_name = "jinaai/jina-embeddings-v3"
|
||||
dim = 1024
|
||||
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
|
||||
task_id = Task.SEPARATION
|
||||
@@ -209,14 +218,14 @@ def test_parallel_processing(dim: int, model_name: str):
|
||||
|
||||
def test_task_assignment():
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
|
||||
if is_ci and not is_manual:
|
||||
pytest.skip("Skipping in CI non-manual mode")
|
||||
for model_desc in TextEmbedding._list_supported_models():
|
||||
if not is_ci and model_desc.size_in_GB > 1:
|
||||
continue
|
||||
|
||||
for model_desc in JinaEmbeddingV3._list_supported_models():
|
||||
# todo: once we add more models, we should not test models >1GB size locally
|
||||
model_name = model_desc.model
|
||||
if model_name not in CANONICAL_VECTOR_VALUES.keys():
|
||||
continue
|
||||
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
|
||||
@@ -228,14 +237,12 @@ def test_task_assignment():
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["jinaai/jina-embeddings-v3"])
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
["jinaai/jina-embeddings-v3"],
|
||||
)
|
||||
def test_lazy_load(model_name: str):
|
||||
is_ci = os.getenv("CI")
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
|
||||
if is_ci and not is_manual:
|
||||
pytest.skip("Skipping in CI non-manual mode")
|
||||
|
||||
model = TextEmbedding(model_name=model_name, lazy_load=True)
|
||||
assert not hasattr(model.model, "model")
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import numpy as np
|
||||
import pytest
|
||||
|
||||
from fastembed.text.text_embedding import TextEmbedding
|
||||
from tests.utils import delete_model_cache, should_test_model
|
||||
from tests.utils import delete_model_cache
|
||||
|
||||
CANONICAL_VECTOR_VALUES = {
|
||||
"BAAI/bge-small-en": np.array([-0.0232, -0.0255, 0.0174, -0.0639, -0.0006]),
|
||||
@@ -72,19 +72,17 @@ CANONICAL_VECTOR_VALUES = {
|
||||
MULTI_TASK_MODELS = ["jinaai/jina-embeddings-v3"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["BAAI/bge-small-en-v1.5"])
|
||||
def test_embedding(model_name: str) -> None:
|
||||
def test_embedding() -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
is_mac = platform.system() == "Darwin"
|
||||
is_manual = os.getenv("GITHUB_EVENT_NAME") == "workflow_dispatch"
|
||||
|
||||
for model_desc in TextEmbedding._list_supported_models():
|
||||
if model_desc.model in MULTI_TASK_MODELS or (
|
||||
is_mac and model_desc.model == "nomic-ai/nomic-embed-text-v1.5-Q"
|
||||
if (
|
||||
(not is_ci and model_desc.size_in_GB > 1)
|
||||
or model_desc.model in MULTI_TASK_MODELS
|
||||
or (is_mac and model_desc.model == "nomic-ai/nomic-embed-text-v1.5-Q")
|
||||
):
|
||||
continue
|
||||
if not should_test_model(model_desc, model_name, is_ci, is_manual):
|
||||
continue
|
||||
|
||||
dim = model_desc.dim
|
||||
|
||||
@@ -97,12 +95,15 @@ def test_embedding(model_name: str) -> None:
|
||||
canonical_vector = CANONICAL_VECTOR_VALUES[model_desc.model]
|
||||
assert np.allclose(
|
||||
embeddings[0, : canonical_vector.shape[0]], canonical_vector, atol=1e-3
|
||||
), model_desc.model
|
||||
), model_desc["model"]
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@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"), (768, "jinaai/jina-embeddings-v2-base-en")],
|
||||
)
|
||||
def test_batch_embedding(n_dims: int, model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
@@ -111,12 +112,15 @@ def test_batch_embedding(n_dims: int, model_name: str) -> None:
|
||||
embeddings = list(model.embed(docs, batch_size=10))
|
||||
embeddings = np.stack(embeddings, axis=0)
|
||||
|
||||
assert embeddings.shape == (len(docs), n_dims)
|
||||
assert embeddings.shape == (200, n_dims)
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@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"), (768, "jinaai/jina-embeddings-v2-base-en")],
|
||||
)
|
||||
def test_parallel_processing(n_dims: int, model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
model = TextEmbedding(model_name=model_name)
|
||||
@@ -131,7 +135,7 @@ def test_parallel_processing(n_dims: int, model_name: str) -> None:
|
||||
embeddings_3 = list(model.embed(docs, batch_size=10, parallel=0))
|
||||
embeddings_3 = np.stack(embeddings_3, axis=0)
|
||||
|
||||
assert embeddings.shape == (len(docs), n_dims)
|
||||
assert embeddings.shape == (200, n_dims)
|
||||
assert np.allclose(embeddings, embeddings_2, atol=1e-3)
|
||||
assert np.allclose(embeddings, embeddings_3, atol=1e-3)
|
||||
|
||||
@@ -139,7 +143,10 @@ def test_parallel_processing(n_dims: int, model_name: str) -> None:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["BAAI/bge-small-en-v1.5"])
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
["BAAI/bge-small-en-v1.5"],
|
||||
)
|
||||
def test_lazy_load(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
model = TextEmbedding(model_name=model_name, lazy_load=True)
|
||||
|
||||
+1
-31
@@ -3,9 +3,7 @@ import traceback
|
||||
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Union, Callable, Any, Type, Optional
|
||||
|
||||
from fastembed.common.model_description import BaseModelDescription
|
||||
from typing import Union, Callable, Any, Type
|
||||
|
||||
|
||||
def delete_model_cache(model_dir: Union[str, Path]) -> None:
|
||||
@@ -37,31 +35,3 @@ def delete_model_cache(model_dir: Union[str, Path]) -> None:
|
||||
if model_dir.exists():
|
||||
# todo: PermissionDenied is raised on blobs removal in Windows, with blobs > 2GB
|
||||
shutil.rmtree(model_dir, onerror=on_error)
|
||||
|
||||
|
||||
def should_test_model(
|
||||
model_desc: BaseModelDescription,
|
||||
autotest_model_name: str,
|
||||
is_ci: Optional[str],
|
||||
is_manual: bool,
|
||||
):
|
||||
"""Determine if a model should be tested based on environment
|
||||
|
||||
Tests can be run either in ci or locally.
|
||||
Testing all models each time in ci is too long.
|
||||
The testing scheme in ci and on a local machine are different, therefore, there are 3 possible scenarious.
|
||||
1) Run lightweight tests in ci:
|
||||
- test only one model that has been manually chosen as a representative for a certain class family
|
||||
2) Run heavyweight (manual) tests in ci:
|
||||
- test all models
|
||||
Running tests in ci each time is too expensive, however, it's fine to run it one time with a manual dispatch
|
||||
3) Run tests locally:
|
||||
- test all models, which are not too heavy, since network speed might be a bottleneck
|
||||
|
||||
"""
|
||||
if not is_ci:
|
||||
if model_desc.size_in_GB > 1:
|
||||
return False
|
||||
elif not is_manual and model_desc.model != autotest_model_name:
|
||||
return False
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user