fastembed-gpu 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fastembed/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ import importlib.metadata
2
+
3
+ from fastembed.text import TextEmbedding
4
+ from fastembed.sparse import SparseTextEmbedding, SparseEmbedding
5
+
6
+ try:
7
+ version = importlib.metadata.version("fastembed")
8
+ except importlib.metadata.PackageNotFoundError as _:
9
+ version = importlib.metadata.version("fastembed-gpu")
10
+
11
+ __version__ = version
12
+ __all__ = ["TextEmbedding", "SparseTextEmbedding", "SparseEmbedding"]
@@ -0,0 +1,3 @@
1
+ from fastembed.common.onnx_model import OnnxProvider
2
+
3
+ __all__ = ["OnnxProvider"]
@@ -0,0 +1,244 @@
1
+ import os
2
+ import shutil
3
+ import tarfile
4
+ from pathlib import Path
5
+ from typing import List, Optional, Dict, Any
6
+
7
+ import requests
8
+ from huggingface_hub import snapshot_download
9
+ from huggingface_hub.utils import RepositoryNotFoundError
10
+ from tqdm import tqdm
11
+ from loguru import logger
12
+
13
+
14
+ class ModelManagement:
15
+ @classmethod
16
+ def list_supported_models(cls) -> List[Dict[str, Any]]:
17
+ """Lists the supported models.
18
+
19
+ Returns:
20
+ List[Dict[str, Any]]: A list of dictionaries containing the model information.
21
+ """
22
+ raise NotImplementedError()
23
+
24
+ @classmethod
25
+ def _get_model_description(cls, model_name: str) -> Dict[str, Any]:
26
+ """
27
+ Gets the model description from the model_name.
28
+
29
+ Args:
30
+ model_name (str): The name of the model.
31
+
32
+ raises:
33
+ ValueError: If the model_name is not supported.
34
+
35
+ Returns:
36
+ Dict[str, Any]: The model description.
37
+ """
38
+ for model in cls.list_supported_models():
39
+ if model_name.lower() == model["model"].lower():
40
+ return model
41
+
42
+ raise ValueError(f"Model {model_name} is not supported in {cls.__name__}.")
43
+
44
+ @classmethod
45
+ def download_file_from_gcs(cls, url: str, output_path: str, show_progress: bool = True) -> str:
46
+ """
47
+ Downloads a file from Google Cloud Storage.
48
+
49
+ Args:
50
+ url (str): The URL to download the file from.
51
+ output_path (str): The path to save the downloaded file to.
52
+ show_progress (bool, optional): Whether to show a progress bar. Defaults to True.
53
+
54
+ Returns:
55
+ str: The path to the downloaded file.
56
+ """
57
+
58
+ if os.path.exists(output_path):
59
+ return output_path
60
+ response = requests.get(url, stream=True)
61
+
62
+ # Handle HTTP errors
63
+ if response.status_code == 403:
64
+ raise PermissionError(
65
+ "Authentication Error: You do not have permission to access this resource. "
66
+ "Please check your credentials."
67
+ )
68
+
69
+ # Get the total size of the file
70
+ total_size_in_bytes = int(response.headers.get("content-length", 0))
71
+
72
+ # Warn if the total size is zero
73
+ if total_size_in_bytes == 0:
74
+ print(f"Warning: Content-length header is missing or zero in the response from {url}.")
75
+
76
+ show_progress = total_size_in_bytes and show_progress
77
+
78
+ with tqdm(
79
+ total=total_size_in_bytes, unit="iB", unit_scale=True, disable=not show_progress
80
+ ) as progress_bar:
81
+ with open(output_path, "wb") as file:
82
+ for chunk in response.iter_content(chunk_size=1024):
83
+ if chunk: # Filter out keep-alive new chunks
84
+ progress_bar.update(len(chunk))
85
+ file.write(chunk)
86
+ return output_path
87
+
88
+ @classmethod
89
+ def download_files_from_huggingface(
90
+ cls,
91
+ hf_source_repo: str,
92
+ cache_dir: Optional[str] = None,
93
+ extra_patterns: Optional[List[str]] = None,
94
+ **kwargs,
95
+ ) -> str:
96
+ """
97
+ Downloads a model from HuggingFace Hub.
98
+ Args:
99
+ hf_source_repo (str): Name of the model on HuggingFace Hub, e.g. "qdrant/all-MiniLM-L6-v2-onnx".
100
+ cache_dir (Optional[str]): The path to the cache directory.
101
+ extra_patterns (Optional[List[str]]): extra patterns to allow in the snapshot download, typically
102
+ includes the required model files.
103
+ Returns:
104
+ Path: The path to the model directory.
105
+ """
106
+ allow_patterns = [
107
+ "config.json",
108
+ "tokenizer.json",
109
+ "tokenizer_config.json",
110
+ "special_tokens_map.json",
111
+ ]
112
+ if extra_patterns is not None:
113
+ allow_patterns.extend(extra_patterns)
114
+
115
+ return snapshot_download(
116
+ repo_id=hf_source_repo,
117
+ allow_patterns=allow_patterns,
118
+ cache_dir=cache_dir,
119
+ local_files_only=kwargs.get("local_files_only", False),
120
+ )
121
+
122
+ @classmethod
123
+ def decompress_to_cache(cls, targz_path: str, cache_dir: str):
124
+ """
125
+ Decompresses a .tar.gz file to a cache directory.
126
+
127
+ Args:
128
+ targz_path (str): Path to the .tar.gz file.
129
+ cache_dir (str): Path to the cache directory.
130
+
131
+ Returns:
132
+ cache_dir (str): Path to the cache directory.
133
+ """
134
+ # Check if targz_path exists and is a file
135
+ if not os.path.isfile(targz_path):
136
+ raise ValueError(f"{targz_path} does not exist or is not a file.")
137
+
138
+ # Check if targz_path is a .tar.gz file
139
+ if not targz_path.endswith(".tar.gz"):
140
+ raise ValueError(f"{targz_path} is not a .tar.gz file.")
141
+
142
+ try:
143
+ # Open the tar.gz file
144
+ with tarfile.open(targz_path, "r:gz") as tar:
145
+ # Extract all files into the cache directory
146
+ tar.extractall(path=cache_dir)
147
+ except tarfile.TarError as e:
148
+ # If any error occurs while opening or extracting the tar.gz file,
149
+ # delete the cache directory (if it was created in this function)
150
+ # and raise the error again
151
+ if "tmp" in cache_dir:
152
+ shutil.rmtree(cache_dir)
153
+ raise ValueError(f"An error occurred while decompressing {targz_path}: {e}")
154
+
155
+ return cache_dir
156
+
157
+ @classmethod
158
+ def retrieve_model_gcs(cls, model_name: str, source_url: str, cache_dir: str) -> Path:
159
+ fast_model_name = f"fast-{model_name.split('/')[-1]}"
160
+
161
+ cache_tmp_dir = Path(cache_dir) / "tmp"
162
+ model_tmp_dir = cache_tmp_dir / fast_model_name
163
+ model_dir = Path(cache_dir) / fast_model_name
164
+
165
+ # check if the model_dir and the model files are both present for macOS
166
+ if model_dir.exists() and len(list(model_dir.glob("*"))) > 0:
167
+ return model_dir
168
+
169
+ if model_tmp_dir.exists():
170
+ shutil.rmtree(model_tmp_dir)
171
+
172
+ cache_tmp_dir.mkdir(parents=True, exist_ok=True)
173
+
174
+ model_tar_gz = Path(cache_dir) / f"{fast_model_name}.tar.gz"
175
+
176
+ if model_tar_gz.exists():
177
+ model_tar_gz.unlink()
178
+
179
+ cls.download_file_from_gcs(
180
+ source_url,
181
+ output_path=str(model_tar_gz),
182
+ )
183
+
184
+ cls.decompress_to_cache(targz_path=str(model_tar_gz), cache_dir=str(cache_tmp_dir))
185
+ assert model_tmp_dir.exists(), f"Could not find {model_tmp_dir} in {cache_tmp_dir}"
186
+
187
+ model_tar_gz.unlink()
188
+ # Rename from tmp to final name is atomic
189
+ model_tmp_dir.rename(model_dir)
190
+
191
+ return model_dir
192
+
193
+ @classmethod
194
+ def download_model(cls, model: Dict[str, Any], cache_dir: Path, **kwargs) -> Path:
195
+ """
196
+ Downloads a model from HuggingFace Hub or Google Cloud Storage.
197
+
198
+ Args:
199
+ model (Dict[str, Any]): The model description.
200
+ Example:
201
+ ```
202
+ {
203
+ "model": "BAAI/bge-base-en-v1.5",
204
+ "dim": 768,
205
+ "description": "Base English model, v1.5",
206
+ "size_in_GB": 0.44,
207
+ "sources": {
208
+ "url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en-v1.5.tar.gz",
209
+ "hf": "qdrant/bge-base-en-v1.5-onnx-q",
210
+ }
211
+ }
212
+ ```
213
+ cache_dir (str): The path to the cache directory.
214
+
215
+ Returns:
216
+ Path: The path to the downloaded model directory.
217
+ """
218
+
219
+ hf_source = model.get("sources", {}).get("hf")
220
+ url_source = model.get("sources", {}).get("url")
221
+
222
+ if hf_source:
223
+ extra_patterns = [model["model_file"]]
224
+ extra_patterns.extend(model.get("additional_files", []))
225
+
226
+ try:
227
+ return Path(
228
+ cls.download_files_from_huggingface(
229
+ hf_source,
230
+ cache_dir=str(cache_dir),
231
+ extra_patterns=extra_patterns,
232
+ local_files_only=kwargs.get("local_files_only", False),
233
+ )
234
+ )
235
+ except (EnvironmentError, RepositoryNotFoundError, ValueError) as e:
236
+ logger.error(
237
+ f"Could not download model from HuggingFace: {e}"
238
+ "Falling back to other sources."
239
+ )
240
+
241
+ if url_source:
242
+ return cls.retrieve_model_gcs(model["model"], url_source, str(cache_dir))
243
+
244
+ raise ValueError(f"Could not download model {model['model']} from any source.")
@@ -0,0 +1,54 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ from tokenizers import Tokenizer, AddedToken
6
+
7
+
8
+ def load_tokenizer(model_dir: Path, max_length: int = 512) -> Tokenizer:
9
+ config_path = model_dir / "config.json"
10
+ if not config_path.exists():
11
+ raise ValueError(f"Could not find config.json in {model_dir}")
12
+
13
+ tokenizer_path = model_dir / "tokenizer.json"
14
+ if not tokenizer_path.exists():
15
+ raise ValueError(f"Could not find tokenizer.json in {model_dir}")
16
+
17
+ tokenizer_config_path = model_dir / "tokenizer_config.json"
18
+ if not tokenizer_config_path.exists():
19
+ raise ValueError(f"Could not find tokenizer_config.json in {model_dir}")
20
+
21
+ tokens_map_path = model_dir / "special_tokens_map.json"
22
+ if not tokens_map_path.exists():
23
+ raise ValueError(f"Could not find special_tokens_map.json in {model_dir}")
24
+
25
+ with open(str(config_path)) as config_file:
26
+ config = json.load(config_file)
27
+
28
+ with open(str(tokenizer_config_path)) as tokenizer_config_file:
29
+ tokenizer_config = json.load(tokenizer_config_file)
30
+
31
+ with open(str(tokens_map_path)) as tokens_map_file:
32
+ tokens_map = json.load(tokens_map_file)
33
+
34
+ tokenizer = Tokenizer.from_file(str(tokenizer_path))
35
+ tokenizer.enable_truncation(max_length=min(tokenizer_config["model_max_length"], max_length))
36
+ tokenizer.enable_padding(
37
+ pad_id=config.get("pad_token_id", 0), pad_token=tokenizer_config["pad_token"]
38
+ )
39
+
40
+ for token in tokens_map.values():
41
+ if isinstance(token, str):
42
+ tokenizer.add_special_tokens([token])
43
+ elif isinstance(token, dict):
44
+ tokenizer.add_special_tokens([AddedToken(**token)])
45
+
46
+ return tokenizer
47
+
48
+
49
+ def normalize(input_array, p=2, dim=1, eps=1e-12) -> np.ndarray:
50
+ # Calculate the Lp norm along the specified dimension
51
+ norm = np.linalg.norm(input_array, ord=p, axis=dim, keepdims=True)
52
+ norm = np.maximum(norm, eps) # Avoid division by zero
53
+ normalized_array = input_array / norm
54
+ return normalized_array
@@ -0,0 +1,165 @@
1
+ import os
2
+ from multiprocessing import get_all_start_methods
3
+ from pathlib import Path
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ Generic,
8
+ Iterable,
9
+ List,
10
+ Optional,
11
+ Tuple,
12
+ Type,
13
+ TypeVar,
14
+ Union,
15
+ Sequence,
16
+ )
17
+
18
+ import numpy as np
19
+ import onnxruntime as ort
20
+
21
+ from fastembed.common.models import load_tokenizer
22
+ from fastembed.common.utils import iter_batch
23
+ from fastembed.parallel_processor import ParallelWorkerPool, Worker
24
+
25
+
26
+ # Holds type of the embedding result
27
+ T = TypeVar("T")
28
+
29
+ OnnxProvider = Union[str, Tuple[str, Dict[Any, Any]]]
30
+
31
+
32
+ class OnnxModel(Generic[T]):
33
+ @classmethod
34
+ def _get_worker_class(cls) -> Type["EmbeddingWorker"]:
35
+ raise NotImplementedError("Subclasses must implement this method")
36
+
37
+ @classmethod
38
+ def _post_process_onnx_output(cls, output: Tuple[np.ndarray, np.ndarray]) -> Iterable[T]:
39
+ raise NotImplementedError("Subclasses must implement this method")
40
+
41
+ def __init__(self) -> None:
42
+ self.model = None
43
+ self.tokenizer = None
44
+
45
+ def _preprocess_onnx_input(self, onnx_input: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
46
+ """
47
+ Preprocess the onnx input.
48
+ """
49
+ return onnx_input
50
+
51
+ def load_onnx_model(
52
+ self,
53
+ model_dir: Path,
54
+ model_file: str,
55
+ threads: Optional[int],
56
+ providers: Optional[Sequence[OnnxProvider]] = None,
57
+ ) -> None:
58
+ model_path = model_dir / model_file
59
+
60
+ # List of Execution Providers: https://onnxruntime.ai/docs/execution-providers
61
+
62
+ onnx_providers = ["CPUExecutionProvider"] if providers is None else list(providers)
63
+ available_providers = ort.get_available_providers()
64
+ for provider in onnx_providers:
65
+ # check providers available
66
+ provider_name = provider if isinstance(provider, str) else provider[0]
67
+ if provider_name not in available_providers:
68
+ raise ValueError(
69
+ f"Provider {provider_name} is not available. Available providers: {available_providers}"
70
+ )
71
+
72
+ so = ort.SessionOptions()
73
+ so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
74
+
75
+ if threads is not None:
76
+ so.intra_op_num_threads = threads
77
+ so.inter_op_num_threads = threads
78
+
79
+ self.tokenizer = load_tokenizer(model_dir=model_dir)
80
+ self.model = ort.InferenceSession(
81
+ str(model_path), providers=onnx_providers, sess_options=so
82
+ )
83
+
84
+ def onnx_embed(self, documents: List[str]) -> Tuple[np.ndarray, np.ndarray]:
85
+ encoded = self.tokenizer.encode_batch(documents)
86
+ input_ids = np.array([e.ids for e in encoded])
87
+ attention_mask = np.array([e.attention_mask for e in encoded])
88
+
89
+ onnx_input = {
90
+ "input_ids": np.array(input_ids, dtype=np.int64),
91
+ "attention_mask": np.array(attention_mask, dtype=np.int64),
92
+ "token_type_ids": np.array(
93
+ [np.zeros(len(e), dtype=np.int64) for e in input_ids], dtype=np.int64
94
+ ),
95
+ }
96
+
97
+ onnx_input = self._preprocess_onnx_input(onnx_input)
98
+
99
+ model_output = self.model.run(None, onnx_input)
100
+ embeddings = model_output[0]
101
+ return embeddings, attention_mask
102
+
103
+ def _embed_documents(
104
+ self,
105
+ model_name: str,
106
+ cache_dir: str,
107
+ documents: Union[str, Iterable[str]],
108
+ batch_size: int = 256,
109
+ parallel: Optional[int] = None,
110
+ ) -> Iterable[T]:
111
+ is_small = False
112
+
113
+ if isinstance(documents, str):
114
+ documents = [documents]
115
+ is_small = True
116
+
117
+ if isinstance(documents, list):
118
+ if len(documents) < batch_size:
119
+ is_small = True
120
+
121
+ if parallel == 0:
122
+ parallel = os.cpu_count()
123
+
124
+ if parallel is None or is_small:
125
+ for batch in iter_batch(documents, batch_size):
126
+ yield from self._post_process_onnx_output(self.onnx_embed(batch))
127
+ else:
128
+ start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
129
+ params = {
130
+ "model_name": model_name,
131
+ "cache_dir": cache_dir,
132
+ }
133
+ pool = ParallelWorkerPool(
134
+ parallel, self._get_worker_class(), start_method=start_method
135
+ )
136
+ for batch in pool.ordered_map(iter_batch(documents, batch_size), **params):
137
+ yield from self._post_process_onnx_output(batch)
138
+
139
+
140
+ class EmbeddingWorker(Worker):
141
+ def init_embedding(
142
+ self,
143
+ model_name: str,
144
+ cache_dir: str,
145
+ ) -> OnnxModel:
146
+ raise NotImplementedError()
147
+
148
+ def __init__(
149
+ self,
150
+ model_name: str,
151
+ cache_dir: str,
152
+ ):
153
+ self.model = self.init_embedding(model_name, cache_dir)
154
+
155
+ @classmethod
156
+ def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "EmbeddingWorker":
157
+ return cls(
158
+ model_name=model_name,
159
+ cache_dir=cache_dir,
160
+ )
161
+
162
+ def process(self, items: Iterable[Tuple[int, Any]]) -> Iterable[Tuple[int, Any]]:
163
+ for idx, batch in items:
164
+ embeddings, attn_mask = self.model.onnx_embed(batch)
165
+ yield idx, (embeddings, attn_mask)
@@ -0,0 +1,33 @@
1
+ import os
2
+ import tempfile
3
+ from itertools import islice
4
+ from pathlib import Path
5
+ from typing import Union, Iterable, Generator, Optional
6
+
7
+
8
+ def iter_batch(iterable: Union[Iterable, Generator], size: int) -> Iterable:
9
+ """
10
+ >>> list(iter_batch([1,2,3,4,5], 3))
11
+ [[1, 2, 3], [4, 5]]
12
+ """
13
+ source_iter = iter(iterable)
14
+ while source_iter:
15
+ b = list(islice(source_iter, size))
16
+ if len(b) == 0:
17
+ break
18
+ yield b
19
+
20
+
21
+ def define_cache_dir(cache_dir: Optional[str] = None) -> Path:
22
+ """
23
+ Define the cache directory for fastembed
24
+ """
25
+ if cache_dir is None:
26
+ default_cache_dir = os.path.join(tempfile.gettempdir(), "fastembed_cache")
27
+ cache_path = Path(os.getenv("FASTEMBED_CACHE_PATH", default_cache_dir))
28
+ else:
29
+ cache_path = Path(cache_dir)
30
+
31
+ cache_path.mkdir(parents=True, exist_ok=True)
32
+
33
+ return cache_path
fastembed/embedding.py ADDED
@@ -0,0 +1,24 @@
1
+ from typing import Optional
2
+
3
+ from loguru import logger
4
+
5
+ from fastembed import TextEmbedding
6
+
7
+ logger.warning(
8
+ "DefaultEmbedding, FlagEmbedding, JinaEmbedding are deprecated."
9
+ "Use from fastembed import TextEmbedding instead."
10
+ )
11
+
12
+ DefaultEmbedding = TextEmbedding
13
+ FlagEmbedding = TextEmbedding
14
+
15
+
16
+ class JinaEmbedding(TextEmbedding):
17
+ def __init__(
18
+ self,
19
+ model_name: str = "jinaai/jina-embeddings-v2-base-en",
20
+ cache_dir: Optional[str] = None,
21
+ threads: Optional[int] = None,
22
+ **kwargs,
23
+ ):
24
+ super().__init__(model_name, cache_dir, threads, **kwargs)
File without changes