Skip to content

Commit 4dc4d57

Browse files
aleksandr-mokrovIlyasMoutawwakilecharlaix
authored
Support SentenceTransformers models in optimum (#865)
* Support sentence transformer * FIx error * Separate class for SentenceTransfromers and test * Separate class interface for Sentence Transformers * Check if object has encode method * Formatting and change uotput checking * Tokenizer init moved to __init__, sentence-tranformer pipeline in OVModelForFeatureExtraction was changed * Remove model_max_length default value * Update tests/openvino/test_modeling_sentence_transformers.py Co-authored-by: Ella Charlaix <[email protected]> * Update tests/openvino/test_modeling.py Co-authored-by: Ella Charlaix <[email protected]> * Move tokenizer initialization, other improvements * Update optimum/intel/openvino/modeling_sentence_transformers.py Co-authored-by: Ella Charlaix <[email protected]> * Renaming OVModelForSentenceTransformer to OVSentenceTransformer * Make style * Move checking to init * Update tests/openvino/test_modeling.py Co-authored-by: Ella Charlaix <[email protected]> * Add dummy_openvino_and_sentence_transformers_objects.py * refactoring * Update optimum/intel/utils/dummy_openvino_and_sentence_transformers_objects.py Co-authored-by: Ella Charlaix <[email protected]> * Add tests to check saving and loading model * Add tests to check saving and loading model * Support cls._library_name * Fix test * Refactoring * Refactoring fix * Update optimum/intel/openvino/modeling.py * Fix test --------- Co-authored-by: Ilyas Moutawwakil <[email protected]> Co-authored-by: Ella Charlaix <[email protected]> Co-authored-by: Ella Charlaix <[email protected]>
1 parent 3586b5b commit 4dc4d57

13 files changed

+324
-2
lines changed

optimum/intel/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
is_neural_compressor_available,
2525
is_nncf_available,
2626
is_openvino_available,
27+
is_sentence_transformers_available,
2728
)
2829
from .version import __version__
2930

@@ -179,6 +180,21 @@
179180
_import_structure["neural_compressor"].append("INCStableDiffusionPipeline")
180181

181182

183+
try:
184+
if not (is_openvino_available() and is_sentence_transformers_available()):
185+
raise OptionalDependencyNotAvailable()
186+
except OptionalDependencyNotAvailable:
187+
_import_structure["utils.dummy_openvino_and_sentence_transformers_objects"] = [
188+
"OVSentenceTransformer",
189+
]
190+
else:
191+
_import_structure["openvino"].extend(
192+
[
193+
"OVSentenceTransformer",
194+
]
195+
)
196+
197+
182198
if TYPE_CHECKING:
183199
try:
184200
if not is_ipex_available():
@@ -302,6 +318,18 @@
302318
else:
303319
from .neural_compressor import INCStableDiffusionPipeline
304320

321+
try:
322+
if not (is_openvino_available() and is_sentence_transformers_available()):
323+
raise OptionalDependencyNotAvailable()
324+
except OptionalDependencyNotAvailable:
325+
from .utils.dummy_openvino_and_sentence_transformers_objects import (
326+
OVSentenceTransformer,
327+
)
328+
else:
329+
from .openvino import (
330+
OVSentenceTransformer,
331+
)
332+
305333
else:
306334
import sys
307335

optimum/intel/openvino/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
import logging
1616
import warnings
1717

18-
from ..utils.import_utils import is_accelerate_available, is_diffusers_available, is_nncf_available
18+
from ..utils.import_utils import (
19+
is_accelerate_available,
20+
is_diffusers_available,
21+
is_nncf_available,
22+
is_sentence_transformers_available,
23+
)
1924
from .utils import (
2025
OV_DECODER_NAME,
2126
OV_DECODER_WITH_PAST_NAME,
@@ -77,3 +82,7 @@
7782
OVStableDiffusionXLImg2ImgPipeline,
7883
OVStableDiffusionXLPipeline,
7984
)
85+
86+
87+
if is_sentence_transformers_available():
88+
from .modeling_sentence_transformers import OVSentenceTransformer

optimum/intel/openvino/modeling.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,13 @@ class OVModelForFeatureExtraction(OVModel):
369369
auto_model_class = AutoModel
370370

371371
def __init__(self, model=None, config=None, **kwargs):
372+
if {"token_embeddings", "sentence_embedding"}.issubset(
373+
{name for output in model.outputs for name in output.names}
374+
): # Sentence Transormers outputs
375+
raise ValueError(
376+
"This model is a Sentence Transformers model. Please use `OVSentenceTransformer` to load this model."
377+
)
378+
372379
super().__init__(model, config, **kwargs)
373380

374381
@add_start_docstrings_to_model_forward(

optimum/intel/openvino/modeling_base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class OVBaseModel(OptimizedModel):
5454
auto_model_class = None
5555
export_feature = None
5656
_supports_cache_class = False
57+
_library_name = "transformers"
5758

5859
def __init__(
5960
self,
@@ -501,6 +502,7 @@ def _from_transformers(
501502
force_download=force_download,
502503
trust_remote_code=trust_remote_code,
503504
ov_config=ov_config,
505+
library_name=cls._library_name,
504506
)
505507

506508
config.save_pretrained(save_dir_path)

optimum/intel/openvino/modeling_decoder.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ def _from_transformers(
296296
ov_config=ov_export_config,
297297
stateful=stateful,
298298
model_loading_kwargs=model_loading_kwargs,
299+
library_name=cls._library_name,
299300
)
300301

301302
config.is_decoder = True

optimum/intel/openvino/modeling_diffusion.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class OVStableDiffusionPipelineBase(OVBaseModel, OVTextualInversionLoaderMixin):
7878
auto_model_class = StableDiffusionPipeline
7979
config_name = "model_index.json"
8080
export_feature = "text-to-image"
81+
_library_name = "diffusers"
8182

8283
def __init__(
8384
self,
@@ -372,6 +373,7 @@ def _from_transformers(
372373
local_files_only=local_files_only,
373374
force_download=force_download,
374375
ov_config=ov_config,
376+
library_name=cls._library_name,
375377
)
376378

377379
return cls._from_pretrained(
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from pathlib import Path
2+
from types import MethodType
3+
from typing import Dict, List, Optional, Tuple, Union
4+
5+
import numpy as np
6+
import torch
7+
from huggingface_hub.constants import HUGGINGFACE_HUB_CACHE
8+
from sentence_transformers import SentenceTransformer
9+
from transformers import AutoTokenizer, PretrainedConfig
10+
from transformers.file_utils import add_start_docstrings
11+
12+
from .modeling import MODEL_START_DOCSTRING, OVModel
13+
14+
15+
@add_start_docstrings(
16+
"""
17+
OpenVINO Model for feature extraction tasks for Sentence Transformers.
18+
""",
19+
MODEL_START_DOCSTRING,
20+
)
21+
class OVSentenceTransformer(OVModel):
22+
export_feature = "feature-extraction"
23+
_library_name = "sentence_transformers"
24+
25+
def __init__(self, model=None, config=None, tokenizer=None, **kwargs):
26+
super().__init__(model, config, **kwargs)
27+
28+
self.encode = MethodType(SentenceTransformer.encode, self)
29+
self._text_length = MethodType(SentenceTransformer._text_length, self)
30+
self.default_prompt_name = None
31+
self.truncate_dim = None
32+
self.tokenizer = tokenizer
33+
34+
def _save_pretrained(self, save_directory: Union[str, Path]):
35+
super()._save_pretrained(save_directory)
36+
self.tokenizer.save_pretrained(save_directory)
37+
38+
def forward(self, inputs: Dict[str, torch.Tensor]):
39+
self.compile()
40+
input_ids = inputs.get("input_ids")
41+
attention_mask = inputs.get("attention_mask")
42+
token_type_ids = inputs.get("token_type_ids")
43+
44+
np_inputs = isinstance(input_ids, np.ndarray)
45+
if not np_inputs:
46+
input_ids = np.array(input_ids)
47+
attention_mask = np.array(attention_mask)
48+
token_type_ids = np.array(token_type_ids) if token_type_ids is not None else token_type_ids
49+
50+
inputs = {
51+
"input_ids": input_ids,
52+
"attention_mask": attention_mask,
53+
}
54+
55+
# Add the token_type_ids when needed
56+
if "token_type_ids" in self.input_names:
57+
inputs["token_type_ids"] = token_type_ids if token_type_ids is not None else np.zeros_like(input_ids)
58+
59+
outputs = self._inference(inputs)
60+
return {
61+
"token_embeddings": torch.from_numpy(outputs["token_embeddings"]).to(self.device),
62+
"sentence_embedding": torch.from_numpy(outputs["sentence_embedding"]).to(self.device),
63+
}
64+
65+
@classmethod
66+
def _from_pretrained(
67+
cls,
68+
model_id: Union[str, Path],
69+
config: PretrainedConfig,
70+
token: Optional[Union[bool, str]] = None,
71+
revision: Optional[str] = None,
72+
force_download: bool = False,
73+
cache_dir: str = HUGGINGFACE_HUB_CACHE,
74+
file_name: Optional[str] = None,
75+
subfolder: str = "",
76+
from_onnx: bool = False,
77+
local_files_only: bool = False,
78+
**kwargs,
79+
):
80+
trust_remote_code = kwargs.pop("trust_remote_code", False)
81+
tokenizer_kwargs = kwargs.pop("tokenizer_kwargs", None)
82+
83+
tokenizer_args = {
84+
"token": token,
85+
"trust_remote_code": trust_remote_code,
86+
"revision": revision,
87+
"local_files_only": local_files_only,
88+
}
89+
if tokenizer_kwargs:
90+
kwargs["tokenizer_args"].update(tokenizer_kwargs)
91+
92+
tokenizer = AutoTokenizer.from_pretrained(model_id, **tokenizer_args)
93+
94+
return super()._from_pretrained(
95+
model_id=model_id,
96+
config=config,
97+
token=token,
98+
revision=revision,
99+
force_download=force_download,
100+
cache_dir=cache_dir,
101+
file_name=file_name,
102+
subfolder=subfolder,
103+
from_onnx=from_onnx,
104+
local_files_only=local_files_only,
105+
tokenizer=tokenizer,
106+
**kwargs,
107+
)
108+
109+
def tokenize(
110+
self, texts: Union[List[str], List[Dict], List[Tuple[str, str]]], padding: Union[str, bool] = True
111+
) -> Dict[str, torch.Tensor]:
112+
"""Tokenizes a text and maps tokens to token-ids"""
113+
output = {}
114+
if isinstance(texts[0], str):
115+
to_tokenize = [texts]
116+
elif isinstance(texts[0], dict):
117+
to_tokenize = []
118+
output["text_keys"] = []
119+
for lookup in texts:
120+
text_key, text = next(iter(lookup.items()))
121+
to_tokenize.append(text)
122+
output["text_keys"].append(text_key)
123+
to_tokenize = [to_tokenize]
124+
else:
125+
batch1, batch2 = [], []
126+
for text_tuple in texts:
127+
batch1.append(text_tuple[0])
128+
batch2.append(text_tuple[1])
129+
to_tokenize = [batch1, batch2]
130+
131+
# strip
132+
to_tokenize = [[str(s).strip() for s in col] for col in to_tokenize]
133+
134+
output.update(
135+
self.tokenizer(
136+
*to_tokenize,
137+
padding=padding,
138+
truncation="longest_first",
139+
return_tensors="pt",
140+
)
141+
)
142+
return output

optimum/intel/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
is_nncf_available,
2525
is_numa_available,
2626
is_openvino_available,
27+
is_sentence_transformers_available,
2728
is_torch_version,
2829
is_transformers_available,
2930
is_transformers_version,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2023 The HuggingFace Team. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from .import_utils import DummyObject, requires_backends
16+
17+
18+
class OVSentenceTransformer(metaclass=DummyObject):
19+
_backends = ["openvino", "sentence_transformers"]
20+
21+
def __init__(self, *args, **kwargs):
22+
requires_backends(self, ["openvino", "sentence_transformers"])
23+
24+
@classmethod
25+
def from_pretrained(cls, *args, **kwargs):
26+
requires_backends(cls, ["openvino", "sentence_transformers"])

optimum/intel/utils/import_utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@
159159
_numa_available = False
160160

161161

162+
_sentence_transformers_available = importlib.util.find_spec("sentence_transformers") is not None
163+
_sentence_transformers_available = "N/A"
164+
165+
if _sentence_transformers_available:
166+
try:
167+
_sentence_transformers_available = importlib_metadata.version("sentence_transformers")
168+
except importlib_metadata.PackageNotFoundError:
169+
_sentence_transformers_available = False
170+
171+
162172
def is_transformers_available():
163173
return _transformers_available
164174

@@ -280,6 +290,10 @@ def is_accelerate_available():
280290
return _accelerate_available
281291

282292

293+
def is_sentence_transformers_available():
294+
return _sentence_transformers_available
295+
296+
283297
def is_numa_available():
284298
return _numa_available
285299

0 commit comments

Comments
 (0)