Skip to content

Commit 31f2c45

Browse files
committed
slimmify code, add tests
1 parent c180231 commit 31f2c45

4 files changed

Lines changed: 85 additions & 21 deletions

File tree

model2vec/persistence/hf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ def maybe_get_cached_model_path(model_id: str) -> Path | None:
3535
3636
Returns None if there is no cached model. In this case, the model will be downloaded.
3737
"""
38+
# Early exit: a model_id must have a single '/'
39+
if model_id.count("/") != 1:
40+
return None
3841
# Make path object
3942
cache_dir = Path(HF_HUB_CACHE)
4043
# This is specific to how HF stores the files.

model2vec/persistence/persistence.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,14 @@ def load_pretrained(
9898
:return: The embeddings, tokenizer, config, metadata, weights and mapping.
9999
100100
"""
101-
# Logic: by setting cached_folder, we no longer have a valid hub identifier.
102-
# So, in _get_paths, we always hit a local folder, and never pull from the hub.
103-
# If force_download is False, we never set the cached_folder.
104-
if not force_download:
105-
cached_folder = maybe_get_cached_model_path(str(folder_or_repo_path))
106-
if cached_folder:
107-
logger.info(f"Found cached model at {cached_folder}, loading from cache.")
108-
folder_or_repo_path = cached_folder
109-
else:
110-
logger.info(f"No cached model found for {folder_or_repo_path}, loading from hub.")
111-
112101
folder_or_repo_path = Path(folder_or_repo_path)
113102

114-
selected_layout = _get_paths(folder_or_repo_path, subfolder, token)
103+
# We resolve a folder or repo path to an actual local folder.
104+
folder = _resolve_folder(
105+
folder_or_repo_path=folder_or_repo_path, token=token, force_download=force_download, subfolder=subfolder
106+
)
107+
108+
selected_layout = _get_paths(folder)
115109
readme_path = folder_or_repo_path / "README.md"
116110

117111
opened_tensor_file = cast(SafeOpenProtocol, safetensors.safe_open(selected_layout.embeddings, framework="numpy"))
@@ -139,19 +133,23 @@ def load_pretrained(
139133
return embeddings, tokenizer, config, metadata, weights, mapping
140134

141135

142-
def _resolve_folder(folder_or_repo_path: Path, subfolder: str | None, token: str | None) -> Path:
136+
def _resolve_folder(folder_or_repo_path: Path, subfolder: str | None, token: str | None, force_download: bool) -> Path:
143137
"""Resolve a folder locally or from hugging face hub."""
144-
local_candidate = folder_or_repo_path / subfolder if subfolder else folder_or_repo_path
145-
if local_candidate.exists():
146-
folder = local_candidate
147-
else:
148-
folder = Path(huggingface_hub.snapshot_download(str(folder_or_repo_path), repo_type="model", token=token))
149-
return folder / subfolder if subfolder else folder
138+
if folder_or_repo_path.exists():
139+
return folder_or_repo_path
140+
# We now know we're dealing with either an invalid path, or
141+
# a HF model ID.
142+
if not force_download:
143+
if folder := maybe_get_cached_model_path(str(folder_or_repo_path)):
144+
return folder
145+
146+
folder = Path(huggingface_hub.snapshot_download(str(folder_or_repo_path), repo_type="model", token=token))
150147

148+
return folder
151149

152-
def _get_paths(folder_or_repo_path: Path, subfolder: str | None, token: str | None) -> Layout:
153-
folder = _resolve_folder(folder_or_repo_path, subfolder, token)
154150

151+
def _get_paths(folder: Path) -> Layout:
152+
"""Get all paths by trying out multiple layouts."""
155153
for layout in FOLDER_LAYOUTS:
156154
layout = layout.with_parent(folder)
157155
if layout.is_valid():

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from transformers.tokenization_utils_fast import PreTrainedTokenizerFast
1515

1616
from model2vec.inference import StaticModelPipeline
17+
from model2vec.model import StaticModel
1718
from model2vec.train import StaticModelForClassification
1819

1920
_TOKENIZER_TYPES = ["wordpiece", "bpe", "unigram"]
@@ -112,6 +113,17 @@ def mock_inference_pipeline(mock_trained_pipeline: StaticModelForClassification)
112113
return mock_trained_pipeline.to_pipeline()
113114

114115

116+
@pytest.fixture(scope="session")
117+
def mock_static_model() -> StaticModel:
118+
"""A small static model to test saving/loading."""
119+
tokenizer = AutoTokenizer.from_pretrained("tests/data/test_tokenizer").backend_tokenizer
120+
generator = np.random.RandomState(42)
121+
vectors = generator.randn(len(tokenizer.get_vocab()), 12)
122+
model = StaticModel(vectors=vectors, tokenizer=tokenizer)
123+
124+
return model
125+
126+
115127
@pytest.fixture(
116128
params=[
117129
(False, "single_label", "str"),

tests/test_persistence.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import shutil
2+
from pathlib import Path
3+
from tempfile import TemporaryDirectory
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
from model2vec.model import StaticModel
9+
10+
11+
def test_local_loading(mock_static_model: StaticModel) -> None:
12+
"""Test saving and loading."""
13+
with TemporaryDirectory() as dir_name:
14+
mock_static_model.save_pretrained(dir_name)
15+
16+
with patch(
17+
"model2vec.persistence.persistence.huggingface_hub.snapshot_download",
18+
) as mock_snapshot:
19+
mock_snapshot.return_value = dir_name
20+
# Patch the cache to return the local path, simulate a cache hit
21+
# we pass a fake model name to actually hit the cache and snapshot download paths
22+
with patch("model2vec.persistence.persistence.maybe_get_cached_model_path") as cache:
23+
# Simulate cache hit
24+
cache.return_value = dir_name
25+
s = StaticModel.from_pretrained("haha", force_download=True)
26+
assert s.tokens == mock_static_model.tokens
27+
s = StaticModel.from_pretrained("haha", force_download=False)
28+
assert s.tokens == mock_static_model.tokens
29+
30+
# Simulate cache miss
31+
cache.return_value = None
32+
s = StaticModel.from_pretrained("haha", force_download=True)
33+
assert s.tokens == mock_static_model.tokens
34+
s = StaticModel.from_pretrained("haha", force_download=False)
35+
assert s.tokens == mock_static_model.tokens
36+
37+
# Called twice, only when `force_download` is False
38+
assert cache.call_count == 2
39+
# Called three times, two times when force download is True,
40+
# and once when it was false but the cache returned None
41+
assert mock_snapshot.call_count == 3
42+
43+
44+
def test_garbage(mock_static_model: StaticModel) -> None:
45+
"""Test that garbage loading crashes."""
46+
with TemporaryDirectory() as dir_name:
47+
mock_static_model.save_pretrained(dir_name)
48+
dir_name_path = Path(dir_name)
49+
shutil.move(dir_name_path / "model.safetensors", dir_name_path / "model.safetenso")
50+
with pytest.raises(ValueError):
51+
StaticModel.from_pretrained(dir_name)

0 commit comments

Comments
 (0)