Skip to content
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

Stateless model auth #460

Merged
merged 60 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
9d07322
added model auth
pandu-k May 3, 2023
d5fe616
Added objects for structure
pandu-k May 4, 2023
7993dee
introducing AddDocsParams object for addDocuments calls
pandu-k May 5, 2023
d28da08
added pydantic plugin advice to dev guide
pandu-k May 5, 2023
187439a
refactored test_add_docs to use AddDocsParams class
pandu-k May 5, 2023
5df5c4b
updated tensor search docstring
pandu-k May 5, 2023
4c723ce
refactored tests to work with new AddDocsParams object
pandu-k May 5, 2023
d0f2ce1
Commit for refactoring done to test_add_documents_use_existing_tensor…
pandu-k May 5, 2023
be4b7c6
refactored delete_documents to use the add_documents wrapper
pandu-k May 5, 2023
e7d0d75
added transitional add_docs wrapper
pandu-k May 5, 2023
91ce1b4
added transition add docs wrappers to add_documents calls in these te…
pandu-k May 5, 2023
efade22
added transition add docs wrappers to add_documents calls in these te…
pandu-k May 5, 2023
c37bac8
test updated to use new add_docs param object
pandu-k May 5, 2023
79f998d
cleaning up tests with issues
pandu-k May 5, 2023
a8aabd1
made progress integrating s3
pandu-k May 7, 2023
d9d1e1a
add docs, s3 works
pandu-k May 7, 2023
aea82b2
search seems to work (it at least passes through the auth info OK)
pandu-k May 7, 2023
ba37d88
added boto to reqs add hf skeleton funcs
pandu-k May 8, 2023
120c60d
Merge branch 'mainline' into pandu/model-auth-stateless
pandu-k May 8, 2023
c939927
added changes
pandu-k May 8, 2023
d8dca7d
fixed test_bulk_search_different_models_separate_vectorise_calls
pandu-k May 8, 2023
729b189
created test_model_auth_s3 ()
pandu-k May 8, 2023
68a5e76
test_model_auth_s3() asserts boto3 client instantiation
pandu-k May 8, 2023
7965439
made assertions about the presence of the model file
pandu-k May 8, 2023
85a368c
added hf loading tests
pandu-k May 8, 2023
926269d
added tests for hf, s3 search
pandu-k May 8, 2023
5142612
added test for s3/hf mismatch
pandu-k May 8, 2023
a2f9f0f
test refactor
pandu-k May 8, 2023
5e40a76
added cuda test
pandu-k May 8, 2023
a9a15e9
add_docs parses model auth str
pandu-k May 9, 2023
16d7156
corrected add_docs derivatives test
pandu-k May 9, 2023
c4aee67
removed unused vectorise params
pandu-k May 9, 2023
1f6c6a6
fixed bug, not passing through auth in multimodal_combination
pandu-k May 9, 2023
f026d88
added test for no creds
pandu-k May 9, 2023
f027be9
added test_bad_creds_error_s3
pandu-k May 9, 2023
995c028
test for access to non existent hf repo
pandu-k May 9, 2023
81b586e
Added test_after_downloading_search_doesnt_redownload
pandu-k May 9, 2023
98c42ad
fixed checking loaded models
pandu-k May 9, 2023
f556476
added from_s3 tests
pandu-k May 9, 2023
5124da1
Merge branch 'mainline' into pandu/model-auth-stateless
pandu-k May 9, 2023
355ce27
added from_hf tests
pandu-k May 9, 2023
4fbf9d0
added test_custom_clip_utils.py
pandu-k May 9, 2023
b0999f7
added call to search before add docs parallel
pandu-k May 9, 2023
8ecf5f5
Added search call before parallel add_docs
pandu-k May 9, 2023
79b9ccc
fixed casing issue in SearchQuery
pandu-k May 10, 2023
23ec8b6
fixed tests
pandu-k May 10, 2023
0750c85
completed custom clip utils test
pandu-k May 10, 2023
bc539ad
updated version
pandu-k May 10, 2023
ac628cd
improved error msg, added bulk search tests
pandu-k May 10, 2023
920f4e3
made custom clip tests stricter
pandu-k May 10, 2023
8a8e864
added device to model auth cuda setup
pandu-k May 10, 2023
aeb7415
added tests for CLIP._download_from_repo
pandu-k May 10, 2023
53f53b9
added CLIP.load() tests
pandu-k May 10, 2023
fcdd9b7
added OPEN_CLIP.load() tests
pandu-k May 10, 2023
41f30ef
fixed private model and test
pandu-k May 10, 2023
143238a
corrected HF auth and location inheritance
pandu-k May 10, 2023
7b13dea
removed test_put_documents_orchestrator() as put_documents is deprecated
pandu-k May 10, 2023
81bbcf1
Merge branch 'mainline' into pandu/model-auth-stateless
pandu-k May 11, 2023
fb878c9
corrected version
pandu-k May 11, 2023
f381068
Merge remote-tracking branch 'origin/pandu/model-auth-stateless' into…
pandu-k May 11, 2023
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
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pytest
tox
# s2_inference:
more_itertools
boto3==1.25.4
botocore==1.28.4
nltk==3.7
torch==1.12.1
torchvision==0.13.1
Expand Down
5 changes: 5 additions & 0 deletions src/marqo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,8 @@ curl http://localhost:8882/openapi.json
```
To get the human readable spec, visit `http://localhost:8882/docs`

## IDE tips

## PyCharm
Pydantic dataclasses are used in this project. By default, PyCharm can't parse initialisations of these dataclasses.
[This plugin](https://plugins.jetbrains.com/plugin/12861-pydantic) can help.
85 changes: 67 additions & 18 deletions src/marqo/s2_inference/clip_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# from torch import FloatTensor
# from typing import Any, Dict, List, Optional, Union
import os
import PIL.Image
from marqo.tensor_search.enums import ModelProperties, InferenceParams
from marqo.tensor_search.models.private_models import ModelLocation, ModelAuth
import validators
import requests
import numpy as np
Expand All @@ -15,7 +14,7 @@
from marqo.s2_inference.logger import get_logger
from marqo.s2_inference.errors import IncompatibleModelDeviceError, InvalidModelPropertiesError
from torchvision.transforms import Compose, Resize, CenterCrop, ToTensor, Normalize
from marqo.s2_inference.processing.custom_clip_utils import HFTokenizer, download_pretrained_from_url
from marqo.s2_inference.processing.custom_clip_utils import HFTokenizer, download_model
from torchvision.transforms import InterpolationMode
from marqo.s2_inference.configs import ModelCache

Expand Down Expand Up @@ -205,22 +204,63 @@ def __init__(self, model_type: str = "ViT-B/32", device: str = 'cpu', embedding
self.truncate = truncate
self.model_properties = kwargs.get("model_properties", dict())

# model_auth gets passed through add_docs and search requests:
model_auth = kwargs.get(InferenceParams.model_auth, None)
if model_auth is not None:
self.model_auth = model_auth
else:
self.model_auth = None

def _download_from_repo(self):
"""Downloads model from an external repo like s3 and returns the filepath

Returns:
The model's filepath

Raises:
RunTimeError if an empty filepath is detected. This is important
because OpenCLIP will instantiate a model with random weights, if
a filepath isn't specified, and the model isn't a publicly
available HF or OpenAI one.
"""
model_location = ModelLocation(**self.model_properties[ModelProperties.model_location])
download_model_params = {"repo_location": model_location}

if model_location.auth_required:
download_model_params['auth'] = self.model_auth

model_file_path = download_model(**download_model_params)
if model_file_path is None or model_file_path == '':
raise RuntimeError(
'download_model() needs to return a valid filepath to the model! Instead, received '
f' filepath `{model_file_path}`')
return model_file_path

def load(self) -> None:

model_location_presence = ModelProperties.model_location in self.model_properties

path = self.model_properties.get("localpath", None) or self.model_properties.get("url",None)

if path is None:
if path is None and not model_location_presence:
# The original method to load the openai clip model
# https://github.com/openai/CLIP/issues/30
self.model, self.preprocess = clip.load(self.model_type, device='cpu', jit=False, download_root=ModelCache.clip_cache_path)
self.model = self.model.to(self.device)
self.tokenizer = clip.tokenize
else:
logger.info("Detecting custom clip model path. We use generic clip model loading.")
if os.path.isfile(path):
if path and model_location_presence:
raise InvalidModelPropertiesError(
"Only one of `url`, `localpath` or `model_location can be specified in "
"model_properties`. Please ensure that only one of these is specified in "
"model_properties and retry.")
if model_location_presence:
self.model_path = self._download_from_repo()
elif os.path.isfile(path):
self.model_path = path
elif validators.url(path):
self.model_path = download_pretrained_from_url(path)
self.model_path = download_model(url=path)
else:
raise InvalidModelPropertiesError(f"Marqo can not load the custom clip model."
f"The provided model path `{path}` is neither a local file nor a valid url."
Expand Down Expand Up @@ -356,23 +396,33 @@ def load(self) -> None:
# https://github.com/mlfoundations/open_clip
path = self.model_properties.get("localpath", None) or self.model_properties.get("url", None)

if path is None:
model_location_presence = ModelProperties.model_location in self.model_properties

if path is None and not model_location_presence:
self.model, _, self.preprocess = open_clip.create_model_and_transforms(self.model_name,
pretrained=self.pretrained,
device=self.device, jit=False, cache_dir=ModelCache.clip_cache_path)
self.tokenizer = open_clip.get_tokenizer(self.model_name)
self.model.eval()
else:
if path and model_location_presence:
raise InvalidModelPropertiesError(
"Only one of `url`, `localpath` or `model_location can be specified in "
"model_properties`. Please ensure that only one of these is specified in "
"model_properties and retry.")
logger.info("Detecting custom clip model path. We use generic clip model loading.")
if os.path.isfile(path):
if model_location_presence:
self.model_path = self._download_from_repo()
elif os.path.isfile(path):
self.model_path = path
elif validators.url(path):
self.model_path = download_pretrained_from_url(path)
self.model_path = download_model(url=path)
else:
raise InvalidModelPropertiesError(f"Marqo can not load the custom clip model."
f"The provided model path `{path}` is neither a local file nor a valid url."
f"Please check your provided model url and retry."
f"Check `https://docs.marqo.ai/0.0.13/Models-Reference/dense_retrieval/#generic-clip-models` for more info.")
raise InvalidModelPropertiesError(
f"Marqo cannot load the custom clip model. "
f"The provided model path `{path}` is neither a local file nor a valid url. "
f"Please check your provided model url and retry. "
f"Check `https://docs.marqo.ai/0.0.13/Models-Reference/dense_retrieval/#generic-clip-models` for more info.")

self.precision = self.model_properties.get("precision", "fp32")
self.jit = self.model_properties.get("jit", False)
Expand All @@ -384,14 +434,13 @@ def load(self) -> None:

self.model.eval()


def custom_clip_load(self):
self.model_name = self.model_properties.get("name", None)


logger.info(f"The name of the custom clip model is {self.model_name}. We use open_clip load")
model, _, preprocess = open_clip.create_model_and_transforms(model_name=self.model_name, jit = self.jit, pretrained=self.model_path, precision = self.precision,
image_mean=self.mean, image_std=self.std, device = self.device, cache_dir=ModelCache.clip_cache_path)
model, _, preprocess = open_clip.create_model_and_transforms(
model_name=self.model_name, jit = self.jit, pretrained=self.model_path, precision = self.precision,
image_mean=self.mean, image_std=self.std, device = self.device, cache_dir=ModelCache.clip_cache_path)

return model, preprocess

Expand Down
Empty file.
48 changes: 48 additions & 0 deletions src/marqo/s2_inference/model_downloading/from_hf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from marqo.tensor_search.models.external_apis.hf import HfAuth, HfModelLocation
from typing import Optional
from huggingface_hub import hf_hub_download
from marqo.s2_inference.logger import get_logger
from huggingface_hub.utils._errors import RepositoryNotFoundError
from marqo.s2_inference.errors import ModelDownloadError

logger = get_logger(__name__)


def download_model_from_hf(
location: HfModelLocation,
auth: Optional[HfAuth] = None,
download_dir: Optional[str] = None):
"""Downloads a pretrained model from HF, if it doesn't exist locally. The basename of the
location's filename is used as the local filename.

hf_hub_download downloads the model if it does not yet exist in the cache.

Args:
location: repo_id and filename to be downloaded.
auth: contains HF API token for model access
download_dir: [not yet implemented]. The location where the model
should be stored

Returns:
Path to the downloaded model
"""
if download_dir is not None:
logger.warning(
"Hugging Face model download was given the `download_dir` argument, "
"even though it is not yet implemented. "
"The specified model will be downloaded but the `download_dir` "
"parameter will be ignored."
)
download_kwargs = location.dict()
if auth is not None:
download_kwargs = {**download_kwargs, **auth.dict()}
try:
return hf_hub_download(**download_kwargs)
except RepositoryNotFoundError:
# TODO: add link to HF model auth/loc
raise ModelDownloadError(
"Could not find the specified Hugging Face model repository. Please ensure that the request's model_auth's "
"`hf` credentials and the index's model_location are correct. "
"If the index's model_location is not correct, please create a new index with the corrected model_location"
)

74 changes: 74 additions & 0 deletions src/marqo/s2_inference/model_downloading/from_s3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import os
from marqo.s2_inference.configs import ModelCache
from marqo.tensor_search.models.external_apis.s3 import S3Auth, S3Location
from typing import Optional
import boto3
from marqo.s2_inference.errors import ModelDownloadError
from botocore.exceptions import NoCredentialsError


def get_presigned_s3_url(location: S3Location, auth: Optional[S3Auth] = None):
"""Returns the s3 url of a request to get an S3 object

Args:
location: Bucket and key of model file to be downloaded
auth: AWS IAM access keys to a user with access to the model to be downloaded

Returns:
The the presigned s3 URL

TODO: add link to proper usage in error messages
"""
if auth is None:
raise ModelDownloadError(
"Error retrieving private model. s3 authorisation information is required to "
"download a model from an s3 bucket. "
"If the model is publicly accessible, please use the model's publicly accessible URL."
)
s3_client = boto3.client('s3', **auth.dict())
try:
return s3_client.generate_presigned_url('get_object', Params=location.dict())
except NoCredentialsError:
raise ModelDownloadError(
"Error retrieving private model. AWS credentials were not accepted."
)


def get_s3_model_absolute_cache_path(location: S3Location) -> str:
"""Returns the absolute path of an s3 model if it were downloaded.

Args:
location: Bucket and key of model file to be downloaded

Returns:
The absolute path of an s3 model if it were downloaded.
"""
cache_dir = os.path.expanduser(ModelCache.clip_cache_path)
return os.path.join(cache_dir, get_s3_model_cache_filename(location))


def check_s3_model_already_exists(location: S3Location) -> bool:
"""Returns True iff an s3 model is already downloaded

Args:
location: Bucket and key of model file to be downloaded

Returns:
The model cache filename of an s3 object
"""
abs_path = get_s3_model_absolute_cache_path(location)
return os.path.isfile(abs_path)


def get_s3_model_cache_filename(location: S3Location) -> str:
"""Returns the model cache filename of an s3 object

Args:
location: Bucket and key of model file to be downloaded

Returns:
The model cache filename of an s3 object
"""
return os.path.basename(location.Key)


Loading