fastembed-gpu 0.8.1__tar.gz → 0.9.0__tar.gz
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_gpu-0.8.1 → fastembed_gpu-0.9.0}/NOTICE +2 -2
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/PKG-INFO +3 -3
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/README.md +2 -2
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/common/model_management.py +258 -42
- fastembed_gpu-0.9.0/fastembed/common/onnx_external_data.py +91 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/common/onnx_model.py +28 -4
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/common/preprocessor_utils.py +12 -4
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/common/utils.py +29 -11
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/onnx_embedding.py +3 -2
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/onnx_image_model.py +2 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/transform/functional.py +9 -5
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/transform/operators.py +31 -5
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction/colbert.py +39 -16
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction/late_interaction_text_embedding.py +7 -1
- fastembed_gpu-0.9.0/fastembed/late_interaction/lateon.py +128 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction/token_embeddings.py +1 -1
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction_multimodal/colmodernvbert.py +51 -46
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction_multimodal/colpali.py +3 -3
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction_multimodal/late_interaction_multimodal_embedding.py +11 -2
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction_multimodal/onnx_multimodal_model.py +15 -1
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/parallel_processor.py +32 -3
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/postprocess/muvera.py +23 -9
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/rerank/cross_encoder/onnx_text_cross_encoder.py +19 -1
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/rerank/cross_encoder/onnx_text_model.py +16 -4
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/bm25.py +65 -14
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/bm42.py +16 -8
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/if_splade.py +4 -10
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/minicoil.py +31 -11
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/sparse_embedding_base.py +1 -1
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/sparse_text_embedding.py +1 -1
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/splade_pp.py +2 -1
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/builtin_sentence_embedding.py +15 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/custom_text_embedding.py +23 -2
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/multitask_embedding.py +1 -1
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/onnx_embedding.py +32 -9
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/onnx_text_model.py +16 -4
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/pooled_embedding.py +7 -7
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/pooled_normalized_embedding.py +22 -6
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/siglip_embedding.py +14 -13
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/text_embedding.py +2 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/pyproject.toml +1 -1
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/LICENSE +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/common/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/common/model_description.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/common/types.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/embedding.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/image_embedding.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/image_embedding_base.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/normalized_embedding.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/image/siglip_embedding.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction/jina_colbert.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction/late_interaction_embedding_base.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction_multimodal/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/late_interaction_multimodal/late_interaction_multimodal_embedding_base.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/postprocess/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/py.typed +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/rerank/cross_encoder/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/rerank/cross_encoder/custom_text_cross_encoder.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/rerank/cross_encoder/text_cross_encoder.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/rerank/cross_encoder/text_cross_encoder_base.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/utils/minicoil_encoder.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/utils/sparse_vectors_converter.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/utils/tokenizer.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/sparse/utils/vocab_resolver.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/__init__.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/clip_embedding.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/last_token_normalized_embedding.py +0 -0
- {fastembed_gpu-0.8.1 → fastembed_gpu-0.9.0}/fastembed/text/text_embedding_base.py +0 -0
|
@@ -12,8 +12,8 @@ This distribution includes the following Jina AI models, each with its respectiv
|
|
|
12
12
|
|
|
13
13
|
These models are developed by Jina (https://jina.ai/) and are subject to Jina AI's licensing terms.
|
|
14
14
|
|
|
15
|
-
This distribution includes the following
|
|
16
|
-
-
|
|
15
|
+
This distribution includes the following Gemma-based models, each with its respective license:
|
|
16
|
+
- Qdrant/colpali-v1.3-fp16
|
|
17
17
|
- License: gemma
|
|
18
18
|
- google/embeddinggemma-300m
|
|
19
19
|
- License: gemma
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastembed-gpu
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Fast, light, accurate library built for retrieval embedding generation
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -211,10 +211,10 @@ query = "What is Qdrant?"
|
|
|
211
211
|
model = LateInteractionMultimodalEmbedding(model_name="Qdrant/colpali-v1.3-fp16")
|
|
212
212
|
doc_images_embeddings = list(model.embed_image(doc_images))
|
|
213
213
|
# shape (2, 1030, 128)
|
|
214
|
-
# [array([[-0.
|
|
214
|
+
# [array([[-0.03354, -0.0209, ..., -0.1558, -0.0768]], dtype=float16)]
|
|
215
215
|
query_embedding = model.embed_text(query)
|
|
216
216
|
# shape (1, 20, 128)
|
|
217
|
-
# [array([[-0.
|
|
217
|
+
# [array([[-0.002182, 0.1476, ..., -0.02208, 0.1683]], dtype=float16)]
|
|
218
218
|
```
|
|
219
219
|
|
|
220
220
|
### 🔄 Rerankers
|
|
@@ -169,10 +169,10 @@ query = "What is Qdrant?"
|
|
|
169
169
|
model = LateInteractionMultimodalEmbedding(model_name="Qdrant/colpali-v1.3-fp16")
|
|
170
170
|
doc_images_embeddings = list(model.embed_image(doc_images))
|
|
171
171
|
# shape (2, 1030, 128)
|
|
172
|
-
# [array([[-0.
|
|
172
|
+
# [array([[-0.03354, -0.0209, ..., -0.1558, -0.0768]], dtype=float16)]
|
|
173
173
|
query_embedding = model.embed_text(query)
|
|
174
174
|
# shape (1, 20, 128)
|
|
175
|
-
# [array([[-0.
|
|
175
|
+
# [array([[-0.002182, 0.1476, ..., -0.02208, 0.1683]], dtype=float16)]
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
### 🔄 Rerankers
|
|
@@ -5,15 +5,18 @@ import json
|
|
|
5
5
|
import shutil
|
|
6
6
|
import tarfile
|
|
7
7
|
import tempfile
|
|
8
|
+
import warnings
|
|
8
9
|
import contextlib
|
|
9
10
|
from copy import deepcopy
|
|
10
11
|
from pathlib import Path, PureWindowsPath
|
|
11
12
|
from typing import Any, TypeVar, Generic
|
|
12
13
|
|
|
13
14
|
import requests
|
|
14
|
-
from huggingface_hub import snapshot_download, model_info, list_repo_tree
|
|
15
|
+
from huggingface_hub import constants, snapshot_download, model_info, list_repo_tree
|
|
16
|
+
from huggingface_hub.file_download import repo_folder_name
|
|
15
17
|
from huggingface_hub.hf_api import RepoFile
|
|
16
18
|
from huggingface_hub.utils import (
|
|
19
|
+
HFValidationError,
|
|
17
20
|
RepositoryNotFoundError,
|
|
18
21
|
disable_progress_bars,
|
|
19
22
|
enable_progress_bars,
|
|
@@ -21,12 +24,44 @@ from huggingface_hub.utils import (
|
|
|
21
24
|
from loguru import logger
|
|
22
25
|
from tqdm import tqdm
|
|
23
26
|
from fastembed.common.model_description import BaseModelDescription
|
|
27
|
+
from fastembed.common.onnx_external_data import link_external_data
|
|
24
28
|
|
|
25
29
|
T = TypeVar("T", bound=BaseModelDescription)
|
|
26
30
|
|
|
27
31
|
_DOWNLOAD_CHUNK_SIZE = 256 * 1024
|
|
28
32
|
|
|
29
33
|
|
|
34
|
+
def _hf_transport_errors() -> tuple[type[Exception], ...]:
|
|
35
|
+
"""Network errors of huggingface_hub's HTTP library that aren't OSError.
|
|
36
|
+
|
|
37
|
+
A refused connection, a DNS failure or a timeout raises an OSError in huggingface_hub 0.x,
|
|
38
|
+
which is built on requests, but a TransportError in 1.x (httpx) and 2.x (httpx2).
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
# huggingface_hub>=1.30 re-exports whichever of httpx and httpx2 it's built on.
|
|
42
|
+
from huggingface_hub.utils import httpx
|
|
43
|
+
except ImportError:
|
|
44
|
+
try:
|
|
45
|
+
import httpx # huggingface_hub 1.0 to 1.29
|
|
46
|
+
except ImportError: # huggingface_hub 0.x
|
|
47
|
+
return ()
|
|
48
|
+
return (httpx.TransportError,)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Errors from an HF download that download_model handles by falling back to url and retrying.
|
|
52
|
+
_HF_DOWNLOAD_ERRORS = (OSError, RepositoryNotFoundError, ValueError) + _hf_transport_errors()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CorruptedCacheError(ValueError):
|
|
56
|
+
"""A cached model file is present but does not match the size recorded for it.
|
|
57
|
+
|
|
58
|
+
Distinct from the download failures above so that callers can tell "this snapshot is
|
|
59
|
+
unusable, look elsewhere" apart from "the hub could not be reached": only the former
|
|
60
|
+
is worth re-fetching a file the hub's existence-only cache check would otherwise keep.
|
|
61
|
+
Subclasses ValueError to stay inside _HF_DOWNLOAD_ERRORS for callers that don't care.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
30
65
|
class ModelManagement(Generic[T]):
|
|
31
66
|
METADATA_FILE = "files_metadata.json"
|
|
32
67
|
|
|
@@ -135,6 +170,41 @@ class ModelManagement(Generic[T]):
|
|
|
135
170
|
file.write(chunk)
|
|
136
171
|
return output_path
|
|
137
172
|
|
|
173
|
+
@classmethod
|
|
174
|
+
def _find_legacy_cased_source(cls, cache_dir: str, hf_source_repo: str) -> str | None:
|
|
175
|
+
"""Looks for a cached snapshot of the same repo spelled with a different casing.
|
|
176
|
+
|
|
177
|
+
Built-in sources used to be lowercase and were canonicalized once it turned out that
|
|
178
|
+
relying on the hub's normalizing redirect breaks proxies. Both the hub and fastembed
|
|
179
|
+
derive the cache directory from the source verbatim, so on a case-sensitive filesystem
|
|
180
|
+
an offline load would otherwise miss a model an older version had already cached.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
cache_dir (str): The path to the cache directory.
|
|
184
|
+
hf_source_repo (str): Name of the model on HuggingFace Hub.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
str | None: The differently cased source found in the cache, None if there is none.
|
|
188
|
+
"""
|
|
189
|
+
separator = constants.REPO_ID_SEPARATOR
|
|
190
|
+
try:
|
|
191
|
+
expected = repo_folder_name(repo_id=hf_source_repo, repo_type="model")
|
|
192
|
+
entries = list(Path(cache_dir).iterdir())
|
|
193
|
+
except (HFValidationError, OSError):
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
# the repo id sits at the tail of the folder name, with every "/" replaced
|
|
197
|
+
offset = len(expected) - len(hf_source_repo.replace("/", separator))
|
|
198
|
+
|
|
199
|
+
for entry in entries:
|
|
200
|
+
if entry.name == expected or entry.name.lower() != expected.lower():
|
|
201
|
+
continue
|
|
202
|
+
if not entry.is_dir():
|
|
203
|
+
continue
|
|
204
|
+
# the hub forbids the separator inside a repo id, so it only marks the split
|
|
205
|
+
return entry.name[offset:].replace(separator, "/")
|
|
206
|
+
return None
|
|
207
|
+
|
|
138
208
|
@classmethod
|
|
139
209
|
def download_files_from_huggingface(
|
|
140
210
|
cls,
|
|
@@ -147,7 +217,7 @@ class ModelManagement(Generic[T]):
|
|
|
147
217
|
"""
|
|
148
218
|
Downloads a model from HuggingFace Hub.
|
|
149
219
|
Args:
|
|
150
|
-
hf_source_repo (str): Name of the model on HuggingFace Hub, e.g. "
|
|
220
|
+
hf_source_repo (str): Name of the model on HuggingFace Hub, e.g. "Qdrant/all-MiniLM-L6-v2-onnx".
|
|
151
221
|
cache_dir (Optional[str]): The path to the cache directory.
|
|
152
222
|
extra_patterns (list[str]): extra patterns to allow in the snapshot download, typically
|
|
153
223
|
includes the required model files.
|
|
@@ -156,6 +226,15 @@ class ModelManagement(Generic[T]):
|
|
|
156
226
|
Path: The path to the model directory.
|
|
157
227
|
"""
|
|
158
228
|
|
|
229
|
+
def _repo_relative_path(relative_path: Path) -> str:
|
|
230
|
+
# Cached files are stored under `snapshots/<revision>/<repo_path>`. Strip that
|
|
231
|
+
# prefix so the remainder matches a repo file's path, which may itself be nested
|
|
232
|
+
# (e.g. `onnx/model.onnx`).
|
|
233
|
+
parts = relative_path.parts
|
|
234
|
+
if len(parts) > 2 and parts[0] == "snapshots":
|
|
235
|
+
return "/".join(parts[2:])
|
|
236
|
+
return relative_path.as_posix()
|
|
237
|
+
|
|
159
238
|
def _verify_files_from_metadata(
|
|
160
239
|
model_dir: Path, stored_metadata: dict[str, Any], repo_files: list[RepoFile]
|
|
161
240
|
) -> bool:
|
|
@@ -167,7 +246,8 @@ class ModelManagement(Generic[T]):
|
|
|
167
246
|
return False
|
|
168
247
|
|
|
169
248
|
if repo_files: # online verification
|
|
170
|
-
|
|
249
|
+
repo_path = _repo_relative_path(Path(rel_path))
|
|
250
|
+
file_info = next((f for f in repo_files if f.path == repo_path), None)
|
|
171
251
|
if (
|
|
172
252
|
not file_info
|
|
173
253
|
or file_info.size != meta["size"]
|
|
@@ -184,15 +264,19 @@ class ModelManagement(Generic[T]):
|
|
|
184
264
|
return False
|
|
185
265
|
|
|
186
266
|
def _collect_file_metadata(
|
|
187
|
-
model_dir: Path, repo_files: list[RepoFile]
|
|
267
|
+
model_dir: Path, revision: str, repo_files: list[RepoFile]
|
|
188
268
|
) -> dict[str, dict[str, int | str]]:
|
|
189
269
|
meta: dict[str, dict[str, int | str]] = {}
|
|
190
270
|
file_info_map = {f.path: f for f in repo_files}
|
|
191
|
-
|
|
271
|
+
# Only look at the downloaded revision's folder. The cache may also hold folders of
|
|
272
|
+
# older revisions, whose files differ from the ones on the hub now, so checking
|
|
273
|
+
# them would report a good download as corrupted.
|
|
274
|
+
for file_path in (model_dir / "snapshots" / revision).rglob("*"):
|
|
192
275
|
if file_path.is_file() and file_path.name != cls.METADATA_FILE:
|
|
193
|
-
|
|
276
|
+
relative_path = file_path.relative_to(model_dir)
|
|
277
|
+
repo_file = file_info_map.get(_repo_relative_path(relative_path))
|
|
194
278
|
if repo_file:
|
|
195
|
-
meta[str(
|
|
279
|
+
meta[str(relative_path)] = {
|
|
196
280
|
"size": repo_file.size,
|
|
197
281
|
"blob_id": repo_file.blob_id,
|
|
198
282
|
}
|
|
@@ -216,31 +300,96 @@ class ModelManagement(Generic[T]):
|
|
|
216
300
|
|
|
217
301
|
allow_patterns.extend(extra_patterns)
|
|
218
302
|
|
|
219
|
-
snapshot_dir = Path(cache_dir) /
|
|
303
|
+
snapshot_dir = Path(cache_dir) / repo_folder_name(
|
|
304
|
+
repo_id=hf_source_repo, repo_type="model"
|
|
305
|
+
)
|
|
220
306
|
metadata_file = snapshot_dir / cls.METADATA_FILE
|
|
221
307
|
|
|
222
308
|
if local_files_only:
|
|
223
309
|
disable_progress_bars()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)
|
|
238
|
-
|
|
310
|
+
# a canonical directory can exist yet hold no usable snapshot, e.g. when an
|
|
311
|
+
# earlier download was interrupted, so fall back on failure rather than on
|
|
312
|
+
# the directory being absent
|
|
313
|
+
sources = [hf_source_repo]
|
|
314
|
+
legacy_source = cls._find_legacy_cased_source(cache_dir, hf_source_repo)
|
|
315
|
+
if legacy_source is not None:
|
|
316
|
+
sources.append(legacy_source)
|
|
317
|
+
|
|
318
|
+
# a corrupt candidate is remembered rather than raised on the spot: the next
|
|
319
|
+
# candidate may still be loadable, and if none is, this is the error worth
|
|
320
|
+
# reporting, since it is the only one download_model can act on.
|
|
321
|
+
corruption: CorruptedCacheError | None = None
|
|
322
|
+
|
|
323
|
+
for index, source in enumerate(sources):
|
|
324
|
+
last_source = index == len(sources) - 1
|
|
325
|
+
snapshot_dir = Path(cache_dir) / repo_folder_name(
|
|
326
|
+
repo_id=source, repo_type="model"
|
|
327
|
+
)
|
|
328
|
+
metadata_file = snapshot_dir / cls.METADATA_FILE
|
|
329
|
+
if metadata_file.exists():
|
|
330
|
+
metadata = json.loads(metadata_file.read_text())
|
|
331
|
+
if not _verify_files_from_metadata(snapshot_dir, metadata, repo_files=[]):
|
|
332
|
+
# A size mismatch on one of this model's own files (its weights, or
|
|
333
|
+
# whatever else its description lists) means loading would later fail
|
|
334
|
+
# with a cryptic error, so the snapshot is not offered at all. A
|
|
335
|
+
# mismatch confined to a file nothing asked for is left to best-effort
|
|
336
|
+
# loading, preserving the previous behavior.
|
|
337
|
+
model_files = set(extra_patterns)
|
|
338
|
+
model_file_corrupt = any(
|
|
339
|
+
_repo_relative_path(Path(rel_path)) in model_files
|
|
340
|
+
and (snapshot_dir / rel_path).is_file()
|
|
341
|
+
and (snapshot_dir / rel_path).stat().st_size != meta["size"]
|
|
342
|
+
for rel_path, meta in metadata.items()
|
|
343
|
+
)
|
|
344
|
+
if model_file_corrupt:
|
|
345
|
+
corruption = corruption or CorruptedCacheError(
|
|
346
|
+
f"Cached model files for {source} in {cache_dir} do not match "
|
|
347
|
+
"the stored metadata; the cache appears corrupted."
|
|
348
|
+
)
|
|
349
|
+
if last_source:
|
|
350
|
+
raise corruption
|
|
351
|
+
logger.info(
|
|
352
|
+
f"{source} is corrupted in {cache_dir}, loading "
|
|
353
|
+
f"{sources[index + 1]}, cached from the same repo by an older "
|
|
354
|
+
"fastembed version."
|
|
355
|
+
)
|
|
356
|
+
continue
|
|
357
|
+
logger.warning("Local file sizes do not match the metadata.")
|
|
358
|
+
try:
|
|
359
|
+
# a legacy source is only ever resolved against the cache: sending it
|
|
360
|
+
# to the hub would ask for the very redirect this casing avoids
|
|
361
|
+
return snapshot_download(
|
|
362
|
+
repo_id=source,
|
|
363
|
+
allow_patterns=allow_patterns,
|
|
364
|
+
cache_dir=cache_dir,
|
|
365
|
+
local_files_only=True,
|
|
366
|
+
**kwargs,
|
|
367
|
+
)
|
|
368
|
+
except _HF_DOWNLOAD_ERRORS:
|
|
369
|
+
if last_source:
|
|
370
|
+
# an earlier candidate was corrupt and this one is merely absent:
|
|
371
|
+
# report the corruption, the only failure a re-download can fix
|
|
372
|
+
if corruption is not None:
|
|
373
|
+
raise corruption
|
|
374
|
+
raise
|
|
375
|
+
logger.info(
|
|
376
|
+
f"{source} is not usable in {cache_dir}, loading "
|
|
377
|
+
f"{sources[index + 1]}, cached from the same repo by an older "
|
|
378
|
+
"fastembed version."
|
|
379
|
+
)
|
|
239
380
|
|
|
240
|
-
|
|
241
|
-
|
|
381
|
+
# hub sends this request with no timeout unless given one, so an endpoint that accepts
|
|
382
|
+
# the connection but never answers would block here for good, before download_model can
|
|
383
|
+
# retry or fall back to another source. list_repo_tree and snapshot_download's own
|
|
384
|
+
# metadata requests can't be given one, but a silent endpoint now fails here first.
|
|
385
|
+
repo_revision = model_info(hf_source_repo, timeout=constants.HF_HUB_ETAG_TIMEOUT).sha
|
|
386
|
+
repo_tree = list(
|
|
387
|
+
list_repo_tree(
|
|
388
|
+
hf_source_repo, revision=repo_revision, repo_type="model", recursive=True
|
|
389
|
+
)
|
|
390
|
+
)
|
|
242
391
|
|
|
243
|
-
allowed_extensions = {
|
|
392
|
+
allowed_extensions = {Path(pattern).suffix for pattern in allow_patterns}
|
|
244
393
|
repo_files = (
|
|
245
394
|
[
|
|
246
395
|
f
|
|
@@ -253,9 +402,18 @@ class ModelManagement(Generic[T]):
|
|
|
253
402
|
|
|
254
403
|
verified_metadata = False
|
|
255
404
|
|
|
256
|
-
|
|
405
|
+
# a forced download re-fetches every file, so show progress and re-collect the metadata
|
|
406
|
+
if not kwargs.get("force_download") and snapshot_dir.exists() and metadata_file.exists():
|
|
257
407
|
metadata = json.loads(metadata_file.read_text())
|
|
258
|
-
|
|
408
|
+
# metadata lacking a requested file, e.g. a new model_file, can't vouch for it
|
|
409
|
+
requested_files = {f.path for f in repo_files if f.path in allow_patterns}
|
|
410
|
+
stored_files = {_repo_relative_path(Path(rel_path)) for rel_path in metadata}
|
|
411
|
+
# empty metadata lists no files, so it can't vouch for the cached ones
|
|
412
|
+
verified_metadata = (
|
|
413
|
+
bool(metadata)
|
|
414
|
+
and requested_files.issubset(stored_files)
|
|
415
|
+
and _verify_files_from_metadata(snapshot_dir, metadata, repo_files)
|
|
416
|
+
)
|
|
259
417
|
|
|
260
418
|
if verified_metadata:
|
|
261
419
|
disable_progress_bars()
|
|
@@ -272,7 +430,10 @@ class ModelManagement(Generic[T]):
|
|
|
272
430
|
not verified_metadata
|
|
273
431
|
): # metadata is not up-to-date, update it and check whether the files have been
|
|
274
432
|
# downloaded correctly
|
|
275
|
-
|
|
433
|
+
# result is <cache_dir>/.../snapshots/<revision>. Only the revision is taken from
|
|
434
|
+
# it: the hub resolves cache_dir, so the rest of the path may not match ours.
|
|
435
|
+
downloaded_revision = Path(result).name
|
|
436
|
+
metadata = _collect_file_metadata(snapshot_dir, downloaded_revision, repo_files)
|
|
276
437
|
|
|
277
438
|
download_successful = _verify_files_from_metadata(
|
|
278
439
|
snapshot_dir, metadata, repo_files=[]
|
|
@@ -282,7 +443,10 @@ class ModelManagement(Generic[T]):
|
|
|
282
443
|
"Files have been corrupted during downloading process. "
|
|
283
444
|
"Please check your internet connection and try again."
|
|
284
445
|
)
|
|
285
|
-
|
|
446
|
+
# Empty means no file was checked, e.g. the files went to a local_dir instead of
|
|
447
|
+
# the cache. Saving it would make later downloads skip this check.
|
|
448
|
+
if metadata:
|
|
449
|
+
_save_file_metadata(snapshot_dir, metadata)
|
|
286
450
|
|
|
287
451
|
return result
|
|
288
452
|
|
|
@@ -374,6 +538,20 @@ class ModelManagement(Generic[T]):
|
|
|
374
538
|
|
|
375
539
|
# check if the model_dir and the model files are both present for macOS
|
|
376
540
|
if model_dir.exists() and len(list(model_dir.glob("*"))) > 0:
|
|
541
|
+
if deprecated_tar_struct and not source_url:
|
|
542
|
+
# No built-in model is served from the bucket anymore, only copies of it remain.
|
|
543
|
+
# A model with a url of its own (a custom one) got its copy from there instead.
|
|
544
|
+
# stacklevel points at the caller of TextEmbedding(...) via download_model.
|
|
545
|
+
warnings.warn(
|
|
546
|
+
f"Loading {model_name} from {model_dir}, a copy downloaded from Google Cloud "
|
|
547
|
+
"Storage by an older fastembed version. Support for such copies is deprecated "
|
|
548
|
+
"and will be removed in a future release. To switch to Hugging Face, load the "
|
|
549
|
+
"model once while huggingface.co (or an `HF_ENDPOINT` mirror) is reachable, "
|
|
550
|
+
"without `local_files_only=True` or `HF_HUB_OFFLINE=1`, then delete "
|
|
551
|
+
f"{model_dir}.",
|
|
552
|
+
FutureWarning,
|
|
553
|
+
stacklevel=5,
|
|
554
|
+
)
|
|
377
555
|
return model_dir
|
|
378
556
|
|
|
379
557
|
if local_files_only:
|
|
@@ -425,6 +603,19 @@ class ModelManagement(Generic[T]):
|
|
|
425
603
|
|
|
426
604
|
return model_dir
|
|
427
605
|
|
|
606
|
+
@staticmethod
|
|
607
|
+
def _link_onnx_external_data(model: BaseModelDescription, model_dir: Path) -> Path:
|
|
608
|
+
"""Links the external data of `model` right after its download, see link_external_data.
|
|
609
|
+
|
|
610
|
+
Loading the model links it as well, but linking it here also covers lazy_load=True, e.g.
|
|
611
|
+
in a Docker build step, where links made by a later step would copy the files into a new
|
|
612
|
+
image layer. A failure, e.g. in a read-only cache, is reported when the model is loaded.
|
|
613
|
+
"""
|
|
614
|
+
if model.model_file.endswith(".onnx"):
|
|
615
|
+
with contextlib.suppress(OSError):
|
|
616
|
+
link_external_data(model_dir, model.model_file, model.additional_files)
|
|
617
|
+
return model_dir
|
|
618
|
+
|
|
428
619
|
@classmethod
|
|
429
620
|
def download_model(cls, model: T, cache_dir: str, retries: int = 3, **kwargs: Any) -> Path:
|
|
430
621
|
"""
|
|
@@ -440,8 +631,7 @@ class ModelManagement(Generic[T]):
|
|
|
440
631
|
"description": "Base English model, v1.5",
|
|
441
632
|
"size_in_GB": 0.44,
|
|
442
633
|
"sources": {
|
|
443
|
-
"
|
|
444
|
-
"hf": "qdrant/bge-base-en-v1.5-onnx-q",
|
|
634
|
+
"hf": "Qdrant/bge-base-en-v1.5-onnx-Q",
|
|
445
635
|
}
|
|
446
636
|
}
|
|
447
637
|
```
|
|
@@ -466,6 +656,12 @@ class ModelManagement(Generic[T]):
|
|
|
466
656
|
extra_patterns = [model.model_file]
|
|
467
657
|
extra_patterns.extend(model.additional_files)
|
|
468
658
|
|
|
659
|
+
# Only a cached file that is present but does not match its recorded size needs
|
|
660
|
+
# force_download: the hub's cache check is existence-only, so a plain snapshot_download
|
|
661
|
+
# would keep the truncated blob. Every other probe failure (a missing file, an absent
|
|
662
|
+
# cache) is served by an ordinary download, which re-uses what is already on disk.
|
|
663
|
+
force_download = False
|
|
664
|
+
|
|
469
665
|
if hf_source:
|
|
470
666
|
try:
|
|
471
667
|
cache_kwargs = deepcopy(kwargs)
|
|
@@ -481,7 +677,9 @@ class ModelManagement(Generic[T]):
|
|
|
481
677
|
if (resolved_path / model.model_file).exists() and all(
|
|
482
678
|
(resolved_path / file).exists() for file in extra_patterns
|
|
483
679
|
):
|
|
484
|
-
return resolved_path
|
|
680
|
+
return cls._link_onnx_external_data(model, resolved_path)
|
|
681
|
+
except CorruptedCacheError:
|
|
682
|
+
force_download = True
|
|
485
683
|
except Exception:
|
|
486
684
|
pass
|
|
487
685
|
finally:
|
|
@@ -493,28 +691,35 @@ class ModelManagement(Generic[T]):
|
|
|
493
691
|
|
|
494
692
|
if hf_source and not local_files_only:
|
|
495
693
|
# we have already tried loading with `local_files_only=True` via hf and we failed
|
|
694
|
+
|
|
695
|
+
# Merged into kwargs so a caller-supplied force_download can't raise a
|
|
696
|
+
# duplicate keyword error. Cleared straight away: force_download deletes the
|
|
697
|
+
# partially fetched `.incomplete` file, so leaving it on would make every
|
|
698
|
+
# retry restart a large download from zero instead of resuming it.
|
|
699
|
+
attempt_kwargs = {**kwargs, "force_download": True} if force_download else kwargs
|
|
700
|
+
force_download = False
|
|
496
701
|
try:
|
|
497
|
-
|
|
702
|
+
model_dir = Path(
|
|
498
703
|
cls.download_files_from_huggingface(
|
|
499
704
|
hf_source,
|
|
500
705
|
cache_dir=cache_dir,
|
|
501
706
|
extra_patterns=extra_patterns,
|
|
502
|
-
**
|
|
707
|
+
**attempt_kwargs,
|
|
503
708
|
)
|
|
504
709
|
)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
710
|
+
return cls._link_onnx_external_data(model, model_dir)
|
|
711
|
+
except _HF_DOWNLOAD_ERRORS as e:
|
|
712
|
+
logger.error(
|
|
713
|
+
f"Could not download model from HuggingFace: {e} "
|
|
714
|
+
"Falling back to other sources."
|
|
715
|
+
)
|
|
511
716
|
finally:
|
|
512
717
|
enable_progress_bars()
|
|
513
718
|
if url_source or local_files_only:
|
|
514
719
|
try:
|
|
515
720
|
return cls.retrieve_model_gcs(
|
|
516
721
|
model.model,
|
|
517
|
-
|
|
722
|
+
url_source or "",
|
|
518
723
|
str(cache_dir),
|
|
519
724
|
deprecated_tar_struct=model.sources.deprecated_tar_struct,
|
|
520
725
|
local_files_only=local_files_only,
|
|
@@ -522,6 +727,17 @@ class ModelManagement(Generic[T]):
|
|
|
522
727
|
except Exception:
|
|
523
728
|
if not local_files_only:
|
|
524
729
|
logger.error(f"Could not download model from url: {url_source}")
|
|
730
|
+
elif model.sources.deprecated_tar_struct:
|
|
731
|
+
# Nothing is downloaded from the bucket anymore, but an old copy may still be cached.
|
|
732
|
+
legacy_dir = Path(cache_dir) / f"fast-{model.model.split('/')[-1]}"
|
|
733
|
+
if legacy_dir.is_dir() and any(legacy_dir.iterdir()):
|
|
734
|
+
return cls.retrieve_model_gcs(
|
|
735
|
+
model.model,
|
|
736
|
+
"",
|
|
737
|
+
str(cache_dir),
|
|
738
|
+
deprecated_tar_struct=True,
|
|
739
|
+
local_files_only=True,
|
|
740
|
+
)
|
|
525
741
|
|
|
526
742
|
if local_files_only:
|
|
527
743
|
logger.error("Could not find model in cache_dir")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Loading ONNX models with external data from a huggingface_hub cache.
|
|
2
|
+
|
|
3
|
+
onnxruntime>=1.24 refuses external data that, once symlinks are resolved, is located outside of
|
|
4
|
+
the directory of the model file, and of the directory the model file resolves to (a fallback of
|
|
5
|
+
1.24.2, pyproject.toml excludes 1.24.0 and 1.24.1). Since huggingface_hub 1.32, a snapshot is made
|
|
6
|
+
of symlinks into a blob store shared by the whole cache and sharded by hash, where a model and its
|
|
7
|
+
data may resolve into different directories. Then the model and its data are hardlinked into
|
|
8
|
+
`ONNX_SNAPSHOTS_DIR` of the repo cache, laid out as in the snapshot, which takes no extra space.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# Next to `snapshots` in the cache of a huggingface_hub repo: `onnx_snapshots/<revision>/...`
|
|
17
|
+
ONNX_SNAPSHOTS_DIR = "onnx_snapshots"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def link_external_data(model_dir: Path, model_file: str, additional_files: list[str]) -> Path:
|
|
21
|
+
"""Returns a path of `model_file` from which onnxruntime can load its external data.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
model_dir (Path): The directory with the model files, e.g. a huggingface_hub snapshot.
|
|
25
|
+
model_file (str): The path of the ONNX file, relative to `model_dir`.
|
|
26
|
+
additional_files (list[str]): Other files of the model, relative to `model_dir`,
|
|
27
|
+
among which its external data.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path: The ONNX file to load, `model_dir / model_file` unless it had to be linked.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
OSError: If the files had to be linked, but couldn't be, e.g. in a read-only cache or
|
|
34
|
+
on a filesystem without hardlinks.
|
|
35
|
+
"""
|
|
36
|
+
model_path = model_dir / model_file
|
|
37
|
+
snapshots_dir = model_dir.parent
|
|
38
|
+
repo_dir = snapshots_dir.parent
|
|
39
|
+
if snapshots_dir.name != "snapshots" or not repo_dir.name.startswith("models--"):
|
|
40
|
+
return model_path # not a huggingface_hub cache, there is no place of ours to link into
|
|
41
|
+
if not model_path.exists():
|
|
42
|
+
return model_path # onnxruntime reports it more clearly than a failed hardlink would
|
|
43
|
+
|
|
44
|
+
# external data is located relative to the model file, so it can't be anywhere else
|
|
45
|
+
data_paths = [
|
|
46
|
+
path
|
|
47
|
+
for path in (model_dir / file for file in additional_files)
|
|
48
|
+
if path.parent.is_relative_to(model_path.parent) and path.exists()
|
|
49
|
+
]
|
|
50
|
+
# onnxruntime accepts data within these, as in the caches of older huggingface_hub versions
|
|
51
|
+
real_model_dirs = (
|
|
52
|
+
os.path.realpath(model_path.parent),
|
|
53
|
+
os.path.dirname(os.path.realpath(model_path)),
|
|
54
|
+
)
|
|
55
|
+
if all(
|
|
56
|
+
any(Path(os.path.realpath(path)).is_relative_to(d) for d in real_model_dirs)
|
|
57
|
+
for path in data_paths
|
|
58
|
+
):
|
|
59
|
+
return model_path
|
|
60
|
+
|
|
61
|
+
links_dir = repo_dir / ONNX_SNAPSHOTS_DIR
|
|
62
|
+
for path in (model_path, *data_paths):
|
|
63
|
+
_link_file(path, links_dir / model_dir.name / path.relative_to(model_dir))
|
|
64
|
+
|
|
65
|
+
# the links keep the blobs on disk, so drop those of revisions deleted from the cache
|
|
66
|
+
for revision_dir in links_dir.iterdir():
|
|
67
|
+
if not (snapshots_dir / revision_dir.name).is_dir():
|
|
68
|
+
shutil.rmtree(revision_dir, ignore_errors=True)
|
|
69
|
+
|
|
70
|
+
return links_dir / model_dir.name / model_file
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _link_file(source: Path, link: Path) -> None:
|
|
74
|
+
"""Makes `link` a hardlink to the file `source` resolves to, unless it already is one."""
|
|
75
|
+
target = os.path.realpath(source)
|
|
76
|
+
try:
|
|
77
|
+
if os.path.samefile(link, target):
|
|
78
|
+
return
|
|
79
|
+
# a link to a blob downloaded again since, or a copy of the cache without its hardlinks
|
|
80
|
+
link.unlink()
|
|
81
|
+
except FileNotFoundError:
|
|
82
|
+
pass
|
|
83
|
+
except OSError:
|
|
84
|
+
# e.g. such a copy on a read-only filesystem, which works all the same
|
|
85
|
+
if os.path.getsize(link) != os.path.getsize(target):
|
|
86
|
+
raise
|
|
87
|
+
return
|
|
88
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
# os.link is atomic, so if the link exists, another process loading the model just made it
|
|
90
|
+
with contextlib.suppress(FileExistsError):
|
|
91
|
+
os.link(target, link)
|
|
@@ -6,9 +6,11 @@ from typing import Any, Generic, Iterable, Sequence, Type, TypeVar
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import onnxruntime as ort
|
|
8
8
|
|
|
9
|
+
from loguru import logger
|
|
9
10
|
from numpy.typing import NDArray
|
|
10
11
|
from tokenizers import Tokenizer
|
|
11
12
|
|
|
13
|
+
from fastembed.common.onnx_external_data import ONNX_SNAPSHOTS_DIR, link_external_data
|
|
12
14
|
from fastembed.common.types import OnnxProvider, NumpyArray, Device
|
|
13
15
|
from fastembed.parallel_processor import Worker
|
|
14
16
|
|
|
@@ -26,6 +28,8 @@ class OnnxOutputContext:
|
|
|
26
28
|
|
|
27
29
|
class OnnxModel(Generic[T]):
|
|
28
30
|
EXPOSED_SESSION_OPTIONS = ("enable_cpu_mem_arena",)
|
|
31
|
+
# set by the concrete model classes in their __init__
|
|
32
|
+
_model_dir: Path
|
|
29
33
|
|
|
30
34
|
@classmethod
|
|
31
35
|
def _get_worker_class(cls) -> Type["EmbeddingWorker[T]"]:
|
|
@@ -76,8 +80,8 @@ class OnnxModel(Generic[T]):
|
|
|
76
80
|
cuda: bool | Device = Device.AUTO,
|
|
77
81
|
device_id: int | None = None,
|
|
78
82
|
extra_session_options: dict[str, Any] | None = None,
|
|
83
|
+
additional_files: list[str] | None = None,
|
|
79
84
|
) -> None:
|
|
80
|
-
model_path = model_dir / model_file
|
|
81
85
|
# List of Execution Providers: https://onnxruntime.ai/docs/execution-providers
|
|
82
86
|
available_providers = ort.get_available_providers()
|
|
83
87
|
cuda_available = "CUDAExecutionProvider" in available_providers
|
|
@@ -122,9 +126,29 @@ class OnnxModel(Generic[T]):
|
|
|
122
126
|
if extra_session_options is not None:
|
|
123
127
|
self.add_extra_session_options(so, extra_session_options)
|
|
124
128
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
model_path = model_dir / model_file
|
|
130
|
+
link_error: OSError | None = None
|
|
131
|
+
try:
|
|
132
|
+
model_path = link_external_data(model_dir, model_file, additional_files or [])
|
|
133
|
+
except OSError as e:
|
|
134
|
+
# e.g. a read-only cache, which onnxruntime<1.24 loads from all the same
|
|
135
|
+
link_error = e
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
self.model = ort.InferenceSession(
|
|
139
|
+
str(model_path), providers=onnx_providers, sess_options=so
|
|
140
|
+
)
|
|
141
|
+
except Exception:
|
|
142
|
+
if link_error is not None:
|
|
143
|
+
logger.warning(
|
|
144
|
+
f"Could not link the files of {model_path} into {ONNX_SNAPSHOTS_DIR}: "
|
|
145
|
+
f"{link_error}. onnxruntime>=1.24 refuses external data that resolves "
|
|
146
|
+
"outside of the model directory, as in a huggingface_hub>=1.32 cache. "
|
|
147
|
+
"Download the model with fastembed into a writable cache_dir on a "
|
|
148
|
+
"filesystem with hardlinks, or delete it from the cache and download it "
|
|
149
|
+
"again with HF_HUB_DISABLE_SHARED_BLOBS=1."
|
|
150
|
+
)
|
|
151
|
+
raise
|
|
128
152
|
if "CUDAExecutionProvider" in requested_provider_names:
|
|
129
153
|
assert self.model is not None
|
|
130
154
|
current_providers = self.model.get_providers()
|