From adbf0ba0daeefcffa8ff62d3e892a68f3ccb7a39 Mon Sep 17 00:00:00 2001 From: Helena Kloosterman Date: Thu, 10 Oct 2024 19:31:54 +0200 Subject: [PATCH] Add backends: ONNX & OpenVINO + ONNX optimization, quantization (#2712) * Add OpenVINO support * Fix OpenVINO test on Windows * Expand OpenVino support (remote models); add ONNX backend; tests Also update push_to_hub * Move OV test to test_backends * Update push_to_hub test monkeypatching * Remove some dead code * Skip multi-process tests for now Incompatible with OpenVINO imports; still works independently * Move export_optimized_onnx_model to backend.py * Update __init__ to address the export_optimized_onnx_model move * Remove dot in commit message * Add PR description for export_optimized_onnx_model * OpenVINO will override export=False; update tests * Add dynamic quantization exporting; docs; benchmarks, etc. * Require 4.41.0 for eval_strategy, etc. * Restrict optimum-intel rather than optimum * Use subfolder rather than relying only on file_name Relies on the upcoming optimum and optimum-intel versions; this is expected to fail until then. * Add link to OVBaseModel.from_pretrained * Add tips pointing to the new efficiency docs * Another pointer to the new efficiency docs * Expand the benchmark details * Update min. requirements to optimum 1.23.0 & optimum-intel 1.20.0 --------- Co-authored-by: Tom Aarsen --- .github/workflows/tests.yml | 2 +- docs/_static/css/custom.css | 1 + docs/conf.py | 2 + docs/img/backends_benchmark_cpu.png | Bin 0 -> 64757 bytes docs/img/backends_benchmark_gpu.png | Bin 0 -> 58761 bytes docs/installation.md | 62 ++- docs/package_reference/util.md | 6 + docs/quickstart.rst | 7 +- docs/requirements.txt | 1 + .../sentence_transformer/pretrained_models.md | 4 + .../sentence_transformer/usage/efficiency.rst | 443 ++++++++++++++++++ docs/sentence_transformer/usage/usage.rst | 3 +- index.rst | 5 +- pyproject.toml | 7 +- sentence_transformers/SentenceTransformer.py | 118 ++++- sentence_transformers/__init__.py | 3 + sentence_transformers/backend.py | 261 +++++++++++ sentence_transformers/models/Transformer.py | 248 +++++++++- sentence_transformers/util.py | 12 +- tests/conftest.py | 10 + tests/test_backends.py | 158 +++++++ tests/test_multi_process.py | 4 + tests/test_sentence_transformer.py | 32 +- 23 files changed, 1327 insertions(+), 62 deletions(-) create mode 100644 docs/img/backends_benchmark_cpu.png create mode 100644 docs/img/backends_benchmark_gpu.png create mode 100644 docs/sentence_transformer/usage/efficiency.rst create mode 100644 sentence_transformers/backend.py create mode 100644 tests/test_backends.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 247a940c7..f803de40a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,7 +61,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install '.[dev,train]' + python -m pip install '.[train, onnx, openvino, dev]' - name: Run unit tests run: | diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 0bab3e76f..d5a916a4d 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -41,6 +41,7 @@ dl.class > dt { border-color: rgb(55 65 81); background-color: #e3e3e3; color: #404040; /* Override the colors imposed by */ + max-width: 18rem; } .components > .box:nth-child(1) > .header { diff --git a/docs/conf.py b/docs/conf.py index c7d536ee1..d8cd6d304 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,6 +43,7 @@ "sphinx.ext.intersphinx", "sphinx.ext.linkcode", "sphinx_inline_tabs", + "sphinxcontrib.mermaid", ] # Add any paths that contain templates here, relative to this directory. @@ -68,6 +69,7 @@ "datasets": ("https://huggingface.co/docs/datasets/main/en/", None), "transformers": ("https://huggingface.co/docs/transformers/main/en/", None), "huggingface_hub": ("https://huggingface.co/docs/huggingface_hub/main/en/", None), + "optimum": ("https://huggingface.co/docs/optimum/main/en/", None), "torch": ("https://pytorch.org/docs/stable/", None), } diff --git a/docs/img/backends_benchmark_cpu.png b/docs/img/backends_benchmark_cpu.png new file mode 100644 index 0000000000000000000000000000000000000000..72ee321daa5742c25c0ffd5b739df34c5f3114a1 GIT binary patch literal 64757 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 literal 0 HcmV?d00001 diff --git a/docs/img/backends_benchmark_gpu.png b/docs/img/backends_benchmark_gpu.png new file mode 100644 index 0000000000000000000000000000000000000000..11e0a00ee68ef012dba26832042b5a5dae05ee93 GIT binary patch literal 58761 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

hL9Jb!+;+PMIw(J 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 H_n`j($XV`k literal 0 HcmV?d00001 diff --git a/docs/installation.md b/docs/installation.md index 84ce4c0e4..707ad31d8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,10 +1,14 @@ # Installation -We recommend **Python 3.8+**, **[PyTorch 1.11.0+](https://pytorch.org/get-started/locally/)**, and **[transformers v4.34.0+](https://github.com/huggingface/transformers)**. There are three options to install Sentence Transformers: +We recommend **Python 3.8+**, **[PyTorch 1.11.0+](https://pytorch.org/get-started/locally/)**, and **[transformers v4.41.0+](https://github.com/huggingface/transformers)**. There are 5 extra options to install Sentence Transformers: * **Default:** This allows for loading, saving, and inference (i.e., getting embeddings) of models. -* **Default and Training**: All of the above plus training. +* **ONNX:** This allows for loading, saving, inference, optimizing, and quantizing of models using the ONNX backend. +* **OpenVINO:** This allows for loading, saving, and inference of models using the OpenVINO backend. +* **Default and Training**: Like **Default**, plus training. * **Development**: All of the above plus some dependencies for developing Sentence Transformers, see [Editable Install](#editable-install). +Note that you can mix and match the various extras, e.g. ``pip install -U "sentence-transformers[train, onnx-gpu]"`` + ## Install with pip ```eval_rst @@ -15,6 +19,24 @@ We recommend **Python 3.8+**, **[PyTorch 1.11.0+](https://pytorch.org/get-starte pip install -U sentence-transformers +.. tab:: ONNX + + For GPU and CPU: + :: + + pip install -U "sentence-transformers[onnx-gpu]" + + For CPU only: + :: + + pip install -U "sentence-transformers[onnx]" + +.. tab:: OpenVINO + + :: + + pip install -U "sentence-transformers[openvino]" + .. tab:: Default and Training :: @@ -47,6 +69,24 @@ We recommend **Python 3.8+**, **[PyTorch 1.11.0+](https://pytorch.org/get-starte conda install -c conda-forge sentence-transformers +.. tab:: ONNX + + For GPU and CPU: + :: + + pip install -U "sentence-transformers[onnx-gpu]" + + For CPU only: + :: + + pip install -U "sentence-transformers[onnx]" + +.. tab:: OpenVINO + + :: + + pip install -U "sentence-transformers[openvino]" + .. tab:: Default and Training :: @@ -81,6 +121,24 @@ You can install ``sentence-transformers`` directly from source to take advantage pip install git+https://github.com/UKPLab/sentence-transformers.git +.. tab:: ONNX + + For GPU and CPU: + :: + + pip install -U "sentence-transformers[onnx-gpu] @ git+https://github.com/UKPLab/sentence-transformers.git" + + For CPU only: + :: + + pip install -U "sentence-transformers[onnx] @ git+https://github.com/UKPLab/sentence-transformers.git" + +.. tab:: OpenVINO + + :: + + pip install -U "sentence-transformers[openvino] @ git+https://github.com/UKPLab/sentence-transformers.git" + .. tab:: Default and Training :: diff --git a/docs/package_reference/util.md b/docs/package_reference/util.md index a684df522..3e81f6de2 100644 --- a/docs/package_reference/util.md +++ b/docs/package_reference/util.md @@ -7,6 +7,12 @@ :members: paraphrase_mining, semantic_search, community_detection, http_get, truncate_embeddings, normalize_embeddings, is_training_available, mine_hard_negatives ``` +## Model Optimization +```eval_rst +.. automodule:: sentence_transformers.backend + :members: export_optimized_onnx_model, export_dynamic_quantized_onnx_model +``` + ## Similarity Metrics ```eval_rst diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 14f22eed9..3d4c61b82 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -23,6 +23,7 @@ Once you have `installed `_ Sentence Transformers, you can ea - :meth:`SentenceTransformer.similarity_pairwise ` - `SentenceTransformer > Usage <./sentence_transformer/usage/usage.html>`_ + - `SentenceTransformer > Usage > Speeding up Inference <./sentence_transformer/usage/efficiency.html>`_ - `SentenceTransformer > Pretrained Models <./sentence_transformer/pretrained_models.html>`_ - `SentenceTransformer > Training Overview <./sentence_transformer/training_overview.html>`_ - `SentenceTransformer > Dataset Overview <./sentence_transformer/dataset_overview.html>`_ @@ -55,10 +56,14 @@ Once you have `installed `_ Sentence Transformers, you can ea # [0.6660, 1.0000, 0.1411], # [0.1046, 0.1411, 1.0000]]) -With ``SentenceTransformer("all-MiniLM-L6-v2")`` we pick which `Sentence Transformer model `_ we load. In this example, we load `all-MiniLM-L6-v2 `_, which is a MiniLM model finetuned on a large dataset of over 1 billion training pairs. Using `SentenceTransformer.similarity() <./package_reference/sentence_transformer/SentenceTransformer.html#sentence_transformers.SentenceTransformer.similarity>`_, we compute the similarity between all pairs of sentences. As expected, the similarity between the first two sentences (0.6660) is higher than the similarity between the first and the third sentence (0.1046) or the second and the third sentence (0.1411). +With ``SentenceTransformer("all-MiniLM-L6-v2")`` we pick which `Sentence Transformer model `_ we load. In this example, we load `all-MiniLM-L6-v2 `_, which is a MiniLM model finetuned on a large dataset of over 1 billion training pairs. Using :meth:`SentenceTransformer.similarity() `, we compute the similarity between all pairs of sentences. As expected, the similarity between the first two sentences (0.6660) is higher than the similarity between the first and the third sentence (0.1046) or the second and the third sentence (0.1411). Finetuning Sentence Transformer models is easy and requires only a few lines of code. For more information, see the `Training Overview <./sentence_transformer/training_overview.html>`_ section. +.. tip:: + + Read `Sentence Transformer > Usage > Speeding up Inference `_ for tips on how to speed up inference of models by up to 2x-3x. + Cross Encoder ------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index 312d2e2eb..e952976da 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,5 @@ sphinx_markdown_tables==0.0.17 recommonmark==0.7.1 sphinx-copybutton==0.5.2 sphinx_inline_tabs==2023.4.21 +sphinxcontrib-mermaid==0.8.1 -e .. \ No newline at end of file diff --git a/docs/sentence_transformer/pretrained_models.md b/docs/sentence_transformer/pretrained_models.md index b7afc6cb0..316b361ea 100644 --- a/docs/sentence_transformer/pretrained_models.md +++ b/docs/sentence_transformer/pretrained_models.md @@ -31,6 +31,10 @@ similarities = model.similarity(embeddings, embeddings) - **Model sizes**: it is recommended to filter away the large models that might not be feasible without excessive hardware. - **Experimentation is key**: models that perform well on the leaderboard do not necessarily do well on your tasks, it is **crucial** to experiment with various promising models. + +.. tip:: + + Read `Sentence Transformer > Usage > Speeding up Inference <./usage/efficiency.html>`_ for tips on how to speed up inference of models by up to 2x-3x. ``` ## Original Models diff --git a/docs/sentence_transformer/usage/efficiency.rst b/docs/sentence_transformer/usage/efficiency.rst new file mode 100644 index 000000000..c30770078 --- /dev/null +++ b/docs/sentence_transformer/usage/efficiency.rst @@ -0,0 +1,443 @@ + +Speeding up Inference +===================== + +Sentence Transformers supports 3 backends for computing embeddings, each with its own optimizations for speeding up inference: + + +.. raw:: html + +

+
+ +PyTorch +------- + +The PyTorch backend is the default backend for Sentence Transformers. If you don't specify a device, it will use the strongest available option across "cuda", "mps", and "cpu". Its default usage looks like this: + +.. code-block:: python + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer("all-MiniLM-L6-v2") + + sentences = ["This is an example sentence", "Each sentence is converted"] + embeddings = model.encode(sentences) + +If you're using a GPU, then you can use the following options to speed up your inference: + +.. tab:: float16 (fp16) + + Float32 (fp32, full precision) is the default floating-point format in ``torch``, whereas float16 (fp16, half precision) is a reduced-precision floating-point format that can speed up inference on GPUs at a minimal loss of model accuracy. To use it, you can specify the ``torch_dtype`` during initialization or call :meth:`model.half() ` on the initialized model: + + .. code-block:: python + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer("all-MiniLM-L6-v2", model_kwargs={"torch_dtype": "float16"}) + # or: model.half() + + sentences = ["This is an example sentence", "Each sentence is converted"] + embeddings = model.encode(sentences) + +.. tab:: bfloat16 (bf16) + + Bfloat16 (bf16) is similar to fp16, but preserves more of the original accuracy of fp32. To use it, you can specify the ``torch_dtype`` during initialization or call :meth:`model.bfloat16() ` on the initialized model: + + .. code-block:: python + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer("all-MiniLM-L6-v2", model_kwargs={"torch_dtype": "bfloat16"}) + # or: model.bfloat16() + + sentences = ["This is an example sentence", "Each sentence is converted"] + embeddings = model.encode(sentences) + +ONNX +---- + +ONNX can be used to speed up inference by converting the model to ONNX format and using ONNX Runtime to run the model. To use the ONNX backend, you must install Sentence Transformers with the ``onnx`` or ``onnx-gpu`` extra for CPU or GPU acceleration, respectively: + +.. code-block:: bash + + pip install sentence-transformers[onnx-gpu] + # or + pip install sentence-transformers[onnx] + +To convert a model to ONNX format, you can use the following code: + +.. code-block:: python + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer("all-MiniLM-L6-v2", backend="onnx") + + sentences = ["This is an example sentence", "Each sentence is converted"] + embeddings = model.encode(sentences) + +If the model path or repository already contains a model in ONNX format, Sentence Transformers will automatically use it. Otherwise, it will convert the model to ONNX the format. + +All keyword arguments passed via ``model_kwargs`` will be passed on to :meth:`ORTModel.from_pretrained `. Some notable arguments include: + +* ``provider``: ONNX Runtime provider to use for loading the model, e.g. ``"CPUExecutionProvider"`` . See https://onnxruntime.ai/docs/execution-providers/ for possible providers. If not specified, the strongest provider (E.g. ``"CUDAExecutionProvider"``) will be used. +* ``file_name``: The name of the ONNX file to load. If not specified, will default to ``"model.onnx"`` or otherwise ``"onnx/model.onnx"``. This argument is useful for specifying optimized or quantized models. +* ``export``: A boolean flag specifying whether the model will be exported. If not provided, ``export`` will be set to ``True`` if the model repository or directory does not already contain an ONNX model. + +.. tip:: + + It's heavily recommended to save the exported model to prevent having to re-export it every time you run your code. You can do this by calling :meth:`model.save_pretrained() ` if your model was local: + + .. code-block:: python + + model = SentenceTransformer("path/to/my/model", backend="onnx") + model.save_pretrained("path/to/my/model") + + or with :meth:`model.push_to_hub() ` if your model was from the Hugging Face Hub: + + .. code-block:: python + + model = SentenceTransformer("intfloat/multilingual-e5-small", backend="onnx") + model.push_to_hub("intfloat/multilingual-e5-small", create_pr=True) + +Optimizing ONNX Models +^^^^^^^^^^^^^^^^^^^^^^ + +ONNX models can be optimized using Optimum, allowing for speedups on CPUs and GPUs alike. To do this, you can use the :func:`~sentence_transformers.backend.export_optimized_onnx_model` function, which saves the optimized in a directory or model repository that you specify. It expects: + +- ``model``: a Sentence Transformer model loaded with the ONNX backend. +- ``optimization_config``: ``"O1"``, ``"O2"``, ``"O3"``, or ``"O4"`` representing optimization levels from :class:`~optimum.onnxruntime.AutoOptimizationConfig`, or an :class:`~optimum.onnxruntime.OptimizationConfig` instance. +- ``model_name_or_path``: a path to save the optimized model file, or the repository name if you want to push it to the Hugging Face Hub. +- ``push_to_hub``: (Optional) a boolean to push the optimized model to the Hugging Face Hub. +- ``create_pr``: (Optional) a boolean to create a pull request when pushing to the Hugging Face Hub. Useful when you don't have write access to the repository. +- ``file_suffix``: (Optional) a string to append to the model name when saving it. If not specified, the optimization level name string will be used, or just ``"optimized"`` if the optimization config was not just a string optimization level. + +See this example for exporting a model with :doc:`optimization level 3 ` (basic and extended general optimizations, transformers-specific fusions, fast Gelu approximation): + +.. tab:: Hugging Face Hub Model + + Only optimize once:: + + from sentence_transformers import SentenceTransformer, export_optimized_onnx_model + + model = SentenceTransformer("all-MiniLM-L6-v2", backend="onnx") + export_optimized_onnx_model(model, "O3", "all-MiniLM-L6-v2", push_to_hub=True, create_pr=True) + + Before the pull request gets merged:: + + from sentence_transformers import SentenceTransformer + + pull_request_nr = 2 # TODO: Update this to the number of your pull request + model = SentenceTransformer( + "all-MiniLM-L6-v2", + backend="onnx", + model_kwargs={"file_name": "onnx/model_O3.onnx"}, + revision=f"refs/pr/{pull_request_nr}" + ) + + Once the pull request gets merged:: + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer( + "all-MiniLM-L6-v2", + backend="onnx", + model_kwargs={"file_name": "onnx/model_O3.onnx"}, + ) + +.. tab:: Local Model + + Only optimize once:: + + from sentence_transformers import SentenceTransformer, export_optimized_onnx_model + + model = SentenceTransformer("path/to/my/mpnet-legal-finetuned", backend="onnx") + export_optimized_onnx_model(model, "O3", "path/to/my/mpnet-legal-finetuned") + + After optimizing:: + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer( + "path/to/my/mpnet-legal-finetuned", + backend="onnx", + model_kwargs={"file_name": "onnx/model_O3.onnx"}, + ) + +Quantizing ONNX Models +^^^^^^^^^^^^^^^^^^^^^^ + +ONNX models can be quantized to int8 precision using Optimum, allowing for faster inference on CPUs. To do this, you can use the :func:`~sentence_transformers.backend.export_dynamic_quantized_onnx_model` function, which saves the quantized in a directory or model repository that you specify. Dynamic quantization, unlike static quantization, does not require a calibration dataset. It expects: + +- ``model``: a Sentence Transformer model loaded with the ONNX backend. +- ``quantization_config``: ``"arm64"``, ``"avx2"``, ``"avx512"``, or ``"avx512_vnni"`` representing quantization configurations from :class:`~optimum.onnxruntime.AutoQuantizationConfig`, or an :class:`~optimum.onnxruntime.QuantizationConfig` instance. +- ``model_name_or_path``: a path to save the quantized model file, or the repository name if you want to push it to the Hugging Face Hub. +- ``push_to_hub``: (Optional) a boolean to push the quantized model to the Hugging Face Hub. +- ``create_pr``: (Optional) a boolean to create a pull request when pushing to the Hugging Face Hub. Useful when you don't have write access to the repository. +- ``file_suffix``: (Optional) a string to append to the model name when saving it. If not specified, ``"qint8_quantized"`` will be used. + +On my CPU, each of the default quantization configurations (``"arm64"``, ``"avx2"``, ``"avx512"``, ``"avx512_vnni"``) resulted in roughly equivalent speedups. + +See this example for quantizing a model to ``int8`` with :doc:`avx512_vnni `: + +.. tab:: Hugging Face Hub Model + + Only quantize once:: + + from sentence_transformers import SentenceTransformer, export_dynamic_quantized_onnx_model + + model = SentenceTransformer("all-MiniLM-L6-v2", backend="onnx") + export_dynamic_quantized_onnx_model(model, "avx512_vnni", "all-MiniLM-L6-v2", push_to_hub=True, create_pr=True) + + Before the pull request gets merged:: + + from sentence_transformers import SentenceTransformer + + pull_request_nr = 2 # TODO: Update this to the number of your pull request + model = SentenceTransformer( + "all-MiniLM-L6-v2", + backend="onnx", + model_kwargs={"file_name": "onnx/model_qint8_avx512_vnni.onnx"}, + revision=f"refs/pr/{pull_request_nr}" + ) + + Once the pull request gets merged:: + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer( + "all-MiniLM-L6-v2", + backend="onnx", + model_kwargs={"file_name": "onnx/model_qint8_avx512_vnni.onnx"}, + ) + +.. tab:: Local Model + + Only quantize once:: + + from sentence_transformers import SentenceTransformer, export_dynamic_quantized_onnx_model + + model = SentenceTransformer("path/to/my/mpnet-legal-finetuned", backend="onnx") + export_dynamic_quantized_onnx_model(model, "O3", "path/to/my/mpnet-legal-finetuned") + + After quantizing:: + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer( + "path/to/my/mpnet-legal-finetuned", + backend="onnx", + model_kwargs={"file_name": "onnx/model_qint8_avx512_vnni.onnx"}, + ) + +OpenVINO +-------- + +OpenVINO allows for accelerated inference on CPUs by exporting the model to the OpenVINO format. To use the OpenVINO backend, you must install Sentence Transformers with the ``openvino`` extra: + +.. code-block:: bash + + pip install sentence-transformers[openvino] + +To convert a model to OpenVINO format, you can use the following code: + +.. code-block:: python + + from sentence_transformers import SentenceTransformer + + model = SentenceTransformer("all-MiniLM-L6-v2", backend="openvino") + + sentences = ["This is an example sentence", "Each sentence is converted"] + embeddings = model.encode(sentences) + +.. raw:: html + + All keyword arguments passed via model_kwargs will be passed on to OVBaseModel.from_pretrained(). Some notable arguments include: + +* ``file_name``: The name of the ONNX file to load. If not specified, will default to ``"openvino_model.xml"`` or otherwise ``"openvino/openvino_model.xml"``. This argument is useful for specifying optimized or quantized models. +* ``export``: A boolean flag specifying whether the model will be exported. If not provided, ``export`` will be set to ``True`` if the model repository or directory does not already contain an OpenVINO model. + +.. tip:: + + It's heavily recommended to save the exported model to prevent having to re-export it every time you run your code. You can do this by calling :meth:`model.save_pretrained() ` if your model was local: + + .. code-block:: python + + model = SentenceTransformer("path/to/my/model", backend="openvino") + model.save_pretrained("path/to/my/model") + + or with :meth:`model.push_to_hub() ` if your model was from the Hugging Face Hub: + + .. code-block:: python + + model = SentenceTransformer("intfloat/multilingual-e5-small", backend="openvino") + model.push_to_hub("intfloat/multilingual-e5-small", create_pr=True) + +Benchmarks +---------- + +The following images show the benchmark results for the different backends on GPUs and CPUs. The results are averaged across 4 models of various sizes, 3 datasets, and numerous batch sizes. + +.. raw:: html + +
+ Expand the benchmark details + +
+ Speedup ratio: + + Performance ratio: The same models and hardware was used. We compare the performance against the performance of PyTorch with fp32, i.e. the default backend and precision. +
    +
  • + Evaluation: +
      +
    • + Semantic Textual Similarity: Spearman rank correlation based on cosine similarity on the sentence-transformers/stsb test set, computed via the EmbeddingSimilarityEvaluator. +
    • +
    • + Information Retrieval: NDCG@10 based on cosine similarity on the entire NanoBEIR collection of datasets, computed via the InformationRetrievalEvaluator. +
    • +
    +
  • +
+ +
    +
  • + Backends: +
      +
    • + torch-fp32: PyTorch with float32 precision (default). +
    • +
    • + torch-fp16: PyTorch with float16 precision, via model_kwargs={"torch_dtype": "float16"}. +
    • +
    • + torch-bf16: PyTorch with bfloat16 precision, via model_kwargs={"torch_dtype": "bfloat16"}. +
    • +
    • + onnx: ONNX with float32 precision, via backend="onnx". +
    • +
    • + onnx-O1: ONNX with float32 precision and O1 optimization, via export_optimized_onnx_model(..., "O1", ...) and backend="onnx". +
    • +
    • + onnx-O2: ONNX with float32 precision and O2 optimization, via export_optimized_onnx_model(..., "O2", ...) and backend="onnx". +
    • +
    • + onnx-O3: ONNX with float32 precision and O3 optimization, via export_optimized_onnx_model(..., "O3", ...) and backend="onnx". +
    • +
    • + onnx-O4: ONNX with float16 precision and O4 optimization, via export_optimized_onnx_model(..., "O4", ...) and backend="onnx". +
    • +
    • + onnx-qint8: ONNX quantized to int8 with "avx512_vnni", via export_dynamic_quantized_onnx_model(..., "avx512_vnni", ...) and backend="onnx". The different quantization configurations resulted in roughly equivalent speedups. +
    • +
    • + openvino: OpenVINO, via backend="openvino". +
    • +
    • + openvino-igpu: OpenVINO, via backend="openvino" and model_kwargs={"device": "GPU"}) to use the iGPU from my CPU. +
    • +
    +
  • +
+ + Note that the aggressive averaging across models, datasets, and batch sizes prevents some more intricate patterns from being visible. For example, for GPUs, if we only consider the stsb dataset with the shortest texts, ONNX becomes better: 1.46x for ONNX, and ONNX-O4 reaches 1.83x whereas fp16 and bf16 reach 1.54x and 1.53x respectively. So, for shorter texts we recommend ONNX on GPU.
+
+ For CPU, ONNX is also stronger for the stsb dataset with the shortest texts: 1.39x for ONNX, outperforming 1.29x for OpenVINO. ONNX with int8 quantization is even stronger with a 3.08x speedup. For longer texts, ONNX and OpenVINO can even perform slightly worse than PyTorch, so we recommend testing the different backends with your specific model and data to find the best one for your use case. + +
+
+ + +.. image:: ../../img/backends_benchmark_gpu.png + :alt: Benchmark for GPUs + :width: 45% + +.. image:: ../../img/backends_benchmark_cpu.png + :alt: Benchmark for CPUs + :width: 45% + +Recommendations +^^^^^^^^^^^^^^^ + +Based on the benchmarks, this flowchart should help you decide which backend to use for your model: + +.. mermaid:: + + %%{init: { + "theme": "neutral", + "flowchart": { + "curve": "bumpY" + } + }}%% + graph TD + A(What is your hardware?) -->|GPU| B(Is your text usually smaller than 500 characters?) + A -->|CPU| C(Is a 0.4% accuracy loss acceptable?) + B -->|yes| D[onnx-O4] + B -->|no| F[float16] + C -->|yes| G[onnx-int8] + C -->|no| H(Do you have an Intel CPU?) + H -->|yes| I[openvino] + H -->|no| J[onnx] + click D "#optimizing-onnx-models" + click F "#pytorch" + click G "#quantizing-onnx-models" + click I "#openvino" + click J "#onnx" + +.. note:: + + Your milage may vary, and you should always test the different backends with your specific model and data to find the best one for your use case. \ No newline at end of file diff --git a/docs/sentence_transformer/usage/usage.rst b/docs/sentence_transformer/usage/usage.rst index c7cddc0e6..a0c0f7e17 100644 --- a/docs/sentence_transformer/usage/usage.rst +++ b/docs/sentence_transformer/usage/usage.rst @@ -56,5 +56,6 @@ Once you have `installed <../../installation.html>`_ Sentence Transformers, you ../../../examples/applications/parallel-sentence-mining/README ../../../examples/applications/image-search/README ../../../examples/applications/embedding-quantization/README - custom_models.md + efficiency + custom_models diff --git a/index.rst b/index.rst index 980701531..3c5b1cc1f 100644 --- a/index.rst +++ b/index.rst @@ -1,6 +1,6 @@ -.. note:: +.. tip:: - Sentence Transformers v3.0 just released, introducing a new training API for Sentence Transformer models. Read `SentenceTransformer > Training Overview `_ to learn more about the training API, and check out `v3.0 Release Notes `_ for details on the other changes. + Sentence Transformers v3.2 just released, introducing the ONNX and OpenVINO backends for Sentence Transformer models. Read `SentenceTransformer > Usage > Speeding up Inference `_ to learn more about the new backends and what they can mean for your inference speed. SentenceTransformers Documentation ================================== @@ -63,6 +63,7 @@ Consider reading one of the following sections to answer the related questions: * How to **use** Sentence Transformer models? `Sentence Transformers > Usage `_ * What Sentence Transformer **models** can I use? `Sentence Transformers > Pretrained Models `_ +* How do I make Sentence Transformer models **faster**? `Sentence Transformers > Usage > Speeding up Inference `_ * How do I **train/finetune** a Sentence Transformer model? `Sentence Transformers > Training Overview `_ * How to **use** Cross Encoder models? `Cross Encoder > Usage `_ * What Cross Encoder **models** can I use? `Cross Encoder > Pretrained Models `_ diff --git a/pyproject.toml b/pyproject.toml index 092f334c2..8771d81a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,12 +33,12 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", ] dependencies = [ - "transformers>=4.38.0,<5.0.0", + "transformers>=4.41.0,<5.0.0", "tqdm", "torch>=1.11.0", "scikit-learn", "scipy", - "huggingface-hub>=0.19.3", + "huggingface-hub>=0.20.0", "Pillow", ] @@ -49,6 +49,9 @@ Repository = "https://github.com/UKPLab/sentence-transformers/" [project.optional-dependencies] train = ["datasets", "accelerate>=0.20.3"] +onnx = ["optimum[onnxruntime]>=1.23.0"] +onnx-gpu = ["optimum[onnxruntime-gpu]>=1.23.0"] +openvino = ["optimum-intel[openvino]>=1.20.0"] dev = ["datasets", "accelerate>=0.20.3", "pre-commit", "pytest", "pytest-cov"] [build-system] diff --git a/sentence_transformers/SentenceTransformer.py b/sentence_transformers/SentenceTransformer.py index 26fb32126..1a8cb2efb 100644 --- a/sentence_transformers/SentenceTransformer.py +++ b/sentence_transformers/SentenceTransformer.py @@ -104,6 +104,13 @@ class SentenceTransformer(nn.Sequential, FitMixin): or `"flash_attention_2"` (using `Dao-AILab/flash-attention `_). By default, if available, SDPA will be used for torch>=2.1.1. The default is otherwise the manual `"eager"` implementation. + - ``provider``: If backend is "onnx", this is the provider to use for inference, for example "CPUExecutionProvider", + "CUDAExecutionProvider", etc. See https://onnxruntime.ai/docs/execution-providers/ for all ONNX execution providers. + - ``file_name``: If backend is "onnx" or "openvino", this is the file name to load, useful for loading optimized + or quantized ONNX or OpenVINO models. + - ``export``: If backend is "onnx" or "openvino", then this is a boolean flag specifying whether this model should + be exported to the backend. If not specified, the model will be exported only if the model repository or directory + does not already contain an exported model. See the `PreTrainedModel.from_pretrained `_ @@ -119,6 +126,9 @@ class SentenceTransformer(nn.Sequential, FitMixin): model_card_data (:class:`~sentence_transformers.model_card.SentenceTransformerModelCardData`, optional): A model card data object that contains information about the model. This is used to generate a model card when saving the model. If not set, a default model card data object is created. + backend (str): The backend to use for inference. Can be one of "torch" (default), "onnx", or "openvino". + See https://sbert.net/docs/sentence_transformer/usage/efficiency.html for benchmarking information + on the different backends. Example: :: @@ -165,6 +175,7 @@ def __init__( tokenizer_kwargs: dict[str, Any] | None = None, config_kwargs: dict[str, Any] | None = None, model_card_data: SentenceTransformerModelCardData | None = None, + backend: Literal["torch", "onnx", "openvino"] = "torch", ) -> None: # Note: self._load_sbert_model can also update `self.prompts` and `self.default_prompt_name` self.prompts = prompts or {} @@ -177,6 +188,7 @@ def __init__( self._model_card_vars = {} self._model_card_text = None self._model_config = {} + self.backend = backend if use_auth_token is not None: warnings.warn( "The `use_auth_token` argument is deprecated and will be removed in v4 of SentenceTransformers.", @@ -368,6 +380,14 @@ def __init__( # Pass the model to the model card data for later use in generating a model card upon saving this model self.model_card_data.register_model(self) + def get_backend(self) -> Literal["torch", "onnx", "openvino"]: + """Return the backend used for inference, which can be one of "torch", "onnx", or "openvino". + + Returns: + str: The backend used for inference. + """ + return self.backend + @overload def encode( self, @@ -1313,12 +1333,13 @@ def push_to_hub( token: str | None = None, private: bool | None = None, safe_serialization: bool = True, - commit_message: str = "Add new SentenceTransformer model.", + commit_message: str | None = None, local_model_path: str | None = None, exist_ok: bool = False, replace_model_card: bool = False, train_datasets: list[str] | None = None, revision: str | None = None, + create_pr: bool = False, ) -> str: """ Uploads all elements of this Sentence Transformer to a new HuggingFace Hub repository. @@ -1334,6 +1355,7 @@ def push_to_hub( replace_model_card (bool, optional): If true, replace an existing model card in the hub with the automatically created model card train_datasets (List[str], optional): Datasets used to train the model. If set, the datasets will be added to the model card in the Hub. revision (str, optional): Branch to push the uploaded files to + create_pr (bool, optional): If True, create a pull request instead of pushing directly to the main branch Returns: str: The url of the commit of your model in the repository on the Hugging Face Hub. @@ -1343,20 +1365,67 @@ def push_to_hub( repo_id=repo_id, private=private, repo_type=None, - exist_ok=exist_ok, + exist_ok=exist_ok or create_pr, ) repo_id = repo_url.repo_id # Update the repo_id in case the old repo_id didn't contain a user or organization self.model_card_data.set_model_id(repo_id) if revision is not None: api.create_branch(repo_id=repo_id, branch=revision, exist_ok=True) + + if commit_message is None: + backend = self.get_backend() + if backend == "torch": + commit_message = "Add new SentenceTransformer model" + else: + commit_message = f"Add new SentenceTransformer model with an {backend} backend" + + commit_description = "" + if create_pr: + commit_description = f"""\ +Hello! + +*This pull request has been automatically generated from the [`push_to_hub`](https://sbert.net/docs/package_reference/sentence_transformer/SentenceTransformer.html#sentence_transformers.SentenceTransformer.push_to_hub) method from the Sentence Transformers library.* + +## Full Model Architecture: +``` +{self} +``` + +## Tip: +Consider testing this pull request before merging by loading the model from this PR with the `revision` argument: +```python +from sentence_transformers import SentenceTransformer + +# TODO: Fill in the PR number +pr_number = 2 +model = SentenceTransformer( + "{repo_id}", + revision=f"refs/pr/{{pr_number}}", + backend="{self.get_backend()}", +) + +# Verify that everything works as expected +embeddings = model.encode(["The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium."]) +print(embeddings.shape) + +similarities = model.similarity(embeddings, embeddings) +print(similarities) +``` +""" + if local_model_path: folder_url = api.upload_folder( - repo_id=repo_id, folder_path=local_model_path, commit_message=commit_message, revision=revision + repo_id=repo_id, + folder_path=local_model_path, + commit_message=commit_message, + commit_description=commit_description, + revision=revision, + create_pr=create_pr, ) else: with tempfile.TemporaryDirectory() as tmp_dir: create_model_card = replace_model_card or not os.path.exists(os.path.join(tmp_dir, "README.md")) - self.save( + self.save_pretrained( tmp_dir, model_name=repo_url.repo_id, create_model_card=create_model_card, @@ -1364,18 +1433,17 @@ def push_to_hub( safe_serialization=safe_serialization, ) folder_url = api.upload_folder( - repo_id=repo_id, folder_path=tmp_dir, commit_message=commit_message, revision=revision + repo_id=repo_id, + folder_path=tmp_dir, + commit_message=commit_message, + commit_description=commit_description, + revision=revision, + create_pr=create_pr, ) - refs = api.list_repo_refs(repo_id=repo_id) - for branch in refs.branches: - if revision is None and branch.name == "main": - return f"https://huggingface.co/{repo_id}/commit/{branch.target_commit}" - elif branch.name == revision: - return f"https://huggingface.co/{repo_id}/commit/{branch.target_commit}" - - # This isn't expected to ever be reached. - return folder_url + if create_pr: + return folder_url.pr_url + return folder_url.commit_url def _text_length(self, text: list[int] | list[list[int]]) -> int: """ @@ -1457,13 +1525,19 @@ def _load_auto_model( model_args=model_kwargs, tokenizer_args=tokenizer_kwargs, config_args=config_kwargs, + backend=self.backend, ) pooling_model = Pooling(transformer_model.get_word_embedding_dimension(), "mean") self.model_card_data.set_base_model(model_name_or_path, revision=revision) return [transformer_model, pooling_model] def _load_module_class_from_ref( - self, class_ref: str, model_name_or_path: str, trust_remote_code: bool, model_kwargs: dict[str, Any] | None + self, + class_ref: str, + model_name_or_path: str, + trust_remote_code: bool, + revision: str | None, + model_kwargs: dict[str, Any] | None, ) -> nn.Module: # If the class is from sentence_transformers, we can directly import it, # otherwise, we try to import it dynamically, and if that fails, we fall back to the default import @@ -1476,6 +1550,7 @@ def _load_module_class_from_ref( return get_class_from_dynamic_module( class_ref, model_name_or_path, + revision=revision, code_revision=code_revision, ) except OSError: @@ -1578,7 +1653,7 @@ def _load_sbert_model( for module_config in modules_config: class_ref = module_config["type"] module_class = self._load_module_class_from_ref( - class_ref, model_name_or_path, trust_remote_code, model_kwargs + class_ref, model_name_or_path, trust_remote_code, revision, model_kwargs ) # For Transformer, don't load the full directory, rely on `transformers` instead @@ -1643,10 +1718,10 @@ def _load_sbert_model( # Try to initialize the module with a lot of kwargs, but only if the module supports them # Otherwise we fall back to the load method - try: - module = module_class(model_name_or_path, cache_dir=cache_folder, **kwargs) - except TypeError: - module = module_class.load(model_name_or_path) + # try: + module = module_class(model_name_or_path, cache_dir=cache_folder, backend=self.backend, **kwargs) + # except TypeError: + # module = module_class.load(model_name_or_path) else: # Normalize does not require any files to be loaded if module_class == Normalize: @@ -1684,6 +1759,9 @@ def device(self) -> device: Get torch.device from module, assuming that the whole module has one device. In case there are no PyTorch parameters, fall back to CPU. """ + if isinstance(self[0], Transformer): + return self[0].auto_model.device + try: return next(self.parameters()).device except StopIteration: diff --git a/sentence_transformers/__init__.py b/sentence_transformers/__init__.py index 1d6c5f0f5..2c382bdb9 100644 --- a/sentence_transformers/__init__.py +++ b/sentence_transformers/__init__.py @@ -6,6 +6,7 @@ import importlib import os +from sentence_transformers.backend import export_dynamic_quantized_onnx_model, export_optimized_onnx_model from sentence_transformers.cross_encoder.CrossEncoder import CrossEncoder from sentence_transformers.datasets import ParallelSentencesDataset, SentencesDataset from sentence_transformers.LoggingHandler import LoggingHandler @@ -34,4 +35,6 @@ "SentenceTransformerTrainingArguments", "SentenceTransformerModelCardData", "quantize_embeddings", + "export_optimized_onnx_model", + "export_dynamic_quantized_onnx_model", ] diff --git a/sentence_transformers/backend.py b/sentence_transformers/backend.py new file mode 100644 index 000000000..eef76352e --- /dev/null +++ b/sentence_transformers/backend.py @@ -0,0 +1,261 @@ +from __future__ import annotations + +import logging +import os +import shutil +import tempfile +from pathlib import Path +from typing import TYPE_CHECKING, Callable, Literal + +import huggingface_hub + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from sentence_transformers.SentenceTransformer import SentenceTransformer + + try: + from optimum.onnxruntime.configuration import OptimizationConfig, QuantizationConfig + except ImportError: + pass + + +def export_optimized_onnx_model( + model: SentenceTransformer, + optimization_config: OptimizationConfig | Literal["O1", "O2", "O3", "O4"], + model_name_or_path: str, + push_to_hub: bool = False, + create_pr: bool = False, + file_suffix: str | None = None, +) -> None: + """ + Export an optimized ONNX model from a SentenceTransformer model. + + The O1-O4 optimization levels are defined by Optimum and are documented here: + https://huggingface.co/docs/optimum/main/en/onnxruntime/usage_guides/optimization + + The optimization levels are: + + - O1: basic general optimizations. + - O2: basic and extended general optimizations, transformers-specific fusions. + - O3: same as O2 with GELU approximation. + - O4: same as O3 with mixed precision (fp16, GPU-only) + + See https://sbert.net/docs/sentence_transformer/usage/efficiency.html for more information & benchmarks. + + Args: + model (SentenceTransformer): The SentenceTransformer model to be optimized. Must be loaded with `backend="onnx"`. + optimization_config (OptimizationConfig | Literal["O1", "O2", "O3", "O4"]): The optimization configuration or level. + model_name_or_path (str): The path or Hugging Face Hub repository name where the optimized model will be saved. + push_to_hub (bool, optional): Whether to push the optimized model to the Hugging Face Hub. Defaults to False. + create_pr (bool, optional): Whether to create a pull request when pushing to the Hugging Face Hub. Defaults to False. + file_suffix (str | None, optional): The suffix to add to the optimized model file name. Defaults to None. + + Raises: + ImportError: If the required packages `optimum` and `onnxruntime` are not installed. + ValueError: If the provided model is not a valid SentenceTransformer model loaded with `backend="onnx"`. + ValueError: If the provided optimization_config is not valid. + + Returns: + None + """ + from sentence_transformers import SentenceTransformer + from sentence_transformers.models.Transformer import Transformer + + try: + from optimum.onnxruntime import ORTModelForFeatureExtraction, ORTOptimizer + from optimum.onnxruntime.configuration import AutoOptimizationConfig + except ImportError: + raise ImportError( + "Please install Optimum and ONNX Runtime to use this function. " + "You can install them with pip: `pip install optimum[onnxruntime]` " + "or `pip install optimum[onnxruntime-gpu]`" + ) + + if ( + not isinstance(model, SentenceTransformer) + or not len(model) + or not isinstance(model[0], Transformer) + or not isinstance(model[0].auto_model, ORTModelForFeatureExtraction) + ): + raise ValueError('The model must be a SentenceTransformer model loaded with `backend="onnx"`.') + + ort_model: ORTModelForFeatureExtraction = model[0].auto_model + optimizer = ORTOptimizer.from_pretrained(ort_model) + + if isinstance(optimization_config, str): + if optimization_config not in AutoOptimizationConfig._LEVELS: + raise ValueError( + "optimization_config must be an OptimizationConfig instance or one of 'O1', 'O2', 'O3', 'O4'." + ) + + file_suffix = file_suffix or optimization_config + optimization_config = getattr(AutoOptimizationConfig, optimization_config)() + + if file_suffix is None: + file_suffix = "optimized" + + save_or_push_to_hub_onnx_model( + export_function=lambda save_dir: optimizer.optimize(optimization_config, save_dir, file_suffix=file_suffix), + export_function_name="export_optimized_onnx_model", + config=optimization_config, + model_name_or_path=model_name_or_path, + push_to_hub=push_to_hub, + create_pr=create_pr, + file_suffix=file_suffix, + ) + + +def export_dynamic_quantized_onnx_model( + model: SentenceTransformer, + quantization_config: QuantizationConfig | Literal["arm64", "avx2", "avx512", "avx512_vnni"], + model_name_or_path: str, + push_to_hub: bool = False, + create_pr: bool = False, + file_suffix: str | None = None, +) -> None: + """ + Export a quantized ONNX model from a SentenceTransformer model. + + This function applies dynamic quantization, i.e. without a calibration dataset. + Each of the default quantization configurations quantize the model to int8, allowing + for faster inference on CPUs, but are likely slower on GPUs. + + See https://sbert.net/docs/sentence_transformer/usage/efficiency.html for more information & benchmarks. + + Args: + model (SentenceTransformer): The SentenceTransformer model to be quantized. Must be loaded with `backend="onnx"`. + quantization_config (QuantizationConfig): The quantization configuration. + model_name_or_path (str): The path or Hugging Face Hub repository name where the quantized model will be saved. + push_to_hub (bool, optional): Whether to push the quantized model to the Hugging Face Hub. Defaults to False. + create_pr (bool, optional): Whether to create a pull request when pushing to the Hugging Face Hub. Defaults to False. + file_suffix (str | None, optional): The suffix to add to the quantized model file name. Defaults to None. + + Raises: + ImportError: If the required packages `optimum` and `onnxruntime` are not installed. + ValueError: If the provided model is not a valid SentenceTransformer model loaded with `backend="onnx"`. + ValueError: If the provided quantization_config is not valid. + + Returns: + None + """ + from sentence_transformers import SentenceTransformer + from sentence_transformers.models.Transformer import Transformer + + try: + from optimum.onnxruntime import ORTModelForFeatureExtraction, ORTQuantizer + from optimum.onnxruntime.configuration import AutoQuantizationConfig + except ImportError: + raise ImportError( + "Please install Optimum and ONNX Runtime to use this function. " + "You can install them with pip: `pip install optimum[onnxruntime]` " + "or `pip install optimum[onnxruntime-gpu]`" + ) + + if ( + not isinstance(model, SentenceTransformer) + or not len(model) + or not isinstance(model[0], Transformer) + or not isinstance(model[0].auto_model, ORTModelForFeatureExtraction) + ): + raise ValueError('The model must be a SentenceTransformer model loaded with `backend="onnx"`.') + + ort_model: ORTModelForFeatureExtraction = model[0].auto_model + quantizer = ORTQuantizer.from_pretrained(ort_model) + + if isinstance(quantization_config, str): + if quantization_config not in ["arm64", "avx2", "avx512", "avx512_vnni"]: + raise ValueError( + "quantization_config must be an QuantizationConfig instance or one of 'arm64', 'avx2', 'avx512', or 'avx512_vnni'." + ) + + quantization_config_name = quantization_config[:] + quantization_config = getattr(AutoQuantizationConfig, quantization_config)(is_static=False) + file_suffix = file_suffix or f"{quantization_config.weights_dtype.name.lower()}_{quantization_config_name}" + + if file_suffix is None: + file_suffix = f"{quantization_config.weights_dtype.name.lower()}_quantized" + + save_or_push_to_hub_onnx_model( + export_function=lambda save_dir: quantizer.quantize(quantization_config, save_dir, file_suffix=file_suffix), + export_function_name="export_dynamic_quantized_onnx_model", + config=quantization_config, + model_name_or_path=model_name_or_path, + push_to_hub=push_to_hub, + create_pr=create_pr, + file_suffix=file_suffix, + ) + + +def save_or_push_to_hub_onnx_model( + export_function: Callable, + export_function_name: str, + config, + model_name_or_path: str, + push_to_hub: bool = False, + create_pr: bool = False, + file_suffix: str | None = None, +): + if push_to_hub: + with tempfile.TemporaryDirectory() as save_dir: + export_function(save_dir) + file_name = f"model_{file_suffix}.onnx" + source = (Path(save_dir) / file_name).as_posix() + destination = (Path("onnx") / file_name).as_posix() + + commit_description = "" + if create_pr: + opt_config_string = repr(config).replace("(", "(\n\t").replace(", ", ",\n\t").replace(")", "\n)") + commit_description = f"""\ +Hello! + +*This pull request has been automatically generated from the [`{export_function_name}`](https://sbert.net/docs/package_reference/util.html#sentence_transformers.backend.{export_function_name}) function from the Sentence Transformers library.* + +## Config +```python +{opt_config_string} +``` + +## Tip: +Consider testing this pull request before merging by loading the model from this PR with the `revision` argument: +```python +from sentence_transformers import SentenceTransformer + +# TODO: Fill in the PR number +pr_number = 2 +model = SentenceTransformer( + "{model_name_or_path}", + revision=f"refs/pr/{{pr_number}}", + backend="onnx", + model_kwargs={{"file_name": "{destination}"}}, +) + +# Verify that everything works as expected +embeddings = model.encode(["The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium."]) +print(embeddings.shape) + +similarities = model.similarity(embeddings, embeddings) +print(similarities) +``` +""" + + huggingface_hub.upload_file( + path_or_fileobj=source, + path_in_repo=destination, + repo_id=model_name_or_path, + repo_type="model", + commit_message=f"Add exported ONNX model {file_name!r}", + commit_description=commit_description, + create_pr=create_pr, + ) + + else: + with tempfile.TemporaryDirectory() as save_dir: + export_function(save_dir) + + file_name = f"model_{file_suffix}.onnx" + source = os.path.join(save_dir, file_name) + destination = os.path.join(model_name_or_path, "onnx", file_name) + # Create destination if it does not exist + os.makedirs(os.path.dirname(destination), exist_ok=True) + shutil.copy(source, destination) diff --git a/sentence_transformers/models/Transformer.py b/sentence_transformers/models/Transformer.py index 2d3786e15..7592278bf 100644 --- a/sentence_transformers/models/Transformer.py +++ b/sentence_transformers/models/Transformer.py @@ -1,13 +1,27 @@ from __future__ import annotations import json +import logging import os -from typing import Any +from fnmatch import fnmatch +from pathlib import Path +from typing import Any, Callable +import huggingface_hub import torch from torch import nn from transformers import AutoConfig, AutoModel, AutoTokenizer, MT5Config, T5Config +logger = logging.getLogger(__name__) + + +def _save_pretrained_wrapper(_save_pretrained_fn: Callable, subfolder: str) -> Callable[..., None]: + def wrapper(save_directory: str | Path, **kwargs) -> None: + os.makedirs(Path(save_directory) / subfolder, exist_ok=True) + return _save_pretrained_fn(Path(save_directory) / subfolder, **kwargs) + + return wrapper + class Transformer(nn.Module): """Hugging Face AutoModel to generate token embeddings. @@ -29,6 +43,8 @@ class Transformer(nn.Module): model is cased or not) tokenizer_name_or_path: Name or path of the tokenizer. When None, then model_name_or_path is used + backend: Backend used for model inference. Can be `torch`, `onnx`, + or `openvino`. Default is `torch`. """ save_in_root: bool = True @@ -43,10 +59,12 @@ def __init__( cache_dir: str | None = None, do_lower_case: bool = False, tokenizer_name_or_path: str = None, + backend: str = "torch", ) -> None: super().__init__() self.config_keys = ["max_seq_length", "do_lower_case"] self.do_lower_case = do_lower_case + self.backend = backend if model_args is None: model_args = {} if tokenizer_args is None: @@ -55,7 +73,7 @@ def __init__( config_args = {} config = AutoConfig.from_pretrained(model_name_or_path, **config_args, cache_dir=cache_dir) - self._load_model(model_name_or_path, config, cache_dir, **model_args) + self._load_model(model_name_or_path, config, cache_dir, backend, **model_args) if max_seq_length is not None and "model_max_length" not in tokenizer_args: tokenizer_args["model_max_length"] = max_seq_length @@ -79,16 +97,228 @@ def __init__( if tokenizer_name_or_path is not None: self.auto_model.config.tokenizer_class = self.tokenizer.__class__.__name__ - def _load_model(self, model_name_or_path, config, cache_dir, **model_args) -> None: + def _load_model(self, model_name_or_path, config, cache_dir, backend, **model_args) -> None: """Loads the transformer model""" - if isinstance(config, T5Config): - self._load_t5_model(model_name_or_path, config, cache_dir, **model_args) - elif isinstance(config, MT5Config): - self._load_mt5_model(model_name_or_path, config, cache_dir, **model_args) + if backend == "torch": + if isinstance(config, T5Config): + self._load_t5_model(model_name_or_path, config, cache_dir, **model_args) + elif isinstance(config, MT5Config): + self._load_mt5_model(model_name_or_path, config, cache_dir, **model_args) + else: + self.auto_model = AutoModel.from_pretrained( + model_name_or_path, config=config, cache_dir=cache_dir, **model_args + ) + elif backend == "onnx": + self._load_onnx_model(model_name_or_path, config, cache_dir, **model_args) + elif backend == "openvino": + self._load_openvino_model(model_name_or_path, config, cache_dir, **model_args) + else: + raise ValueError(f"Unsupported backend '{backend}'. `backend` should be `torch`, `onnx`, or `openvino`.") + + def _load_openvino_model(self, model_name_or_path, config, cache_dir, **model_args) -> None: + if isinstance(config, T5Config) or isinstance(config, MT5Config): + raise ValueError("T5 models are not yet supported by the OpenVINO backend.") + + try: + from optimum.intel import OVModelForFeatureExtraction + from optimum.intel.openvino import OV_XML_FILE_NAME + except ModuleNotFoundError: + raise Exception( + "Using the OpenVINO backend requires installing Optimum and OpenVINO. " + "You can install them with pip: `pip install optimum[openvino]`." + ) + + load_path = Path(model_name_or_path) + is_local = load_path.exists() + backend_name = "OpenVINO" + target_file_glob = "openvino*.xml" + + # Determine whether the model should be exported or whether we can load it directly + export, model_args = self._backend_should_export( + load_path, is_local, model_args, OV_XML_FILE_NAME, target_file_glob, backend_name + ) + + # If we're exporting, then there's no need for a file_name to load the model from + if export: + model_args.pop("file_name", None) + + # ov_config can be either a dictionary, or point to a json file with an OpenVINO config + if "ov_config" in model_args: + ov_config = model_args["ov_config"] + if not isinstance(ov_config, dict): + if not Path(ov_config).exists(): + raise ValueError( + "ov_config should be a dictionary or a path to a .json file containing an OpenVINO config" + ) + with open(ov_config, encoding="utf-8") as f: + model_args["ov_config"] = json.load(f) + else: + model_args["ov_config"] = {} + + # Either load an exported model, or export the model to ONNX + self.auto_model: OVModelForFeatureExtraction = OVModelForFeatureExtraction.from_pretrained( + model_name_or_path, + config=config, + cache_dir=cache_dir, + export=export, + **model_args, + ) + # Wrap the save_pretrained method to save the model in the correct subfolder + self.auto_model._save_pretrained = _save_pretrained_wrapper(self.auto_model._save_pretrained, self.backend) + + # Warn the user to save the model if they haven't already + if export: + self._backend_warn_to_save(model_name_or_path, is_local, backend_name) + + def _load_onnx_model(self, model_name_or_path, config, cache_dir, **model_args) -> None: + try: + import onnxruntime as ort + from optimum.onnxruntime import ONNX_WEIGHTS_NAME, ORTModelForFeatureExtraction + except ModuleNotFoundError: + raise Exception( + "Using the ONNX backend requires installing Optimum and ONNX Runtime. " + "You can install them with pip: `pip install optimum[onnxruntime]` " + "or `pip install optimum[onnxruntime-gpu]`" + ) + + # Default to the highest priority available provider if not specified + # E.g. Tensorrt > CUDA > CPU + model_args["provider"] = model_args.pop("provider", ort.get_available_providers()[0]) + + load_path = Path(model_name_or_path) + is_local = load_path.exists() + backend_name = "ONNX" + target_file_glob = "*.onnx" + + # Determine whether the model should be exported or whether we can load it directly + export, model_args = self._backend_should_export( + load_path, is_local, model_args, ONNX_WEIGHTS_NAME, target_file_glob, backend_name + ) + + # If we're exporting, then there's no need for a file_name to load the model from + if export: + model_args.pop("file_name", None) + + # Either load an exported model, or export the model to ONNX + self.auto_model: ORTModelForFeatureExtraction = ORTModelForFeatureExtraction.from_pretrained( + model_name_or_path, + config=config, + cache_dir=cache_dir, + export=export, + **model_args, + ) + # Wrap the save_pretrained method to save the model in the correct subfolder + self.auto_model._save_pretrained = _save_pretrained_wrapper(self.auto_model._save_pretrained, self.backend) + + # Warn the user to save the model if they haven't already + if export: + self._backend_warn_to_save(model_name_or_path, is_local, backend_name) + + def _backend_should_export( + self, + load_path: Path, + is_local: bool, + model_args: dict[str, Any], + target_file_name: str, + target_file_glob: str, + backend_name: str, + ) -> tuple[bool, dict[str, Any]]: + """ + Determines whether the model should be exported to the backend, or if it can be loaded directly. + Also update the `file_name` and `subfolder` model_args if necessary. + + These are the cases: + + 1. If export is set in model_args, just return export + 2. If `/` exists; set export to False + 3. If `/` exists; set export to False and set subfolder to the backend (e.g. "onnx") + 4. If `` contains a folder, add those folders to the subfolder and set the file_name to the last part + + We will warn if: + + 1. The expected file does not exist in the model directory given the optional file_name and subfolder. + If there are valid files for this backend, but they're don't align with file_name, then we give a useful warning. + 2. Multiple files are found in the model directory that match the target file name and the user did not + specify the desired file name via `model_kwargs={"file_name": ""}` + + Args: + load_path: The model repository or directory, as a Path instance + is_local: Whether the model is local or remote, i.e. whether load_path is a local directory + model_args: The model_args dictionary. Notable keys are "export", "file_name", and "subfolder" + target_file_name: The expected file name in the model directory, e.g. "model.onnx" or "openvino_model.xml" + target_file_glob: The glob pattern to match the target file name, e.g. "*.onnx" or "openvino*.xml" + backend_name: The human-readable name of the backend for use in warnings, e.g. "ONNX" or "OpenVINO" + + Returns: + Tuple[bool, dict[str, Any]]: A tuple of the export boolean and the updated model_args dictionary. + """ + + export = model_args.pop("export", None) + if export is not None: + return export, model_args + + file_name = model_args.get("file_name", target_file_name) + subfolder = model_args.get("subfolder", None) + primary_full_path = Path(subfolder, file_name).as_posix() if subfolder else file_name + secondary_full_path = ( + Path(subfolder, self.backend, file_name).as_posix() + if subfolder + else Path(self.backend, file_name).as_posix() + ) + glob_pattern = f"{subfolder}/**/{target_file_glob}" if subfolder else f"**/{target_file_glob}" + + # Get the list of files in the model directory that match the target file name + if is_local: + model_file_names = [path.relative_to(load_path).as_posix() for path in load_path.glob(glob_pattern)] else: - self.auto_model = AutoModel.from_pretrained( - model_name_or_path, config=config, cache_dir=cache_dir, **model_args + all_files = huggingface_hub.list_repo_files( + load_path.as_posix(), + repo_type="model", + revision=model_args.get("revision", None), + token=model_args.get("token", None), + ) + model_file_names = [fname for fname in all_files if fnmatch(fname, glob_pattern)] + + # First check if the expected file exists in the root of the model directory + # If it doesn't, check if it exists in the backend subfolder. + # If it does, set the subfolder to include the backend + export = primary_full_path not in model_file_names + if export and "subfolder" not in model_args: + export = secondary_full_path not in model_file_names + if not export: + if len(model_file_names) > 1 and "file_name" not in model_args: + logger.warning( + f"Multiple {backend_name} files found in {load_path.as_posix()!r}: {model_file_names}, defaulting to {secondary_full_path!r}. " + f'Please specify the desired file name via `model_kwargs={{"file_name": ""}}`.' + ) + model_args["subfolder"] = self.backend + model_args["file_name"] = file_name + + # If the file_name contains subfolders, set it as the subfolder instead + file_name_parts = Path(file_name).parts + if len(file_name_parts) > 1: + model_args["file_name"] = file_name_parts[-1] + model_args["subfolder"] = Path(model_args.get("subfolder", ""), *file_name_parts[:-1]).as_posix() + + if export: + logger.warning( + f"No {file_name!r} found in {load_path.as_posix()!r}. Exporting the model to {backend_name}." ) + if model_file_names: + logger.warning( + f"If you intended to load one of the {model_file_names} {backend_name} files, " + f'please specify the desired file name via `model_kwargs={{"file_name": "{model_file_names[0]}"}}`.' + ) + + return export, model_args + + def _backend_warn_to_save(self, model_name_or_path: str, is_local: str, backend_name: str) -> None: + to_log = f"Saving the exported {backend_name} model is heavily recommended to avoid having to export it again." + if is_local: + to_log += f" Do so with `model.save_pretrained({model_name_or_path!r})`." + else: + to_log += f" Do so with `model.push_to_hub({model_name_or_path!r}, create_pr=True)`." + logger.warning(to_log) def _load_t5_model(self, model_name_or_path, config, cache_dir, **model_args) -> None: """Loads the encoder model from T5""" diff --git a/sentence_transformers/util.py b/sentence_transformers/util.py index 13f67d86e..bb4238aae 100644 --- a/sentence_transformers/util.py +++ b/sentence_transformers/util.py @@ -1303,8 +1303,8 @@ def is_sentence_transformer_model( load_file_path( model_name_or_path, "modules.json", - token, - cache_folder, + token=token, + cache_folder=cache_folder, revision=revision, local_files_only=local_files_only, ) @@ -1314,8 +1314,8 @@ def is_sentence_transformer_model( def load_file_path( model_name_or_path: str, filename: str, - token: bool | str | None, - cache_folder: str | None, + token: bool | str | None = None, + cache_folder: str | None = None, revision: str | None = None, local_files_only: bool = False, ) -> str | None: @@ -1356,8 +1356,8 @@ def load_file_path( def load_dir_path( model_name_or_path: str, directory: str, - token: bool | str | None, - cache_folder: str | None, + token: bool | str | None = None, + cache_folder: str | None = None, revision: str | None = None, local_files_only: bool = False, ) -> str | None: diff --git a/tests/conftest.py b/tests/conftest.py index 2ec25e60d..c505dd49b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,6 +23,16 @@ def stsb_bert_tiny_model_reused() -> SentenceTransformer: return SentenceTransformer("sentence-transformers-testing/stsb-bert-tiny-safetensors") +@pytest.fixture() +def stsb_bert_tiny_model_onnx() -> SentenceTransformer: + return SentenceTransformer("sentence-transformers-testing/stsb-bert-tiny-onnx") + + +@pytest.fixture() +def stsb_bert_tiny_model_openvino() -> SentenceTransformer: + return SentenceTransformer("sentence-transformers-testing/stsb-bert-tiny-openvino") + + @pytest.fixture() def paraphrase_distilroberta_base_v1_model() -> SentenceTransformer: return SentenceTransformer("paraphrase-distilroberta-base-v1") diff --git a/tests/test_backends.py b/tests/test_backends.py new file mode 100644 index 000000000..8b4a13966 --- /dev/null +++ b/tests/test_backends.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import gc +import json +import os +import tempfile +from pathlib import Path + +import numpy as np +import pytest + +try: + from optimum.intel import OVModelForFeatureExtraction + from optimum.onnxruntime import ORTModelForFeatureExtraction +except ImportError: + pytest.skip("OpenVINO and ONNX backends are not available", allow_module_level=True) + +from sentence_transformers import SentenceTransformer + + +## Testing exporting: +@pytest.mark.parametrize( + ["backend", "expected_auto_model_class"], + [ + ("onnx", ORTModelForFeatureExtraction), + ("openvino", OVModelForFeatureExtraction), + ], +) +@pytest.mark.parametrize( + "model_kwargs", [{}, {"file_name": "wrong_file_name"}] +) # <- Using a file_name is fine when exporting +def test_backend_export(backend, expected_auto_model_class, model_kwargs) -> None: + model = SentenceTransformer( + "sentence-transformers-testing/stsb-bert-tiny-safetensors", backend=backend, model_kwargs=model_kwargs + ) + assert model.get_backend() == backend + assert isinstance(model[0].auto_model, expected_auto_model_class) + embedding = model.encode("Hello, World!") + assert embedding.shape == (model.get_sentence_embedding_dimension(),) + + +def test_backend_no_export_crash(): + # ONNX Crashes when it can't export & the model repo/path doesn't contain an exported model + with pytest.raises(OSError): + SentenceTransformer( + "sentence-transformers-testing/stsb-bert-tiny-safetensors", backend="onnx", model_kwargs={"export": False} + ) + + # OpenVINO will forcibly override the export=False if the model repo/path doesn't contain an exported model + # But only starting from v1.19.0 + model = SentenceTransformer( + "sentence-transformers-testing/stsb-bert-tiny-safetensors", backend="openvino", model_kwargs={"export": False} + ) + assert isinstance(model[0].auto_model, OVModelForFeatureExtraction) + + +## Testing loading exported models: +@pytest.mark.parametrize( + ["backend", "model_id"], + [ + ("onnx", "sentence-transformers-testing/stsb-bert-tiny-onnx"), + ("openvino", "sentence-transformers-testing/stsb-bert-tiny-openvino"), + ], +) +@pytest.mark.parametrize( + ["model_kwargs", "exception"], + [ + [{}, False], + [{"file_name": "wrong_file_name", "export": True}, False], # Using a file_name is fine when exporting + [{"file_name": "wrong_file_name", "export": False}, True], # ... but fails when not exporting + ], +) +def test_backend_load(backend, model_id, model_kwargs, exception) -> None: + if exception: + with pytest.raises((OSError, RuntimeError)): + SentenceTransformer(model_id, backend=backend, model_kwargs=model_kwargs) + else: + model = SentenceTransformer(model_id, backend=backend, model_kwargs=model_kwargs) + assert model.get_backend() == backend + embedding = model.encode("Hello, World!") + assert embedding.shape == (model.get_sentence_embedding_dimension(),) + + +def test_onnx_provider_crash() -> None: + with pytest.raises(ValueError): + SentenceTransformer( + "sentence-transformers-testing/stsb-bert-tiny-onnx", + backend="onnx", + model_kwargs={"provider": "incorrect_provider"}, + ) + + +def test_openvino_provider() -> None: + model = SentenceTransformer( + "sentence-transformers-testing/stsb-bert-tiny-openvino", + backend="openvino", + model_kwargs={"ov_config": {"INFERENCE_PRECISION_HINT": "precision_1"}}, + ) + assert model[0].auto_model.ov_config == {"INFERENCE_PRECISION_HINT": "precision_1", "PERFORMANCE_HINT": "LATENCY"} + + with tempfile.TemporaryDirectory() as temp_dir: + ov_config_path = os.path.join(temp_dir, "ov_config.json") + with open(ov_config_path, "w") as ov_config_file: + json.dump({"INFERENCE_PRECISION_HINT": "precision_2"}, ov_config_file) + + model = SentenceTransformer( + "sentence-transformers-testing/stsb-bert-tiny-openvino", + backend="openvino", + model_kwargs={"ov_config": ov_config_path}, + ) + assert model[0].auto_model.ov_config == { + "INFERENCE_PRECISION_HINT": "precision_2", + "PERFORMANCE_HINT": "LATENCY", + } + + +def test_incorrect_backend() -> None: + with pytest.raises(ValueError): + SentenceTransformer("sentence-transformers-testing/stsb-bert-tiny-safetensors", backend="incorrect_backend") + + +def test_openvino_backend() -> None: + model_id = "sentence-transformers-testing/stsb-bert-tiny-safetensors" + # Test that OpenVINO output is close to PyTorch output + pytorch_model = SentenceTransformer(model_id) + openvino_model = SentenceTransformer( + model_id, + backend="openvino", + model_kwargs={"ov_config": {"INFERENCE_PRECISION_HINT": "f32"}}, + ) + pytorch_result = pytorch_model.encode(["Hello there!"]) + openvino_result = openvino_model.encode(["Hello there!"]) + assert np.allclose(openvino_result, pytorch_result, atol=0.000001), "OpenVINO and Pytorch outputs are not close" + + with tempfile.TemporaryDirectory() as tmpdirname: + # Test that loading with ov_config file works as expected + config_file = str(Path(tmpdirname) / "ov_config.json") + with open(Path(config_file), "w") as f: + f.write('{"NUM_STREAMS" : "2"}') + openvino_model_with_config = SentenceTransformer( + model_id, + backend="openvino", + model_kwargs={"ov_config": config_file}, + ) + # The transformers model is an Optimum model with an OpenVINO inference request property + assert openvino_model_with_config[0].auto_model.request.get_property("NUM_STREAMS") == 2 + + # Test that saving and loading local OpenVINO models works as expected + openvino_model_with_config.save_pretrained(tmpdirname) + local_openvino_model = SentenceTransformer( + tmpdirname, backend="openvino", model_kwargs={"ov_config": {"INFERENCE_PRECISION_HINT": "f32"}} + ) + local_openvino_result = local_openvino_model.encode(["Hello there!"]) + assert np.allclose( + local_openvino_result, openvino_result + ), "OpenVINO saved model output differs from in-memory converted model" + del local_openvino_model + gc.collect() diff --git a/tests/test_multi_process.py b/tests/test_multi_process.py index 5966e779b..5de94a7ab 100644 --- a/tests/test_multi_process.py +++ b/tests/test_multi_process.py @@ -10,6 +10,10 @@ from sentence_transformers import SentenceTransformer +@pytest.mark.skip( + "This test fails if optimum.intel.openvino is imported, because openvinotoolkit/nncf " + "patches torch._C._nn.gelu in a way that breaks pickling." +) @pytest.mark.parametrize("normalize_embeddings", (False, True)) @pytest.mark.parametrize("prompt_name", (None, "retrieval")) def test_encode_multi_process( diff --git a/tests/test_sentence_transformer.py b/tests/test_sentence_transformer.py index 3d96bead2..9d8e9c347 100644 --- a/tests/test_sentence_transformer.py +++ b/tests/test_sentence_transformer.py @@ -15,7 +15,7 @@ import numpy as np import pytest import torch -from huggingface_hub import GitRefInfo, GitRefs, HfApi, RepoUrl +from huggingface_hub import CommitInfo, HfApi, RepoUrl from torch import nn from sentence_transformers import SentenceTransformer, util @@ -108,30 +108,26 @@ def mock_create_repo(self, repo_id, **kwargs): def mock_upload_folder(self, **kwargs): nonlocal mock_upload_folder_kwargs mock_upload_folder_kwargs = kwargs - - def mock_list_repo_refs(self, repo_id=None, **kwargs): - try: - git_ref_info = GitRefInfo(name="main", ref="refs/heads/main", target_commit="123456") - git_ref_info2 = GitRefInfo(name="revision_test", ref="refs/heads/revision_test", target_commit="678901") - except TypeError: - git_ref_info = GitRefInfo(dict(name="main", ref="refs/heads/main", targetCommit="123456")) - git_ref_info2 = GitRefInfo( - dict(name="revision_test", ref="refs/heads/revision_test", target_commit="678901") + if kwargs.get("revision") is None: + return CommitInfo( + commit_url=f"https://huggingface.co/{kwargs.get('repo_id')}/commit/123456", + commit_message="commit_message", + commit_description="commit_description", + oid="oid", + ) + else: + return CommitInfo( + commit_url=f"https://huggingface.co/{kwargs.get('repo_id')}/commit/678901", + commit_message="commit_message", + commit_description="commit_description", + oid="oid", ) - # workaround for https://github.com/huggingface/huggingface_hub/issues/1956 - git_ref_kwargs = {"branches": [git_ref_info, git_ref_info2], "converts": [], "tags": [], "pull_requests": None} - try: - return GitRefs(**git_ref_kwargs) - except TypeError: - git_ref_kwargs.pop("pull_requests") - return GitRefs(**git_ref_kwargs) def mock_create_branch(self, repo_id, branch, revision=None, **kwargs): return None monkeypatch.setattr(HfApi, "create_repo", mock_create_repo) monkeypatch.setattr(HfApi, "upload_folder", mock_upload_folder) - monkeypatch.setattr(HfApi, "list_repo_refs", mock_list_repo_refs) monkeypatch.setattr(HfApi, "create_branch", mock_create_branch) model = SentenceTransformer("sentence-transformers-testing/stsb-bert-tiny-safetensors")