-
Notifications
You must be signed in to change notification settings - Fork 0
USE 113 - OSNeuralSparseDocV3GTE download and load #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import functools | ||
| import logging | ||
| import os | ||
| import time | ||
| from collections.abc import Callable | ||
| from datetime import timedelta | ||
|
|
@@ -90,6 +91,25 @@ def download_model(model_uri: str, output: Path) -> None: | |
| click.echo(result_path) | ||
|
|
||
|
|
||
| @main.command() | ||
| def test_model_load() -> None: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarifying: is this method part of the expected workflow or just for testing? Not fully clear based on the name and docstring
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not part of a normal workflow, just for testing. It's akin to a I can update the docstring to be a bit clearer.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops! Just merged without that update. I'll make sure it makes it into the next PR! Sorry about that. |
||
| """Test loading of embedding class and local model based on env vars. | ||
|
|
||
| In a deployed context, the following env vars are expected: | ||
| - TE_MODEL_URI | ||
| - TE_MODEL_DOWNLOAD_PATH | ||
|
|
||
| With these set, the embedding class should be registered successfully and initialized, | ||
| and the model loaded from a local copy. | ||
| """ | ||
| # load embedding model class | ||
| model_class = get_model_class(os.environ["TE_MODEL_URI"]) | ||
| model = model_class() | ||
|
|
||
| model.load(os.environ["TE_MODEL_DOWNLOAD_PATH"]) | ||
| click.echo("OK") | ||
|
|
||
|
|
||
| @main.command() | ||
| @model_required | ||
| def create_embeddings(_model_uri: str) -> None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,24 @@ | ||
| """OpenSearch Neural Sparse Doc v3 GTE model.""" | ||
|
|
||
| import json | ||
| import logging | ||
| import shutil | ||
| import tempfile | ||
| import time | ||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from huggingface_hub import snapshot_download | ||
| from transformers import AutoModelForMaskedLM, AutoTokenizer | ||
|
|
||
| from embeddings.models.base import BaseEmbeddingModel | ||
|
|
||
| if TYPE_CHECKING: | ||
| from transformers import PreTrainedModel | ||
| from transformers.models.distilbert.tokenization_distilbert_fast import ( | ||
| DistilBertTokenizerFast, | ||
| ) | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
|
|
@@ -16,11 +30,134 @@ class OSNeuralSparseDocV3GTE(BaseEmbeddingModel): | |
|
|
||
| MODEL_URI = "opensearch-project/opensearch-neural-sparse-encoding-doc-v3-gte" | ||
|
|
||
| def download(self, output_path: Path) -> Path: | ||
| def __init__(self) -> None: | ||
| """Initialize the model.""" | ||
| super().__init__() | ||
| self._model: PreTrainedModel | None = None | ||
| self._tokenizer: DistilBertTokenizerFast | None = None | ||
| self._special_token_ids: list | None = None | ||
| self._id_to_token: list | None = None | ||
|
|
||
| def download(self, output_path: str | Path) -> Path: | ||
| """Download and prepare model, saving to output_path. | ||
|
|
||
| Args: | ||
| output_path: Path where the model zip should be saved. | ||
| output_path: Path where the model should be saved. | ||
| """ | ||
| logger.info(f"Downloading model: { self.model_uri}, saving to: {output_path}.") | ||
| raise NotImplementedError | ||
| start_time = time.perf_counter() | ||
|
|
||
| output_path = Path(output_path) | ||
| logger.info(f"Downloading model: {self.model_uri}, saving to: {output_path}.") | ||
|
|
||
| with tempfile.TemporaryDirectory() as temp_dir: | ||
| temp_path = Path(temp_dir) | ||
|
|
||
| # download snapshot of HuggingFace model | ||
| snapshot_download(repo_id=self.model_uri, local_dir=temp_path) | ||
| logger.debug("Model download complete.") | ||
|
|
||
| # patch local model with files from dependency model "Alibaba-NLP/new-impl" | ||
| self._patch_local_model_with_alibaba_new_impl(temp_path) | ||
|
|
||
| # compress model directory as a zip file | ||
| if output_path.suffix.lower() == ".zip": | ||
| logger.debug("Creating zip file of model contents.") | ||
| shutil.make_archive(str(output_path.with_suffix("")), "zip", temp_path) | ||
|
|
||
| # copy to output directory without zipping | ||
| else: | ||
| logger.debug(f"Copying model contents to {output_path}") | ||
| if output_path.exists(): | ||
| shutil.rmtree(output_path) | ||
| shutil.copytree(temp_path, output_path) | ||
|
|
||
| logger.info(f"Model downloaded successfully, {time.perf_counter() - start_time}s") | ||
| return output_path | ||
|
|
||
| def _patch_local_model_with_alibaba_new_impl(self, model_temp_path: Path) -> None: | ||
| """Patch downloaded model with required assets from Alibaba-NLP/new-impl. | ||
|
|
||
| Our main model, opensearch-project/opensearch-neural-sparse-encoding-doc-v3-gte, | ||
| has configurations that attempt dynamic downloading of another model for files. | ||
| This can be seen here: https://huggingface.co/opensearch-project/opensearch- | ||
| neural-sparse-encoding-doc-v3-gte/blob/main/config.json#L6-L14. | ||
|
|
||
| To avoid our deployed CLI application making requests to the HuggingFace API to | ||
| retrieve these required files, which is problematic during high concurrency, we | ||
| manually download these files and patch the model during our local download and | ||
| save. | ||
|
|
||
| This allows us to load the primary model without any HuggingFace API calls. | ||
| """ | ||
|
Comment on lines
+77
to
+91
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was surprising that we need to do this, and it's possible we may discover a more idiomatic way to do this in the future. The long and the short is: sometimes a HuggingFace model -- like ours -- will actually reference other models in its internal configurations. When the model is first loaded, the This method downloads a required model in advance, and copies small configuration files from it into our model, which is all that's needed. Then our model's |
||
| logger.info("Downloading custom code from Alibaba-NLP/new-impl") | ||
| with tempfile.TemporaryDirectory() as temp_dir: | ||
| temp_path = Path(temp_dir) | ||
| snapshot_download( | ||
| repo_id="Alibaba-NLP/new-impl", | ||
| local_dir=str(temp_path), | ||
| ) | ||
|
|
||
| logger.info("Copying Alibaba code and updating config.json") | ||
| shutil.copy(temp_path / "modeling.py", model_temp_path / "modeling.py") | ||
| shutil.copy( | ||
| temp_path / "configuration.py", | ||
| model_temp_path / "configuration.py", | ||
| ) | ||
|
|
||
| with open(model_temp_path / "config.json") as f: | ||
| config_json = json.load(f) | ||
| config_json["auto_map"] = { | ||
| "AutoConfig": "configuration.NewConfig", | ||
| "AutoModel": "modeling.NewModel", | ||
| "AutoModelForMaskedLM": "modeling.NewForMaskedLM", | ||
| "AutoModelForMultipleChoice": "modeling.NewForMultipleChoice", | ||
| "AutoModelForQuestionAnswering": "modeling.NewForQuestionAnswering", | ||
| "AutoModelForSequenceClassification": ( | ||
| "modeling.NewForSequenceClassification" | ||
| ), | ||
| "AutoModelForTokenClassification": ( | ||
| "modeling.NewForTokenClassification" | ||
| ), | ||
| } | ||
| with open(model_temp_path / "config.json", "w") as f: | ||
| f.write(json.dumps(config_json)) | ||
|
|
||
| logger.debug("Dependency model Alibaba-NLP/new-impl downloaded and used.") | ||
|
|
||
| def load(self, model_path: str | Path) -> None: | ||
| """Load the model from the specified path. | ||
|
|
||
| Args: | ||
| model_path: Path to the model directory. | ||
| """ | ||
| start_time = time.perf_counter() | ||
| logger.info(f"Loading model from: {model_path}") | ||
| model_path = Path(model_path) | ||
|
|
||
| # ensure model exists locally | ||
| if not model_path.exists(): | ||
| raise FileNotFoundError(f"Model not found at path: {model_path}") | ||
|
|
||
| # load local model and tokenizer | ||
| self._model = AutoModelForMaskedLM.from_pretrained( | ||
| model_path, | ||
| trust_remote_code=True, | ||
| local_files_only=True, | ||
| ) | ||
| self._tokenizer = AutoTokenizer.from_pretrained( # type: ignore[no-untyped-call] | ||
| model_path, | ||
| local_files_only=True, | ||
| ) | ||
|
|
||
| # setup special tokens | ||
| self._special_token_ids = [ | ||
| self._tokenizer.vocab[str(token)] | ||
| for token in self._tokenizer.special_tokens_map.values() | ||
| ] | ||
|
|
||
| # setup id_to_token mapping | ||
| self._id_to_token = ["" for _ in range(self._tokenizer.vocab_size)] | ||
| for token, token_id in self._tokenizer.vocab.items(): | ||
| self._id_to_token[token_id] = token | ||
|
Comment on lines
+141
to
+161
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This ported almost directly from the model's HuggingFace "card" (website): https://huggingface.co/opensearch-project/opensearch-neural-sparse-encoding-doc-v3-gte#usage-huggingface. It's likely that we'll return to this for tweaks and updates, so it's probably not worth a deep review now. Big picture, we load two things:
Just to reiterate: this was added in this PR mostly to confirm that the download process was working correctly. Once we get into creating embeddings, I anticipate updates to this simplistic model loading. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Context is appreciated as well as acknowledging this is likely to be in flux |
||
|
|
||
| logger.info(f"Model loaded successfully, {time.perf_counter()-start_time}s") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great overview