Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 80 additions & 18 deletions fastembed/common/model_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

import requests
from huggingface_hub import constants, snapshot_download, model_info, list_repo_tree
from huggingface_hub.file_download import repo_folder_name
from huggingface_hub.hf_api import RepoFile
from huggingface_hub.utils import (
HFValidationError,
RepositoryNotFoundError,
disable_progress_bars,
enable_progress_bars,
Expand Down Expand Up @@ -157,6 +159,41 @@ def download_file_from_gcs(cls, url: str, output_path: str, show_progress: bool
file.write(chunk)
return output_path

@classmethod
def _find_legacy_cased_source(cls, cache_dir: str, hf_source_repo: str) -> str | None:
"""Looks for a cached snapshot of the same repo spelled with a different casing.

Built-in sources used to be lowercase and were canonicalized once it turned out that
relying on the hub's normalizing redirect breaks proxies. Both the hub and fastembed
derive the cache directory from the source verbatim, so on a case-sensitive filesystem
an offline load would otherwise miss a model an older version had already cached.

Args:
cache_dir (str): The path to the cache directory.
hf_source_repo (str): Name of the model on HuggingFace Hub.

Returns:
str | None: The differently cased source found in the cache, None if there is none.
"""
separator = constants.REPO_ID_SEPARATOR
try:
expected = repo_folder_name(repo_id=hf_source_repo, repo_type="model")
entries = list(Path(cache_dir).iterdir())
except (HFValidationError, OSError):
return None

# the repo id sits at the tail of the folder name, with every "/" replaced
offset = len(expected) - len(hf_source_repo.replace("/", separator))

for entry in entries:
if entry.name == expected or entry.name.lower() != expected.lower():
continue
if not entry.is_dir():
continue
# the hub forbids the separator inside a repo id, so it only marks the split
return entry.name[offset:].replace(separator, "/")
return None

@classmethod
def download_files_from_huggingface(
cls,
Expand All @@ -169,7 +206,7 @@ def download_files_from_huggingface(
"""
Downloads a model from HuggingFace Hub.
Args:
hf_source_repo (str): Name of the model on HuggingFace Hub, e.g. "qdrant/all-MiniLM-L6-v2-onnx".
hf_source_repo (str): Name of the model on HuggingFace Hub, e.g. "Qdrant/all-MiniLM-L6-v2-onnx".
cache_dir (Optional[str]): The path to the cache directory.
extra_patterns (list[str]): extra patterns to allow in the snapshot download, typically
includes the required model files.
Expand Down Expand Up @@ -238,26 +275,51 @@ def _save_file_metadata(model_dir: Path, meta: dict[str, dict[str, int | str]])

allow_patterns.extend(extra_patterns)

snapshot_dir = Path(cache_dir) / f"models--{hf_source_repo.replace('/', '--')}"
snapshot_dir = Path(cache_dir) / repo_folder_name(
repo_id=hf_source_repo, repo_type="model"
)
metadata_file = snapshot_dir / cls.METADATA_FILE

if local_files_only:
disable_progress_bars()
if metadata_file.exists():
metadata = json.loads(metadata_file.read_text())
verified = _verify_files_from_metadata(snapshot_dir, metadata, repo_files=[])
if not verified:
logger.warning(
"Local file sizes do not match the metadata."
) # do not raise, still make an attempt to load the model
result = snapshot_download(
repo_id=hf_source_repo,
allow_patterns=allow_patterns,
cache_dir=cache_dir,
local_files_only=local_files_only,
**kwargs,
)
return result
# a canonical directory can exist yet hold no usable snapshot, e.g. when an
# earlier download was interrupted, so fall back on failure rather than on
# the directory being absent
sources = [hf_source_repo]
legacy_source = cls._find_legacy_cased_source(cache_dir, hf_source_repo)
if legacy_source is not None:
sources.append(legacy_source)

for index, source in enumerate(sources):
snapshot_dir = Path(cache_dir) / repo_folder_name(
repo_id=source, repo_type="model"
)
metadata_file = snapshot_dir / cls.METADATA_FILE
if metadata_file.exists():
metadata = json.loads(metadata_file.read_text())
verified = _verify_files_from_metadata(snapshot_dir, metadata, repo_files=[])
if not verified:
logger.warning(
"Local file sizes do not match the metadata."
) # do not raise, still make an attempt to load the model
try:
# a legacy source is only ever resolved against the cache: sending it
# to the hub would ask for the very redirect this casing avoids
return snapshot_download(
repo_id=source,
allow_patterns=allow_patterns,
cache_dir=cache_dir,
local_files_only=True,
**kwargs,
)
except _HF_DOWNLOAD_ERRORS:
if index == len(sources) - 1:
raise
logger.info(
f"{source} is not usable in {cache_dir}, loading "
f"{sources[index + 1]}, cached from the same repo by an older "
"fastembed version."
)

# hub sends this request with no timeout unless given one, so an endpoint that accepts
# the connection but never answers would block here for good, before download_model can
Expand Down Expand Up @@ -480,7 +542,7 @@ def download_model(cls, model: T, cache_dir: str, retries: int = 3, **kwargs: An
"description": "Base English model, v1.5",
"size_in_GB": 0.44,
"sources": {
"hf": "qdrant/bge-base-en-v1.5-onnx-q",
"hf": "Qdrant/bge-base-en-v1.5-onnx-Q",
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion fastembed/late_interaction/token_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
" Prefixes for queries/documents: not necessary, 2023 year.",
license="apache-2.0",
size_in_GB=0.12,
sources=ModelSource(hf="xenova/jina-embeddings-v2-small-en"),
sources=ModelSource(hf="Xenova/jina-embeddings-v2-small-en"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve access to existing caches when repository casing changes.

The downloader and Hugging Face Hub derive cache paths from the supplied repository ID, preserving its casing. These updates therefore make prior lowercase cache directories invisible to local_files_only=True, so offline users can fail to load models they already cached. Add a fallback or migration for the previous cache keys. (raw.githubusercontent.com)

  • fastembed/late_interaction/token_embeddings.py#L22-L22: support the previous lowercase xenova/... cache key.
  • fastembed/text/onnx_embedding.py#L50-L50: support the previous lowercase qdrant/... cache key.
  • fastembed/text/onnx_embedding.py#L116-L116: support the previous lowercase snowflake/... cache key.
  • fastembed/text/onnx_embedding.py#L128-L128: support the previous lowercase snowflake/... cache key.
  • fastembed/text/onnx_embedding.py#L152-L152: support the previous lowercase snowflake/... cache key.
  • fastembed/text/onnx_embedding.py#L164-L164: support the previous lowercase snowflake/... cache key.
  • fastembed/text/pooled_embedding.py#L58-L58: support the previous lowercase qdrant/... cache key.
  • fastembed/text/pooled_embedding.py#L70-L70: support the previous lowercase xenova/... cache key.
  • fastembed/text/pooled_embedding.py#L83-L83: support the previous lowercase qdrant/... cache key.
  • fastembed/text/pooled_normalized_embedding.py#L22-L22: support the previous lowercase qdrant/... cache key.
  • fastembed/text/pooled_normalized_embedding.py#L36-L36: support the previous lowercase xenova/... cache key.
  • fastembed/text/pooled_normalized_embedding.py#L48-L48: support the previous lowercase xenova/... cache key.
  • fastembed/text/pooled_normalized_embedding.py#L120-L120: support the previous lowercase qdrant/... cache key.
📍 Affects 4 files
  • fastembed/late_interaction/token_embeddings.py#L22-L22 (this comment)
  • fastembed/text/onnx_embedding.py#L50-L50
  • fastembed/text/onnx_embedding.py#L116-L116
  • fastembed/text/onnx_embedding.py#L128-L128
  • fastembed/text/onnx_embedding.py#L152-L152
  • fastembed/text/onnx_embedding.py#L164-L164
  • fastembed/text/pooled_embedding.py#L58-L58
  • fastembed/text/pooled_embedding.py#L70-L70
  • fastembed/text/pooled_embedding.py#L83-L83
  • fastembed/text/pooled_normalized_embedding.py#L22-L22
  • fastembed/text/pooled_normalized_embedding.py#L36-L36
  • fastembed/text/pooled_normalized_embedding.py#L48-L48
  • fastembed/text/pooled_normalized_embedding.py#L120-L120

model_file="onnx/model.onnx",
),
]
Expand Down
2 changes: 1 addition & 1 deletion fastembed/sparse/sparse_text_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def list_supported_models(cls) -> list[dict[str, Any]]:
"license": "apache-2.0",
"size_in_GB": 0.532,
"sources": {
"hf": "qdrant/SPLADE_PP_en_v1",
"hf": "Qdrant/Splade_PP_en_v1",
},
}
]
Expand Down
10 changes: 5 additions & 5 deletions fastembed/text/onnx_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
),
license="mit",
size_in_GB=1.20,
sources=ModelSource(hf="qdrant/bge-large-en-v1.5-onnx"),
sources=ModelSource(hf="Qdrant/bge-large-en-v1.5-onnx"),
model_file="model.onnx",
),
DenseModelDescription(
Expand Down Expand Up @@ -113,7 +113,7 @@
),
license="apache-2.0",
size_in_GB=0.09,
sources=ModelSource(hf="snowflake/snowflake-arctic-embed-xs"),
sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-xs"),
model_file="onnx/model.onnx",
),
DenseModelDescription(
Expand All @@ -125,7 +125,7 @@
),
license="apache-2.0",
size_in_GB=0.13,
sources=ModelSource(hf="snowflake/snowflake-arctic-embed-s"),
sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-s"),
model_file="onnx/model.onnx",
),
DenseModelDescription(
Expand All @@ -149,7 +149,7 @@
),
license="apache-2.0",
size_in_GB=0.54,
sources=ModelSource(hf="snowflake/snowflake-arctic-embed-m-long"),
sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-m-long"),
model_file="onnx/model.onnx",
),
DenseModelDescription(
Expand All @@ -161,7 +161,7 @@
),
license="apache-2.0",
size_in_GB=1.02,
sources=ModelSource(hf="snowflake/snowflake-arctic-embed-l"),
sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-l"),
model_file="onnx/model.onnx",
),
DenseModelDescription(
Expand Down
6 changes: 3 additions & 3 deletions fastembed/text/pooled_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
),
license="apache-2.0",
size_in_GB=0.22,
sources=ModelSource(hf="qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q"),
sources=ModelSource(hf="Qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q"),
model_file="model_optimized.onnx",
),
DenseModelDescription(
Expand All @@ -67,7 +67,7 @@
),
license="apache-2.0",
size_in_GB=1.00,
sources=ModelSource(hf="xenova/paraphrase-multilingual-mpnet-base-v2"),
sources=ModelSource(hf="Xenova/paraphrase-multilingual-mpnet-base-v2"),
model_file="onnx/model.onnx",
),
DenseModelDescription(
Expand All @@ -80,7 +80,7 @@
license="mit",
size_in_GB=2.24,
sources=ModelSource(
hf="qdrant/multilingual-e5-large-onnx",
hf="Qdrant/multilingual-e5-large-onnx",
_deprecated_tar_struct=True,
),
model_file="model.onnx",
Expand Down
8 changes: 4 additions & 4 deletions fastembed/text/pooled_normalized_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
license="apache-2.0",
size_in_GB=0.09,
sources=ModelSource(
hf="qdrant/all-MiniLM-L6-v2-onnx",
hf="Qdrant/all-MiniLM-L6-v2-onnx",
_deprecated_tar_struct=True,
),
model_file="model.onnx",
Expand All @@ -33,7 +33,7 @@
),
license="apache-2.0",
size_in_GB=0.52,
sources=ModelSource(hf="xenova/jina-embeddings-v2-base-en"),
sources=ModelSource(hf="Xenova/jina-embeddings-v2-base-en"),
model_file="onnx/model.onnx",
),
DenseModelDescription(
Expand All @@ -45,7 +45,7 @@
),
license="apache-2.0",
size_in_GB=0.12,
sources=ModelSource(hf="xenova/jina-embeddings-v2-small-en"),
sources=ModelSource(hf="Xenova/jina-embeddings-v2-small-en"),
model_file="onnx/model.onnx",
),
DenseModelDescription(
Expand Down Expand Up @@ -117,7 +117,7 @@
),
license="mit",
size_in_GB=1.20,
sources=ModelSource(hf="qdrant/gte-large-onnx"),
sources=ModelSource(hf="Qdrant/gte-large-onnx"),
model_file="model.onnx",
),
]
Expand Down
Loading