diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 5c33d231a..4f7c64d8e 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -1,4 +1,5 @@ name: Tests +run-name: Tests (gpu) on: push: @@ -21,8 +22,6 @@ jobs: - '3.13.x' os: - ubuntu-latest - - macos-latest - - windows-latest runs-on: ${{ matrix.os }} diff --git a/experiments/Throughput_Across_Models_GPU.ipynb b/experiments/Throughput_Across_Models_GPU.ipynb new file mode 100644 index 000000000..8d5ca43c6 --- /dev/null +++ b/experiments/Throughput_Across_Models_GPU.ipynb @@ -0,0 +1,493 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "2Novt26HXCxC" + }, + "source": [ + "# 🤗 Huggingface vs ⚡ FastEmbed️\n", + "\n", + "Comparing the performance of Huggingface's 🤗 Transformers and ⚡ FastEmbed️ on a simple task (GPU)\n", + "## 📦 Imports\n", + "\n", + "Importing the necessary libraries for this comparison." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ZPBcPVLFapCs", + "outputId": "d8d78e12-8cf9-4115-d187-dfbc38e63bae" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: fastembed-gpu in /usr/local/lib/python3.11/dist-packages (0.6.0)\n", + "Requirement already satisfied: huggingface-hub<1.0,>=0.20 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (0.28.1)\n", + "Requirement already satisfied: loguru<0.8.0,>=0.7.2 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (0.7.3)\n", + "Requirement already satisfied: mmh3<6.0.0,>=4.1.0 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (5.1.0)\n", + "Requirement already satisfied: numpy>=1.21 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (1.26.4)\n", + "Requirement already satisfied: onnxruntime-gpu!=1.20.0,>=1.17.0 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (1.20.1)\n", + "Requirement already satisfied: pillow<12.0.0,>=10.3.0 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (11.1.0)\n", + "Requirement already satisfied: py-rust-stemmers<0.2.0,>=0.1.0 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (0.1.5)\n", + "Requirement already satisfied: requests<3.0,>=2.31 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (2.32.3)\n", + "Requirement already satisfied: tokenizers<1.0,>=0.15 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (0.21.0)\n", + "Requirement already satisfied: tqdm<5.0,>=4.66 in /usr/local/lib/python3.11/dist-packages (from fastembed-gpu) (4.67.1)\n", + "Requirement already satisfied: filelock in /usr/local/lib/python3.11/dist-packages (from huggingface-hub<1.0,>=0.20->fastembed-gpu) (3.17.0)\n", + "Requirement already satisfied: fsspec>=2023.5.0 in /usr/local/lib/python3.11/dist-packages (from huggingface-hub<1.0,>=0.20->fastembed-gpu) (2024.10.0)\n", + "Requirement already satisfied: packaging>=20.9 in /usr/local/lib/python3.11/dist-packages (from huggingface-hub<1.0,>=0.20->fastembed-gpu) (24.2)\n", + "Requirement already satisfied: pyyaml>=5.1 in /usr/local/lib/python3.11/dist-packages (from huggingface-hub<1.0,>=0.20->fastembed-gpu) (6.0.2)\n", + "Requirement already satisfied: typing-extensions>=3.7.4.3 in /usr/local/lib/python3.11/dist-packages (from huggingface-hub<1.0,>=0.20->fastembed-gpu) (4.12.2)\n", + "Requirement already satisfied: coloredlogs in /usr/local/lib/python3.11/dist-packages (from onnxruntime-gpu!=1.20.0,>=1.17.0->fastembed-gpu) (15.0.1)\n", + "Requirement already satisfied: flatbuffers in /usr/local/lib/python3.11/dist-packages (from onnxruntime-gpu!=1.20.0,>=1.17.0->fastembed-gpu) (25.2.10)\n", + "Requirement already satisfied: protobuf in /usr/local/lib/python3.11/dist-packages (from onnxruntime-gpu!=1.20.0,>=1.17.0->fastembed-gpu) (4.25.6)\n", + "Requirement already satisfied: sympy in /usr/local/lib/python3.11/dist-packages (from onnxruntime-gpu!=1.20.0,>=1.17.0->fastembed-gpu) (1.13.1)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.11/dist-packages (from requests<3.0,>=2.31->fastembed-gpu) (3.4.1)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.11/dist-packages (from requests<3.0,>=2.31->fastembed-gpu) (3.10)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.11/dist-packages (from requests<3.0,>=2.31->fastembed-gpu) (2.3.0)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.11/dist-packages (from requests<3.0,>=2.31->fastembed-gpu) (2025.1.31)\n", + "Requirement already satisfied: humanfriendly>=9.1 in /usr/local/lib/python3.11/dist-packages (from coloredlogs->onnxruntime-gpu!=1.20.0,>=1.17.0->fastembed-gpu) (10.0)\n", + "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.11/dist-packages (from sympy->onnxruntime-gpu!=1.20.0,>=1.17.0->fastembed-gpu) (1.3.0)\n" + ] + } + ], + "source": [ + "!pip install fastembed-gpu" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "iFgsdd7vabSW", + "outputId": "3a6abe8e-6111-4820-aa1e-8c2d6bb25381" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: torch in /usr/local/lib/python3.11/dist-packages (2.5.1+cu124)\n", + "Requirement already satisfied: transformers in /usr/local/lib/python3.11/dist-packages (4.48.3)\n", + "Requirement already satisfied: matplotlib in /usr/local/lib/python3.11/dist-packages (3.10.0)\n", + "Requirement already satisfied: filelock in /usr/local/lib/python3.11/dist-packages (from torch) (3.17.0)\n", + "Requirement already satisfied: typing-extensions>=4.8.0 in /usr/local/lib/python3.11/dist-packages (from torch) (4.12.2)\n", + "Requirement already satisfied: networkx in /usr/local/lib/python3.11/dist-packages (from torch) (3.4.2)\n", + "Requirement already satisfied: jinja2 in /usr/local/lib/python3.11/dist-packages (from torch) (3.1.5)\n", + "Requirement already satisfied: fsspec in /usr/local/lib/python3.11/dist-packages (from torch) (2024.10.0)\n", + "Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.4.127 in /usr/local/lib/python3.11/dist-packages (from torch) (12.4.127)\n", + "Requirement already satisfied: nvidia-cuda-runtime-cu12==12.4.127 in /usr/local/lib/python3.11/dist-packages (from torch) (12.4.127)\n", + "Requirement already satisfied: nvidia-cuda-cupti-cu12==12.4.127 in /usr/local/lib/python3.11/dist-packages (from torch) (12.4.127)\n", + "Requirement already satisfied: nvidia-cudnn-cu12==9.1.0.70 in /usr/local/lib/python3.11/dist-packages (from torch) (9.1.0.70)\n", + "Requirement already satisfied: nvidia-cublas-cu12==12.4.5.8 in /usr/local/lib/python3.11/dist-packages (from torch) (12.4.5.8)\n", + "Requirement already satisfied: nvidia-cufft-cu12==11.2.1.3 in /usr/local/lib/python3.11/dist-packages (from torch) (11.2.1.3)\n", + "Requirement already satisfied: nvidia-curand-cu12==10.3.5.147 in /usr/local/lib/python3.11/dist-packages (from torch) (10.3.5.147)\n", + "Requirement already satisfied: nvidia-cusolver-cu12==11.6.1.9 in /usr/local/lib/python3.11/dist-packages (from torch) (11.6.1.9)\n", + "Requirement already satisfied: nvidia-cusparse-cu12==12.3.1.170 in /usr/local/lib/python3.11/dist-packages (from torch) (12.3.1.170)\n", + "Requirement already satisfied: nvidia-nccl-cu12==2.21.5 in /usr/local/lib/python3.11/dist-packages (from torch) (2.21.5)\n", + "Requirement already satisfied: nvidia-nvtx-cu12==12.4.127 in /usr/local/lib/python3.11/dist-packages (from torch) (12.4.127)\n", + "Requirement already satisfied: nvidia-nvjitlink-cu12==12.4.127 in /usr/local/lib/python3.11/dist-packages (from torch) (12.4.127)\n", + "Requirement already satisfied: triton==3.1.0 in /usr/local/lib/python3.11/dist-packages (from torch) (3.1.0)\n", + "Requirement already satisfied: sympy==1.13.1 in /usr/local/lib/python3.11/dist-packages (from torch) (1.13.1)\n", + "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.11/dist-packages (from sympy==1.13.1->torch) (1.3.0)\n", + "Requirement already satisfied: huggingface-hub<1.0,>=0.24.0 in /usr/local/lib/python3.11/dist-packages (from transformers) (0.28.1)\n", + "Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.11/dist-packages (from transformers) (1.26.4)\n", + "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.11/dist-packages (from transformers) (24.2)\n", + "Requirement already satisfied: pyyaml>=5.1 in /usr/local/lib/python3.11/dist-packages (from transformers) (6.0.2)\n", + "Requirement already satisfied: regex!=2019.12.17 in /usr/local/lib/python3.11/dist-packages (from transformers) (2024.11.6)\n", + "Requirement already satisfied: requests in /usr/local/lib/python3.11/dist-packages (from transformers) (2.32.3)\n", + "Requirement already satisfied: tokenizers<0.22,>=0.21 in /usr/local/lib/python3.11/dist-packages (from transformers) (0.21.0)\n", + "Requirement already satisfied: safetensors>=0.4.1 in /usr/local/lib/python3.11/dist-packages (from transformers) (0.5.3)\n", + "Requirement already satisfied: tqdm>=4.27 in /usr/local/lib/python3.11/dist-packages (from transformers) (4.67.1)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib) (1.3.1)\n", + "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.11/dist-packages (from matplotlib) (0.12.1)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.11/dist-packages (from matplotlib) (4.56.0)\n", + "Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib) (1.4.8)\n", + "Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.11/dist-packages (from matplotlib) (11.1.0)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib) (3.2.1)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.11/dist-packages (from matplotlib) (2.8.2)\n", + "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.11/dist-packages (from python-dateutil>=2.7->matplotlib) (1.17.0)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.11/dist-packages (from jinja2->torch) (3.0.2)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.11/dist-packages (from requests->transformers) (3.4.1)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.11/dist-packages (from requests->transformers) (3.10)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.11/dist-packages (from requests->transformers) (2.3.0)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.11/dist-packages (from requests->transformers) (2025.1.31)\n" + ] + } + ], + "source": [ + "!pip3 install torch transformers matplotlib" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-30T00:33:35.753669Z", + "start_time": "2024-03-30T00:33:34.371658Z" + }, + "id": "veEu-ceoXCxF" + }, + "outputs": [], + "source": [ + "import time\n", + "from typing import Callable\n", + "\n", + "import torch\n", + "import torch.nn.functional as F\n", + "from fastembed import TextEmbedding\n", + "import matplotlib.pyplot as plt\n", + "from transformers import AutoModel, AutoTokenizer" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9IfJREvLXCxG" + }, + "source": [ + "## 📖 Data\n", + "\n", + "data is a list of strings, each string is a document." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-30T00:33:35.766679Z", + "start_time": "2024-03-30T00:33:35.755112Z" + }, + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "BHqha4PNXCxG", + "outputId": "e08b7609-b3ac-4512-aac5-3a14022b2abb" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "12" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "documents: list[str] = [\n", + " \"Chandrayaan-3 is India's third lunar mission\",\n", + " \"It aimed to land a rover on the Moon's surface - joining the US, China and Russia\",\n", + " \"The mission is a follow-up to Chandrayaan-2, which had partial success\",\n", + " \"Chandrayaan-3 will be launched by the Indian Space Research Organisation (ISRO)\",\n", + " \"The estimated cost of the mission is around $35 million\",\n", + " \"It will carry instruments to study the lunar surface and atmosphere\",\n", + " \"Chandrayaan-3 landed on the Moon's surface on 23rd August 2023\",\n", + " \"It consists of a lander named Vikram and a rover named Pragyan similar to Chandrayaan-2. Its propulsion module would act like an orbiter.\",\n", + " \"The propulsion module carries the lander and rover configuration until the spacecraft is in a 100-kilometre (62 mi) lunar orbit\",\n", + " \"The mission used GSLV Mk III rocket for its launch\",\n", + " \"Chandrayaan-3 was launched from the Satish Dhawan Space Centre in Sriharikota\",\n", + " \"Chandrayaan-3 was launched earlier in the year 2023\",\n", + "]\n", + "len(documents)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-30T00:33:35.766791Z", + "start_time": "2024-03-30T00:33:35.756803Z" + }, + "id": "7xdiTTcuXCxH" + }, + "outputs": [], + "source": [ + "model_id = \"BAAI/bge-small-en\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s2Ei2QrmXCxH" + }, + "source": [ + "## Setting up 🤗 Huggingface\n", + "\n", + "We'll be using the [Huggingface Transformers](https://huggingface.co/transformers/) with PyTorch library to generate embeddings. We'll be using the same model across both libraries for a fair(er?) comparison." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-30T00:34:03.988Z", + "start_time": "2024-03-30T00:33:37.460865Z" + }, + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "QiE-lfI5XCxH", + "outputId": "edc71144-62f1-4349-a203-208b8e5fc386" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([12, 384])" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class HF:\n", + " \"\"\"\n", + " HuggingFace Transformer implementation of FlagEmbedding\n", + " Based on https://huggingface.co/BAAI/bge-base-en\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self, model_id: str, device: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", + " ):\n", + " self.device = device\n", + " self.model = AutoModel.from_pretrained(model_id).to(self.device)\n", + " self.tokenizer = AutoTokenizer.from_pretrained(model_id)\n", + "\n", + " def embed(self, texts: list[str]):\n", + " encoded_input = self.tokenizer(\n", + " texts, max_length=512, padding=True, truncation=True, return_tensors=\"pt\"\n", + " ).to(self.device)\n", + "\n", + " model_output = self.model(**encoded_input)\n", + " sentence_embeddings = model_output[0][:, 0]\n", + " sentence_embeddings = F.normalize(sentence_embeddings)\n", + " return sentence_embeddings\n", + "\n", + "\n", + "hf = HF(model_id=model_id)\n", + "hf.embed(documents).shape" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ySKCZyJbXCxJ" + }, + "source": [ + "## Setting up ⚡️FastEmbed\n", + "\n", + "Sorry, don't have a lot to set up here. We'll be using the default model, which is Flag Embedding, same as the Huggingface model." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-30T00:34:04.076422Z", + "start_time": "2024-03-30T00:34:03.987162Z" + }, + "id": "HQU9j4_AXCxJ" + }, + "outputs": [], + "source": [ + "embedding_model = TextEmbedding(model_name=model_id, cuda=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ziD5ifdCXCxK" + }, + "source": [ + "## 📊 Comparison\n", + "\n", + "We'll be comparing the following metrics: Minimum, Maximum, Mean, across k runs. Let's write a function to do that:\n", + "\n", + "### 🚀 Calculating Stats" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-30T00:34:06.543782Z", + "start_time": "2024-03-30T00:34:06.357816Z" + }, + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "E5BPzLwrXCxK", + "outputId": "d9c9bae5-cb54-46e1-8cfe-f2bd24beb64d" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Huggingface Transformers (Average, Max, Min): (0.014735164642333985, 0.03357124328613281, 0.011344671249389648)\n", + "FastEmbed (Average, Max, Min): (0.008367671966552734, 0.020212650299072266, 0.006894826889038086)\n" + ] + } + ], + "source": [ + "import types\n", + "\n", + "\n", + "def calculate_time_stats(\n", + " embed_func: Callable, documents: list[str], k: int\n", + ") -> tuple[float, float, float]:\n", + " times = []\n", + " for _ in range(k):\n", + " # Timing the embed_func call\n", + " start_time = time.time()\n", + " embeddings = embed_func(documents)\n", + " # Force computation if embed_func returns a generator\n", + " if isinstance(embeddings, types.GeneratorType):\n", + " list(embeddings)\n", + "\n", + " end_time = time.time()\n", + " times.append(end_time - start_time)\n", + "\n", + " # Returning mean, max, and min time for the call\n", + " return (sum(times) / k, max(times), min(times))\n", + "\n", + "\n", + "hf_stats = calculate_time_stats(hf.embed, documents, k=100)\n", + "print(f\"Huggingface Transformers (Average, Max, Min): {hf_stats}\")\n", + "fst_stats = calculate_time_stats(embedding_model.embed, documents, k=100)\n", + "print(f\"FastEmbed (Average, Max, Min): {fst_stats}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dpudV0MIXCxK" + }, + "source": [ + "## 📈 Results\n", + "\n", + "Let's run the comparison and see the results." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-30T00:34:11.032206Z", + "start_time": "2024-03-30T00:34:10.828410Z" + }, + "colab": { + "base_uri": "https://localhost:8080/", + "height": 452 + }, + "id": "c7Bi9_3XXCxL", + "outputId": "eabef8b4-ae7a-466c-c625-6bdb52885872" + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlUAAAGzCAYAAAAG8+KwAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAZLZJREFUeJzt3XdYFFfbBvB76XUXkSaCoKgIil0RWywI9ppYE1FREwV7T2JLNESNiSXGkqLGaGI3VixgxCg27AXsXZoICAgIe74/fJnPdUFZHYLo/bsurmTPeebMM8Pu8jhz9qxCCCFARERERG9Er7gTICIiInoXsKgiIiIikgGLKiIiIiIZsKgiIiIikgGLKiIiIiIZsKgiIiIikgGLKiIiIiIZsKgiIiIikgGLKiIiIiIZsKiiYqdQKBAcHFzcaRABePZ8nDZtWnGnQQBcXV3Rr1+/4k6jROjXrx9cXV2LO433HosqKjLXrl3Dp59+igoVKsDExARKpRKNGjXC/Pnz8eTJk+JO743dv38f06ZNw+nTp4s7FQ3Tpk2DQqGQfszMzODp6Ykvv/wSqampxZ0eyejmzZvo378/3NzcYGJiAgcHBzRt2hRTp04t7tT+c5mZmfjhhx/g7e0NlUoFExMTVK5cGcHBwbh8+XJxp0fvCYPiToDeTTt27MBHH30EY2Nj9O3bF9WqVUN2djb+/fdfjBs3DhcuXMCyZcuKO803cv/+fUyfPh2urq6oWbNmcaejZfHixbCwsEBaWhr27NmDmTNnIjw8HIcOHYJCoSju9OgNXb16FfXq1YOpqSkGDBgAV1dXPHjwACdPnsSsWbMwffr04k7xP5OYmIjWrVsjKioK7du3R+/evWFhYYGYmBj89ddfWLZsGbKzs4s7zSL1888/Q61WF3ca7z0WVSS7GzduoGfPnnBxcUF4eDjKlCkj9QUFBeHq1avYsWPHf5pTeno6zM3N/9N9vi65cv3www9hY2MDAPjss8/QrVs3bNq0CUeOHIGPj0++22RkZMDMzOyN903yeNlz4YcffkBaWhpOnz4NFxcXjb74+Pj/Ir23Rr9+/XDq1Cls2LAB3bp10+j7+uuv8cUXXxRTZkUv7zliaGhY3KkQePuPisDs2bORlpaGX3/9VaOgylOxYkWMGDFCq33Lli2oVq0ajI2NUbVqVYSGhmr037p1C0OHDoW7uztMTU1RunRpfPTRR7h586ZG3IoVK6BQKHDgwAEMHToUdnZ2cHJy0mkMAEhOTsaoUaPg6uoKY2NjODk5oW/fvkhMTMQ///yDevXqAQD69+8v3WpbsWKFtP3Ro0fRunVrqFQqmJmZ4YMPPsChQ4c09pF3q+7ixYvo3bs3SpUqhcaNGwMAYmNj0b9/fzg5OcHY2BhlypRBp06d8s21MFq0aAHgWdELAM2aNUO1atUQFRWFpk2bwszMDJ9//jmAZ3+UAwMDYW9vDxMTE9SoUQMrV67UGlOtVmP+/Pnw8vKCiYkJbG1t0bp1a5w4cUIj7o8//kCdOnVgamoKa2tr9OzZE3fu3NGIuXLlCrp16wYHBweYmJjAyckJPXv2REpKihSzd+9eNG7cGFZWVrCwsIC7u7uUc56srCxMnToVFStWhLGxMZydnTF+/HhkZWVpxY0aNQq2trawtLREx44dcffu3UKdy3/++QcKhQJr167F559/DgcHB5ibm6Njx45axwW8+XMhP9euXYOTk5NWQQUAdnZ2Wm27du1CkyZNYG5uDktLS7Rr1w4XLlzQiouOjkb37t1ha2sLU1NTuLu7axUlp06dQps2baBUKmFhYYGWLVviyJEjGjF5r8NDhw5h9OjRsLW1hbm5Obp06YKEhASNWCEEZsyYAScnJ5iZmaF58+b55pafo0ePYseOHQgMDNQqqADA2NgY3333nUZbeHi4dC6srKzQqVMnXLp0SSMm7/dx+fJlfPzxx1CpVLC1tcXkyZMhhMCdO3fQqVMnKJVKODg4YO7cuRrb6/IcOXjwID766COUK1dOes6OGjVKa5pEv379YGFhgWvXrqFt27awtLREnz59pL4X51T99ddfqFOnDiwtLaFUKuHl5YX58+drxFy/fh0fffQRrK2tYWZmhgYNGmj9ozfvWNatW4eZM2fCyckJJiYmaNmyJa5evVrAb+b9xCtVJLtt27ahQoUKaNiwYaG3+ffff7Fp0yYMHToUlpaWWLBgAbp164bbt2+jdOnSAIDjx4/j8OHD6NmzJ5ycnHDz5k0sXrwYzZo1w8WLF7WusAwdOhS2traYMmUK0tPTdRojLS0NTZo0waVLlzBgwADUrl0biYmJ2Lp1K+7evQsPDw989dVXmDJlCgYPHowmTZoAgHTM4eHhaNOmDerUqYOpU6dCT08Py5cvR4sWLXDw4EHUr19fI9ePPvoIlSpVwjfffAMhBACgW7duuHDhAoYNGwZXV1fEx8dj7969uH379mtNSL127RoASOcTAB4+fIg2bdqgZ8+e+Pjjj2Fvb48nT56gWbNmuHr1KoKDg1G+fHmsX78e/fr1Q3JyskZBHBgYiBUrVqBNmzYYOHAgcnJycPDgQRw5cgR169YFAMycOROTJ09G9+7dMXDgQCQkJGDhwoVo2rQpTp06BSsrK2RnZ8Pf3x9ZWVkYNmwYHBwccO/ePWzfvh3JyclQqVS4cOEC2rdvj+rVq+Orr76CsbExrl69qlGcqNVqdOzYEf/++y8GDx4MDw8PnDt3Dj/88AMuX76MLVu2SLEDBw7EH3/8gd69e6Nhw4YIDw9Hu3btdDqnM2fOhEKhwIQJExAfH4958+bB19cXp0+fhqmpKQB5ngv5cXFxwb59+xAeHi4VzAVZtWoVAgIC4O/vj1mzZiEjIwOLFy9G48aNcerUKen5dPbsWTRp0gSGhoYYPHgwXF1dce3aNWzbtg0zZ84EAFy4cAFNmjSBUqnE+PHjYWhoiKVLl6JZs2Y4cOAAvL29NfY9bNgwlCpVClOnTsXNmzcxb948BAcHY+3atVLMlClTMGPGDLRt2xZt27bFyZMn4efnV6hbdlu3bgUAfPLJJ6+MBYB9+/ahTZs2qFChAqZNm4YnT55g4cKFaNSoEU6ePKn12urRowc8PDzw7bffYseOHZgxYwasra2xdOlStGjRArNmzcLq1asxduxY1KtXD02bNtXYvjDPkfXr1yMjIwNDhgxB6dKlcezYMSxcuBB3797F+vXrNcbLycmBv78/GjdujO+++67AK8t79+5Fr1690LJlS8yaNQsAcOnSJRw6dEh6DcfFxaFhw4bIyMjA8OHDUbp0aaxcuRIdO3bEhg0b0KVLF40xv/32W+jp6WHs2LFISUnB7Nmz0adPHxw9erRQ5/69IIhklJKSIgCITp06FXobAMLIyEhcvXpVajtz5owAIBYuXCi1ZWRkaG0bGRkpAIjff/9dalu+fLkAIBo3bixycnI04gs7xpQpUwQAsWnTJq14tVothBDi+PHjAoBYvny5Vn+lSpWEv7+/FJu37/Lly4tWrVpJbVOnThUARK9evTTGePTokQAg5syZo7X/V8kbMyYmRiQkJIgbN26IpUuXCmNjY2Fvby/S09OFEEJ88MEHAoBYsmSJxvbz5s0TAMQff/whtWVnZwsfHx9hYWEhUlNThRBChIeHCwBi+PDhBZ6jmzdvCn19fTFz5kyN/nPnzgkDAwOp/dSpUwKAWL9+fYHH9cMPPwgAIiEhocCYVatWCT09PXHw4EGN9iVLlggA4tChQ0IIIU6fPi0AiKFDh2rE9e7dWwAQU6dOLXAfQgixf/9+AUCULVtWOh9CCLFu3ToBQMyfP186D2/6XCjI+fPnhampqQAgatasKUaMGCG2bNki/X7zPH78WFhZWYlBgwZptMfGxgqVSqXR3rRpU2FpaSlu3bqlEft87p07dxZGRkbi2rVrUtv9+/eFpaWlaNq0qdSW9zr09fXV2H7UqFFCX19fJCcnCyGEiI+PF0ZGRqJdu3YacZ9//rkAIAICAl56Hrp06SIAiEePHr00Lk/NmjWFnZ2dePjwodR25swZoaenJ/r27Su15f0+Bg8eLLXl5OQIJycnoVAoxLfffiu1P3r0SJiammrkWtjniBD5vy+FhIQIhUKh8bsICAgQAMTEiRO14gMCAoSLi4v0eMSIEUKpVGq9Bz5v5MiRAoDG6+Xx48eifPnywtXVVeTm5moci4eHh8jKypJi58+fLwCIc+fOFbiP9w1v/5Gs8j5dZmlpqdN2vr6+cHNzkx5Xr14dSqUS169fl9ry/lUHAE+fPsXDhw9RsWJFWFlZ4eTJk1pjDho0CPr6+hpthR1j48aNqFGjhta/1AC8cpL36dOnceXKFfTu3RsPHz5EYmIiEhMTkZ6ejpYtWyIiIkJrQulnn32mlaeRkRH++ecfPHr06KX7K4i7uztsbW1Rvnx5fPrpp6hYsSJ27Nih8S9bY2Nj9O/fX2O7nTt3wsHBAb169ZLaDA0NMXz4cKSlpeHAgQMAnp0jhUKR7yfN8s7Rpk2boFar0b17d+k8JCYmwsHBAZUqVcL+/fsBACqVCgCwe/duZGRk5Hs8VlZWAIC///67wAm569evh4eHB6pUqaKxv7wrOXn727lzJwBg+PDhGtuPHDky33EL0rdvX43n+ocffogyZcpI48vxXChI1apVcfr0aXz88ce4efMm5s+fj86dO8Pe3h4///yzFLd3714kJyejV69eGudEX18f3t7e0jlJSEhAREQEBgwYgHLlymnsK+/3mZubiz179qBz586oUKGC1F+mTBn07t0b//77r9YnTAcPHqzxmmnSpAlyc3Nx69YtAM+uHGVnZ2PYsGEacYX9XejynvPgwQOcPn0a/fr1g7W1tdRevXp1tGrVSvq9PW/gwIHS/+vr66Nu3boQQiAwMFBqt7Kygru7u8b7VZ5XPUcAzfel9PR0JCYmomHDhhBC4NSpU1pjDhky5JXHamVlhfT0dOzdu7fAmJ07d6J+/foat5ktLCwwePBg3Lx5ExcvXtSI79+/P4yMjKTHeVfo8zvu9xVv/5GslEolAODx48c6bffimzgAlCpVSqOgePLkCUJCQrB8+XLcu3dP49bI8/Nu8pQvX16rrbBjXLt2Ld/5GYVx5coVAEBAQECBMSkpKShVqlSBuRobG2PWrFkYM2YM7O3t0aBBA7Rv3x59+/aFg4NDofLYuHEjlEolDA0N4eTkpFG05ilbtqzGmyTwbN5ZpUqVoKen+W8uDw8PqR94do4cHR01/ji96MqVKxBCoFKlSvn2502uLV++PEaPHo3vv/8eq1evRpMmTdCxY0dpLgvw7DbML7/8goEDB2LixIlo2bIlunbtig8//FDK9cqVK7h06RJsbW3z3V/eBO5bt25BT09P65y4u7sXeCz5efG4FAoFKlasKM17k+O58DKVK1fGqlWrkJubi4sXL2L79u2YPXs2Bg8ejPLly8PX11fKoaBbhHmv2bw/jNWqVStwfwkJCcjIyMj3PHl4eECtVuPOnTuoWrWq1P7iazvvWPNe23nPpxfPpa2trcZ5Kcjz7zl5hXdB8vZVUP67d+/W+nDAi/nnLdeQ9yGQ59sfPnyoNe6rniMAcPv2bUyZMgVbt27V+kfUi+9tBgYG0hzRlxk6dCjWrVuHNm3aoGzZsvDz80P37t3RunVrKebWrVtat2sBzdf688+HV/0uiUUVyUypVMLR0RHnz5/XabsXryjleb7oGTZsGJYvX46RI0fCx8cHKpUKCoUCPXv2zPfKxfP/+nvdMV5H3jhz5swpcKkFCwuLV+Y6cuRIdOjQAVu2bMHu3bsxefJkhISEIDw8HLVq1XplHk2bNtV6439RfvuVk1qthkKhwK5du/L9HT9/HubOnYt+/frh77//xp49ezB8+HCEhITgyJEjcHJygqmpKSIiIrB//37s2LEDoaGhWLt2LVq0aIE9e/ZAX18farUaXl5e+P777/PNx9nZuciONT9yPRdeRV9fH15eXvDy8oKPjw+aN2+O1atXw9fXV8ph1apV+RbkBgZF+2egMK/tN1GlShUAwLlz56QrJ3LKL385jyk3NxetWrVCUlISJkyYgCpVqsDc3Bz37t1Dv379tN6XjI2Ntf7Bkx87OzucPn0au3fvxq5du7Br1y4sX74cffv2zfdDJ4VR1L/LdwGLKpJd+/btsWzZMkRGRhb40f3XsWHDBgQEBGh8yiYzMxPJycmyj+Hm5vbKwrCg24B5Vz+USiV8fX0LnVtBY40ZMwZjxozBlStXULNmTcydOxd//PHHG437Mi4uLjh79izUarXGm3d0dLTUn5fb7t27kZSUVODVKjc3NwghUL58eVSuXPmV+84rDL788kscPnwYjRo1wpIlSzBjxgwAgJ6eHlq2bImWLVvi+++/xzfffIMvvvgC+/fvl24hnzlzBi1btnzpbVoXFxeo1Wpcu3ZN46pFTEzMq0/Qc/KuAuURQuDq1auoXr26dPyAPM+Fwsr7gMCDBw80crCzs3tpDnm38172vLe1tYWZmVm+5yk6Ohp6eno6F655z6crV65o3FJMSEgo1BWQDh06ICQkBH/88ccri6q8fRWUv42NjexLr7zqOXLu3DlcvnwZK1euRN++faW4l922KywjIyN06NABHTp0gFqtxtChQ7F06VJMnjwZFStWhIuLS4HnAkC+nyyll+OcKpLd+PHjYW5ujoEDByIuLk6r/9q1a1of6y0MfX19rX8RLVy4ELm5ubKP0a1bN5w5cwabN2/WGiNv+7w33xcLsjp16sDNzQ3fffcd0tLStLZ/8ePk+cnIyEBmZqZGm5ubGywtLbWWBpBb27ZtERsbq/HprJycHCxcuBAWFhb44IMPADw7R0KIfBeZzDtHXbt2hb6+PqZPn6513oUQ0u2S1NRU5OTkaPR7eXlBT09POt6kpCSt/eRd/cmL6d69O+7du6cxpyjPkydPpE+BtmnTBgCwYMECjZh58+blc0YK9vvvv2vc6t6wYQMePHggjS/Hc6EgBw8exNOnT7Xa8+bq5BWL/v7+UCqV+Oabb/KNz8vB1tYWTZs2xW+//Ybbt29rxOT97vT19eHn54e///5b4/ZVXFwc1qxZg8aNG0u34wrL19cXhoaGWLhwocZzpLC/Cx8fH7Ru3Rq//PKLxqc782RnZ2Ps2LEAns39qlmzJlauXKnxuj1//jz27NmDtm3b6pR7YbzqOZJ39ef5YxdCvNZ75PNevBWpp6cnFXJ5r5e2bdvi2LFjiIyMlOLS09OxbNkyuLq6wtPT841yeB/xShXJzs3NDWvWrJE+ivz8iuqHDx+WPp6vq/bt22PVqlVQqVTw9PREZGQk9u3bp7FEgFxjjBs3Dhs2bMBHH32EAQMGoE6dOkhKSsLWrVuxZMkS1KhRA25ubrCyssKSJUtgaWkJc3NzeHt7o3z58vjll1/Qpk0bVK1aFf3790fZsmVx79497N+/H0qlEtu2bXtpnpcvX0bLli3RvXt3eHp6wsDAAJs3b0ZcXBx69uyp87nTxeDBg7F06VL069cPUVFRcHV1xYYNG3Do0CHMmzdPmnTbvHlzfPLJJ1iwYAGuXLmC1q1bQ61W4+DBg2jevDmCg4Ph5uaGGTNmYNKkSbh58yY6d+4MS0tL3LhxA5s3b8bgwYMxduxYhIeHIzg4GB999BEqV66MnJwcrFq1Cvr6+tLctq+++goRERFo164dXFxcEB8fj59++glOTk7SRNtPPvkE69atw2effYb9+/ejUaNGyM3NRXR0NNatW4fdu3ejbt26qFmzJnr16oWffvoJKSkpaNiwIcLCwnRec8fa2hqNGzdG//79ERcXh3nz5qFixYoYNGgQgGd/yN70uVCQWbNmISoqCl27dpX+WJ48eRK///47rK2tpYneSqUSixcvxieffILatWujZ8+esLW1xe3bt7Fjxw40atQIP/74I4BnRWbjxo1Ru3ZtaV7WzZs3sWPHDunrmGbMmCGtFzZ06FAYGBhg6dKlyMrKwuzZs3U+DltbW4wdOxYhISFo37492rZti1OnTmHXrl2vvH2d5/fff4efnx+6du2KDh06oGXLljA3N8eVK1fw119/4cGDB9JaVXPmzEGbNm3g4+ODwMBAaUkFlUpVJN/5+KrnSJUqVeDm5oaxY8fi3r17UCqV2Lhx4xvPUxo4cCCSkpLQokULODk54datW1i4cCFq1qwpzZmaOHEi/vzzT7Rp0wbDhw+HtbU1Vq5ciRs3bmDjxo2Fus1IL/gPP2lI75nLly+LQYMGCVdXV2FkZCQsLS1Fo0aNxMKFC0VmZqYUB0AEBQVpbe/i4qLxEeVHjx6J/v37CxsbG2FhYSH8/f1FdHS0VlzeR7mPHz+uNWZhxxBCiIcPH4rg4GBRtmxZYWRkJJycnERAQIBITEyUYv7++2/h6ekpDAwMtJZXOHXqlOjatasoXbq0MDY2Fi4uLqJ79+4iLCxMisn72PaLywQkJiaKoKAgUaVKFWFubi5UKpXw9vYW69ate9VpL3DMF33wwQeiatWq+fbFxcVJ58nIyEh4eXlpLR0hxLOPmM+ZM0dUqVJFGBkZCVtbW9GmTRsRFRWlEbdx40bRuHFjYW5uLszNzUWVKlVEUFCQiImJEUIIcf36dTFgwADh5uYmTExMhLW1tWjevLnYt2+fNEZYWJjo1KmTcHR0FEZGRsLR0VH06tVLXL58WWNf2dnZYtasWaJq1arC2NhYlCpVStSpU0dMnz5dpKSkSHFPnjwRw4cPF6VLlxbm5uaiQ4cO4s6dOzotqfDnn3+KSZMmCTs7O2FqairatWuntRyBEG/2XCjIoUOHRFBQkKhWrZpQqVTC0NBQlCtXTvTr109juYPnc/b39xcqlUqYmJgINzc30a9fP3HixAmNuPPnz4suXboIKysrYWJiItzd3cXkyZM1Yk6ePCn8/f2FhYWFMDMzE82bNxeHDx/WiCnodZh37vbv3y+15ebmiunTp4syZcoIU1NT0axZM3H+/Pl8X5cFycjIEN99952oV6+esLCwEEZGRqJSpUpi2LBhGsu1CCHEvn37RKNGjYSpqalQKpWiQ4cO4uLFixoxBf0+AgIChLm5udb+X3w96fIcuXjxovD19RUWFhbCxsZGDBo0SFpW5vnXXUH7zut7fkmFDRs2CD8/P2FnZyeMjIxEuXLlxKeffioePHigsd21a9fEhx9+KP2+69evL7Zv364Rk3csLy55cuPGjXyXlXmfKYTgDDMiIl38888/aN68OdavX48PP/ywuNOhtxCfI+8nXtsjIiIikgGLKiIiIiIZsKgiIiIikgHnVBERERHJgFeqiIiIiGTAooqIiIhIBlz88z+kVqtx//59WFpavvQrNIiIiOjtIYTA48eP4ejo+NJFUVlU/Yfu37//n3+hKxEREcnjzp07cHJyKrCfRdV/KO/rPe7cuaPz92MREb3vMp5kYfm6gzh36S7Ox9xF6uMn+HpcN3Tyr60Ve/1WPGYv3olT52/B0FAfTb3dMfaztrC2+v8vTP5pZRiWrAovcH8r5w1GrWrPvlT4y9kbsHXPKa0YV2cbbF0+Snp8L/YR2nz8Xb7jzfqiB9o0r67RplarsX77cWzYcQw37yTCxNgQld3KYPyQtnB3K/PS85GV/RSrNhzG9n2ncD8uGUoLE9So6oIhfVugoqv9S7cl3aSmpsLZ2Vn6O14QFlX/obxbfkqlkkUVEZGOUtIeYumq/SjrUAqelZ1wJOoKTExNtN5PH8Q9woAxv8LSwgTjgzogIyMLy1aH49qtBPy9YiyMDJ/96evUuj7cK2pfdZjz0zakP8mCTz0PKdbQ0BBGRgaY9UVvjVhLC839p6Q9+9Lqjn510LxRVY3YejXdtHIdM30V/g49ga5t66N/j+bIyMzGhZg7yMwWr/w78dmEX7Av4hx6dm6Iau7OiEtMwaoNB9F3xDKErpkEpzLWL92edPeqqTssqoiIqESws1Hi2M6ZsLNR4uzF2+jYb06+cYtW7EHGkyxs+30cyjo8KyxqVHXBx8GLsGH7UfTu0ggA4FGpLDwqldXY9n7cIzyIT0bPTj5SQZXHQF8PXdrUK1Su1ao4vzJ2+96T2LjjGJbMGojWzWsUatw8sfHJCN1/BoM/bonPh3eW2uvVdEPvoQsRuv80BvZuodOY9Ob46T8iIioRjI0MYWfz6qv8ofvPoGXjalJBBQCN61dBhXJ22LHv5Eu33bo7CkIIdGpdN9/+3Fw1Hqc9KVS+GU+ykP00p8D+X/7cjxpVXdC6eQ2o1WpkPMkq1LgAkJaRCQCwsda8HZV3fkyMjQo9FsmHV6qIiOidERufjMSkx/DyKKfVV6OqC/YfuvDS7beEHoejfSl416qo1fck8ymqNR+HJ5nZUCnN0NGvDiYGd4K5mbFW7PxfduGbBVugUCjgVcUZY4e0R9MGHlL/47QnOHPhFj75sDFm/7QVK9dFID0jC86OpTEhqCPat9KeJ/Y8FydblLGzws+rw1HBxQ5VKzshLjEFIQv/hrNjaXTwe/n2VDRYVBER0TsjPjEVAPK9omVXWonk1AxkZT+FsZGhVv/law8QffU+Pv3EV2vujJ2NCp9+0hLV3J2hFgIHIi9i1YaDuHTlHv5aPBwGBvoAAD09BZp4V4F/sxpwsFPh9r2H+HVNOPqNXIxfvhuMFo2rAQBu3UuEEALb9pyEvr4eJgZ3gtLCFL+t/QfDvlwBCwsTNPPxLPA4DQ30sXhWIEZMXomBY5ZJ7V5VnLHxl9FQWZrpfvLojbGoIiKid0ZmVjYAaM2HAgBjY8P/xeRfVG3ZfRwA0DmfW38TgjpqPO7oVwcVytlhzuLt2Bl+Gh396gAAyjpYY9XCII3Yrm3qwbfHTMyYv1kqqjIynt3qe5SSjs2/jUGtaq4AAN+mXmjSeRp+/G33S4sqAFBZmsGzshPatqyFWtVccetuAn5asRdBk37Fqh+DYWKsfYxUtDinioiI3hl5c4nym8uUlfX0fzHaxYYQAn/vjoK7WxmtyesFCezVHHp6Chw6FvPSOCuVOT7q0ADXb8XjQdwjjTydHUtLBRUAmJsZo2WTajhz4RZycnILHDM17Qk+GjwPtb1cMSGoI/w+qI5BfVpi8axAHD9zHeu3HSnUMZC8WFQREdE7I++2X95twOfFP0yFldIs36tUJ85cx70HSQVOUM+PiYkRSqnMkZya/spYR3srAEByagYAwN5WBUB7ojkAlC5lgac5ucjIzC5wvNDw00hMegzfJl4a7Q1qV4KluQmizl4v7GGQjFhUERHRO8PBzgqlS1ng3KXbWn1nLtyCZ+X8V8PeEnoCCoUCnfwLX1SlpWciKTkd1qVeviAkANy+9xDAs4IJeFZU2ZZWIi4hRSs2PiEFxsaGsMhnAnyehKTHAIBctVqjXQiBXLUaObnq/DajIsaiioiI3imtm9dE2L/ncf9/t9oA4NCxGFy/HY+2LWtpxT/NycXOsFOoV6OCxjIMeTKzniItPVOrfeGvoRBC4IPnPtX38NFjrbjY+GSs23YEVSo6ws5GJbW3b1Ub9+Me4eDRaKktKTkNeyPOoWHdStJ3zD3NycXVm7GIT/z/AqxCOTsAwLY9mktE7I04h4wn2ahaQPFIRYsT1YmIqMRYue4AUh8/Qdz/Coywg+cRG5cMAAjo8QGUFqYI6u+HnWGn0GvIAvTv0QzpT7Kw7I8wVKnoiI86eGuNGRF5CY9S0gu89ZfwMBXtPpmFjn514Pa/r3+JOHIJ+w9dxAc+HvD74P9vwYUs/Bu37yaiYb3KsLdV4e79JKzZfAhPnmRj6pgPNcYdGtAKO/adxJCJvyKwV3NYWphgzaZDeJqTi3FDOkhxsfHJ8O0+E93a1cfcqZ8AAFo2qYbKFcpgwa+huBeb9L+J6olYuT4CdjZK9Ojk8/onmV4biyoiIioxlq0Ox70HSdLj0P1nELr/DACgc5t6UFqYwtG+FNYuGYGv523CrEVbYWiojxaNquKLEV0K/NSfoYE+2uVzFQsAlJamaNm4Gv49FoONO44hV62Gq5Mtxg3tgMEft5SuKAFAE+8qWH3vEFZtOIiU1AwoLc1Qv5Ybhg1ojWpVnDXGtS2txIafR2Hm/M347c/9eJqTi9pe5fHDV30LvE2Zx8jQAOuXjcSCX0Ox/9AFbN0TBQszE/h94IXxQzrA2sqi0OeU5KMQQojiTuJ9kZqaCpVKhZSUFH73HxERUQlR2L/fnFNFREREJAMWVUREREQy4JwqIqIS5Lpr++JOgeitVeHm9mLdP69UEREREcmARRURERGRDFhUEREREcmARRURERGRDFhUEREREcmARRURERGRDFhUEREREcmARRURERGRDFhUEREREcmARRURERGRDFhUEREREcmARRURERGRDFhUEREREcmARRURERGRDFhUEREREcmARRURERGRDFhUEREREcmARRURERGRDFhUEREREcmARRURERGRDIq1qIqIiECHDh3g6OgIhUKBLVu2aPQLITBlyhSUKVMGpqam8PX1xZUrVzRikpKS0KdPHyiVSlhZWSEwMBBpaWkaMWfPnkWTJk1gYmICZ2dnzJ49WyuX9evXo0qVKjAxMYGXlxd27typcy5ERET0/irWoio9PR01atTAokWL8u2fPXs2FixYgCVLluDo0aMwNzeHv78/MjMzpZg+ffrgwoUL2Lt3L7Zv346IiAgMHjxY6k9NTYWfnx9cXFwQFRWFOXPmYNq0aVi2bJkUc/jwYfTq1QuBgYE4deoUOnfujM6dO+P8+fM65UJERETvL4UQQhR3EgCgUCiwefNmdO7cGcCzK0OOjo4YM2YMxo4dCwBISUmBvb09VqxYgZ49e+LSpUvw9PTE8ePHUbduXQBAaGgo2rZti7t378LR0RGLFy/GF198gdjYWBgZGQEAJk6ciC1btiA6OhoA0KNHD6Snp2P79u1SPg0aNEDNmjWxZMmSQuVSGKmpqVCpVEhJSYFSqZTlvBHR++W6a/viToHorVXh5vZXB72Gwv79fmvnVN24cQOxsbHw9fWV2lQqFby9vREZGQkAiIyMhJWVlVRQAYCvry/09PRw9OhRKaZp06ZSQQUA/v7+iImJwaNHj6SY5/eTF5O3n8Lkkp+srCykpqZq/BAREdG76a0tqmJjYwEA9vb2Gu329vZSX2xsLOzs7DT6DQwMYG1trRGT3xjP76OgmOf7X5VLfkJCQqBSqaQfZ2fnVxw1ERERlVRvbVH1Lpg0aRJSUlKknzt37hR3SkRERFRE3tqiysHBAQAQFxen0R4XFyf1OTg4ID4+XqM/JycHSUlJGjH5jfH8PgqKeb7/Vbnkx9jYGEqlUuOHiIiI3k1vbVFVvnx5ODg4ICwsTGpLTU3F0aNH4ePjAwDw8fFBcnIyoqKipJjw8HCo1Wp4e3tLMREREXj69KkUs3fvXri7u6NUqVJSzPP7yYvJ209hciEiIqL3W7EWVWlpaTh9+jROnz4N4NmE8NOnT+P27dtQKBQYOXIkZsyYga1bt+LcuXPo27cvHB0dpU8Ienh4oHXr1hg0aBCOHTuGQ4cOITg4GD179oSjoyMAoHfv3jAyMkJgYCAuXLiAtWvXYv78+Rg9erSUx4gRIxAaGoq5c+ciOjoa06ZNw4kTJxAcHAwAhcqFiIiI3m8GxbnzEydOoHnz5tLjvEInICAAK1aswPjx45Geno7BgwcjOTkZjRs3RmhoKExMTKRtVq9ejeDgYLRs2RJ6enro1q0bFixYIPWrVCrs2bMHQUFBqFOnDmxsbDBlyhSNtawaNmyINWvW4Msvv8Tnn3+OSpUqYcuWLahWrZoUU5hciIiI6P311qxT9T7gOlVE9Ka4ThVRwbhOFREREdE7gEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJ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0aJFGDduHBo1aoQTJ06ga9eu+PXXX4sq1xKvsOtcvA6uU0X0clyniujdV+LWqVq2bBnUajUASF8Pc/jwYXTs2BGffvrp62dMREREVILpXFTp6elBT+//57f37NkTPXv2lDUpIiIiopLmtdapSk5OxrFjxxAfHy9dtcrTt29fWRIjIiIiKkl0Lqq2bduGPn36IC0tDUqlEgqFQupTKBQsqoiIiOi9pPM6VWPGjMGAAQOQlpaG5ORkPHr0SPpJSkoqihyJiIiI3no6F1X37t3D8OHDYWZmVhT5EBEREZVIOhdV/v7+OHHiRFHkQkRERFRiFWpO1datW6X/b9euHcaNG4eLFy/Cy8sLhoaGGrEdO3aUN0MiIiKiEqBQRVXnzp212p5fkTyPQqFAbm7uGydFREREVNIUqqh6cdkEIiIiItKk85wqIiIiItJW6KIqPDwcnp6eSE1N1epLSUlB1apVERERIWtyRERERCVFoYuqefPmYdCgQfl+kaBKpcKnn36KH374QdbkiIiIiEqKQhdVZ86cQevWrQvs9/PzQ1RUlCxJEREREZU0hS6q4uLitJZPeJ6BgQESEhJkSYqIiIiopCl0UVW2bFmcP3++wP6zZ8+iTJkysiRFREREVNIUuqhq27YtJk+ejMzMTK2+J0+eYOrUqWjfvr2syRERERGVFIVapwoAvvzyS2zatAmVK1dGcHAw3N3dAQDR0dFYtGgRcnNz8cUXXxRZokRERERvs0IXVfb29jh8+DCGDBmCSZMmQQgB4Nkq6v7+/li0aBHs7e2LLFEiIiKit1mhiyoAcHFxwc6dO/Ho0SNcvXoVQghUqlQJpUqVKqr8iIiIiEoEnYqqPKVKlUK9evXkzoWIiIioxOLX1BARERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxYVBERERHJgEUVERERkQxY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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def plot_character_per_second_comparison(\n", + " hf_stats: tuple[float, ...], fst_stats: tuple[float, ...], documents: list[str]\n", + "):\n", + " # Calculating total characters in documents\n", + " total_characters = sum(len(doc) for doc in documents)\n", + "\n", + " # Calculating characters per second for each model\n", + " hf_chars_per_sec = total_characters / hf_stats[0] # Mean time is at index 0\n", + " fst_chars_per_sec = total_characters / fst_stats[0]\n", + "\n", + " # Plotting the bar chart\n", + " models = [\"HF Embed (Torch)\", \"FastEmbed\"]\n", + " chars_per_sec = [hf_chars_per_sec, fst_chars_per_sec]\n", + "\n", + " bars = plt.bar(models, chars_per_sec, color=[\"#1f356c\", \"#dd1f4b\"])\n", + " plt.ylabel(\"Characters per Second\")\n", + " plt.title(\"Characters Processed per Second Comparison\")\n", + "\n", + " # Adding the number at the top of each bar\n", + " for bar, chars in zip(bars, chars_per_sec):\n", + " plt.text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height(),\n", + " f\"{chars:.1f}\",\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " color=\"#1f356c\",\n", + " fontsize=12,\n", + " )\n", + "\n", + " plt.show()\n", + "\n", + "\n", + "plot_character_per_second_comparison(hf_stats, fst_stats, documents)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "id": "5Y_ilYACXCxL" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/fastembed/common/onnx_model.py b/fastembed/common/onnx_model.py index 52b08b430..020f16a40 100644 --- a/fastembed/common/onnx_model.py +++ b/fastembed/common/onnx_model.py @@ -68,7 +68,15 @@ def _load_onnx_model( if device_id is None: onnx_providers = ["CUDAExecutionProvider"] else: - onnx_providers = [("CUDAExecutionProvider", {"device_id": device_id})] + # kSameAsRequested: Allocates only the requested memory, avoiding over-allocation. + # more precise than 'kNextPowerOfTwo', which grows memory aggressively. + # source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage: + onnx_providers = [ + ( + "CUDAExecutionProvider", + {"device_id": device_id, "arena_extend_strategy": "kSameAsRequested"}, + ) + ] else: onnx_providers = ["CPUExecutionProvider"] @@ -132,5 +140,7 @@ def __init__( def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "EmbeddingWorker[T]": return cls(model_name=model_name, cache_dir=cache_dir, **kwargs) - def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]: + def process( + self, items: Iterable[tuple[int, Any]], **kwargs: Any + ) -> Iterable[tuple[int, Any]]: raise NotImplementedError("Subclasses must implement this method") diff --git a/fastembed/common/utils.py b/fastembed/common/utils.py index 02ff615bc..1563bfe4c 100644 --- a/fastembed/common/utils.py +++ b/fastembed/common/utils.py @@ -5,12 +5,12 @@ import unicodedata from pathlib import Path from itertools import islice -from typing import Iterable, Optional, TypeVar +from typing import Iterable, Optional, TypeVar, Sequence import numpy as np from numpy.typing import NDArray -from fastembed.common.types import NumpyArray +from fastembed.common.types import NumpyArray, OnnxProvider T = TypeVar("T") @@ -67,3 +67,18 @@ def get_all_punctuation() -> set[str]: def remove_non_alphanumeric(text: str) -> str: return re.sub(r"[^\w\s]", " ", text, flags=re.UNICODE) + + +def is_cuda_enabled(cuda: bool, providers: Optional[Sequence[OnnxProvider]]) -> bool: + """ + Check if CUDA is enabled based on the `cuda` and `providers` parameters + """ + if cuda: + return True + if not providers: + return False + if isinstance(providers, str): + return "CUDAExecutionProvider" in providers + return isinstance(providers, (list, tuple)) and any( + isinstance(p, str) and "CUDAExecutionProvider" in p for p in providers + ) diff --git a/fastembed/image/onnx_image_model.py b/fastembed/image/onnx_image_model.py index a178e6c54..7508933cf 100644 --- a/fastembed/image/onnx_image_model.py +++ b/fastembed/image/onnx_image_model.py @@ -6,13 +6,14 @@ import numpy as np from PIL import Image +import onnxruntime as ort from fastembed.image.transform.operators import Compose from fastembed.common.types import NumpyArray from fastembed.common import ImageInput, OnnxProvider from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T from fastembed.common.preprocessor_utils import load_preprocessor -from fastembed.common.utils import iter_batch +from fastembed.common.utils import iter_batch, is_cuda_enabled from fastembed.parallel_processor import ParallelWorkerPool # Holds type of the embedding result @@ -74,7 +75,21 @@ def onnx_embed(self, images: list[ImageInput], **kwargs: Any) -> OnnxOutputConte encoded = np.array(self.processor(image_files)) onnx_input = self._build_onnx_input(encoded) onnx_input = self._preprocess_onnx_input(onnx_input) - model_output = self.model.run(None, onnx_input) # type: ignore[union-attr] + + run_options = ort.RunOptions() + providers = kwargs.get("providers", None) + cuda = kwargs.get("cuda", False) + if is_cuda_enabled(cuda, providers): + device_id = kwargs.get("device_id", None) + device_id = str(device_id if isinstance(device_id, int) else 0) + # enables memory arena shrinkage, freeing unused memory after each Run() cycle. + # helps prevent excessive memory retention, especially for dynamic workloads. + # source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage: + run_options.add_run_config_entry( + "memory.enable_memory_arena_shrinkage", f"gpu:{device_id}" + ) + + model_output = self.model.run(None, onnx_input, run_options) # type: ignore[union-attr] embeddings = model_output[0].reshape(len(images), -1) return OnnxOutputContext(model_output=embeddings) @@ -104,7 +119,9 @@ def _embed_images( self.load_onnx_model() for batch in iter_batch(images, batch_size): - yield from self._post_process_onnx_output(self.onnx_embed(batch)) + yield from self._post_process_onnx_output( + self.onnx_embed(batch, cuda=cuda, providers=providers) + ) else: if parallel == 0: parallel = os.cpu_count() @@ -129,7 +146,9 @@ def _embed_images( class ImageEmbeddingWorker(EmbeddingWorker[T]): - def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]: + def process( + self, items: Iterable[tuple[int, Any]], **kwargs: Any + ) -> Iterable[tuple[int, Any]]: for idx, batch in items: - embeddings = self.model.onnx_embed(batch) + embeddings = self.model.onnx_embed(batch, **kwargs) yield idx, embeddings diff --git a/fastembed/late_interaction_multimodal/onnx_multimodal_model.py b/fastembed/late_interaction_multimodal/onnx_multimodal_model.py index 089ba1b75..57cdc7dbf 100644 --- a/fastembed/late_interaction_multimodal/onnx_multimodal_model.py +++ b/fastembed/late_interaction_multimodal/onnx_multimodal_model.py @@ -6,13 +6,14 @@ import numpy as np from PIL import Image +import onnxruntime as ort from tokenizers import Encoding, Tokenizer from fastembed.common import OnnxProvider, ImageInput from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T from fastembed.common.preprocessor_utils import load_tokenizer, load_preprocessor from fastembed.common.types import NumpyArray -from fastembed.common.utils import iter_batch +from fastembed.common.utils import iter_batch, is_cuda_enabled from fastembed.image.transform.operators import Compose from fastembed.parallel_processor import ParallelWorkerPool @@ -103,7 +104,21 @@ def onnx_embed_text( ) onnx_input = self._preprocess_onnx_text_input(onnx_input, **kwargs) - model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr] + + run_options = ort.RunOptions() + providers = kwargs.get("providers", None) + cuda = kwargs.get("cuda", False) + if is_cuda_enabled(cuda, providers): + device_id = kwargs.get("device_id", None) + device_id = str(device_id if isinstance(device_id, int) else 0) + # enables memory arena shrinkage, freeing unused memory after each Run() cycle. + # helps prevent excessive memory retention, especially for dynamic workloads. + # source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage: + run_options.add_run_config_entry( + "memory.enable_memory_arena_shrinkage", f"gpu:{device_id}" + ) + + model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input, run_options) # type: ignore[union-attr] return OnnxOutputContext( model_output=model_output[0], attention_mask=onnx_input.get("attention_mask", attention_mask), @@ -136,7 +151,9 @@ def _embed_documents( if not hasattr(self, "model") or self.model is None: self.load_onnx_model() for batch in iter_batch(documents, batch_size): - yield from self._post_process_onnx_text_output(self.onnx_embed_text(batch)) + yield from self._post_process_onnx_text_output( + self.onnx_embed_text(batch, cuda=cuda, providers=providers) + ) else: if parallel == 0: parallel = os.cpu_count() @@ -169,7 +186,21 @@ def onnx_embed_image(self, images: list[ImageInput], **kwargs: Any) -> OnnxOutpu encoded = np.array(self.processor(image_files)) onnx_input = {"pixel_values": encoded} onnx_input = self._preprocess_onnx_image_input(onnx_input, **kwargs) - model_output = self.model.run(None, onnx_input) # type: ignore[union-attr] + + run_options = ort.RunOptions() + providers = kwargs.get("providers", None) + cuda = kwargs.get("cuda", False) + if is_cuda_enabled(cuda, providers): + device_id = kwargs.get("device_id", None) + device_id = str(device_id if isinstance(device_id, int) else 0) + # enables memory arena shrinkage, freeing unused memory after each Run() cycle. + # helps prevent excessive memory retention, especially for dynamic workloads. + # source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage: + run_options.add_run_config_entry( + "memory.enable_memory_arena_shrinkage", f"gpu:{device_id}" + ) + + model_output = self.model.run(None, onnx_input, run_options) # type: ignore[union-attr] embeddings = model_output[0].reshape(len(images), -1) return OnnxOutputContext(model_output=embeddings) @@ -199,7 +230,9 @@ def _embed_images( self.load_onnx_model() for batch in iter_batch(images, batch_size): - yield from self._post_process_onnx_image_output(self.onnx_embed_image(batch)) + yield from self._post_process_onnx_image_output( + self.onnx_embed_image(batch, cuda=cuda, providers=providers) + ) else: if parallel == 0: parallel = os.cpu_count() @@ -241,9 +274,11 @@ def init_embedding( ) -> OnnxMultimodalModel: raise NotImplementedError() - def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]: + def process( + self, items: Iterable[tuple[int, Any]], **kwargs: Any + ) -> Iterable[tuple[int, Any]]: for idx, batch in items: - onnx_output = self.model.onnx_embed_text(batch) + onnx_output = self.model.onnx_embed_text(batch, **kwargs) yield idx, onnx_output @@ -265,7 +300,9 @@ def init_embedding( ) -> OnnxMultimodalModel: raise NotImplementedError() - def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]: + def process( + self, items: Iterable[tuple[int, Any]], **kwargs: Any + ) -> Iterable[tuple[int, Any]]: for idx, batch in items: - embeddings = self.model.onnx_embed_image(batch) + embeddings = self.model.onnx_embed_image(batch, **kwargs) yield idx, embeddings diff --git a/fastembed/parallel_processor.py b/fastembed/parallel_processor.py index 9a20a8e9e..fc10c7264 100644 --- a/fastembed/parallel_processor.py +++ b/fastembed/parallel_processor.py @@ -28,7 +28,9 @@ class Worker: def start(cls, *args: Any, **kwargs: Any) -> "Worker": raise NotImplementedError() - def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]: + def process( + self, items: Iterable[tuple[int, Any]], **kwargs: Any + ) -> Iterable[tuple[int, Any]]: raise NotImplementedError() @@ -63,7 +65,7 @@ def input_queue_iterable() -> Iterable[Any]: break yield item - for processed_item in worker.process(input_queue_iterable()): + for processed_item in worker.process(input_queue_iterable(), **kwargs): output_queue.put(processed_item) except Exception as e: # pylint: disable=broad-except logging.exception(e) diff --git a/fastembed/rerank/cross_encoder/onnx_text_model.py b/fastembed/rerank/cross_encoder/onnx_text_model.py index bc6198566..336fb7beb 100644 --- a/fastembed/rerank/cross_encoder/onnx_text_model.py +++ b/fastembed/rerank/cross_encoder/onnx_text_model.py @@ -4,6 +4,7 @@ from typing import Any, Iterable, Optional, Sequence, Type import numpy as np +import onnxruntime as ort from tokenizers import Encoding from fastembed.common.onnx_model import ( @@ -14,7 +15,7 @@ ) from fastembed.common.types import NumpyArray from fastembed.common.preprocessor_utils import load_tokenizer -from fastembed.common.utils import iter_batch +from fastembed.common.utils import iter_batch, is_cuda_enabled from fastembed.parallel_processor import ParallelWorkerPool @@ -71,7 +72,21 @@ def onnx_embed_pairs(self, pairs: list[tuple[str, str]], **kwargs: Any) -> OnnxO tokenized_input = self.tokenize(pairs, **kwargs) inputs = self._build_onnx_input(tokenized_input) onnx_input = self._preprocess_onnx_input(inputs, **kwargs) - outputs = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr] + + run_options = ort.RunOptions() + providers = kwargs.get("providers", None) + cuda = kwargs.get("cuda", False) + if is_cuda_enabled(cuda, providers): + device_id = kwargs.get("device_id", None) + device_id = str(device_id if isinstance(device_id, int) else 0) + # Enables memory arena shrinkage, freeing unused memory after each Run() cycle. + # Helps prevent excessive memory retention, especially for dynamic workloads. + # Source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage: + run_options.add_run_config_entry( + "memory.enable_memory_arena_shrinkage", f"gpu:{device_id}" + ) + + outputs = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input, run_options) # type: ignore[union-attr] relevant_output = outputs[0] scores: NumpyArray = relevant_output[:, 0] return OnnxOutputContext(model_output=scores) @@ -110,7 +125,9 @@ def _rerank_pairs( if not hasattr(self, "model") or self.model is None: self.load_onnx_model() for batch in iter_batch(pairs, batch_size): - yield from self._post_process_onnx_output(self.onnx_embed_pairs(batch, **kwargs)) + yield from self._post_process_onnx_output( + self.onnx_embed_pairs(batch, cuda=cuda, providers=providers, **kwargs) + ) else: if parallel == 0: parallel = os.cpu_count() @@ -163,7 +180,9 @@ def init_embedding( ) -> OnnxCrossEncoderModel: raise NotImplementedError() - def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]: + def process( + self, items: Iterable[tuple[int, Any]], **kwargs: Any + ) -> Iterable[tuple[int, Any]]: for idx, batch in items: - onnx_output = self.model.onnx_embed_pairs(batch) + onnx_output = self.model.onnx_embed_pairs(batch, **kwargs) yield idx, onnx_output diff --git a/fastembed/sparse/bm25.py b/fastembed/sparse/bm25.py index bd2b43eec..8714126c4 100644 --- a/fastembed/sparse/bm25.py +++ b/fastembed/sparse/bm25.py @@ -344,7 +344,7 @@ def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "Bm25Worker": return cls(model_name=model_name, cache_dir=cache_dir, **kwargs) def process( - self, items: Iterable[tuple[int, Any]] + self, items: Iterable[tuple[int, Any]], **kwargs: Any ) -> Iterable[tuple[int, list[SparseEmbedding]]]: for idx, batch in items: onnx_output = self.model.raw_embed(batch) diff --git a/fastembed/text/onnx_text_model.py b/fastembed/text/onnx_text_model.py index 625ab6b33..120b59c9c 100644 --- a/fastembed/text/onnx_text_model.py +++ b/fastembed/text/onnx_text_model.py @@ -4,13 +4,14 @@ from typing import Any, Iterable, Optional, Sequence, Type, Union import numpy as np +import onnxruntime as ort from numpy.typing import NDArray from tokenizers import Encoding, Tokenizer from fastembed.common.types import NumpyArray, OnnxProvider from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T from fastembed.common.preprocessor_utils import load_tokenizer -from fastembed.common.utils import iter_batch +from fastembed.common.utils import iter_batch, is_cuda_enabled from fastembed.parallel_processor import ParallelWorkerPool @@ -82,7 +83,21 @@ def onnx_embed( ) onnx_input = self._preprocess_onnx_input(onnx_input, **kwargs) - model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr] + run_options = ort.RunOptions() + providers = kwargs.get("providers", None) + cuda = kwargs.get("cuda", False) + + if is_cuda_enabled(cuda, providers): + device_id = kwargs.get("device_id", None) + device_id = str(device_id if isinstance(device_id, int) else 0) + # enables memory arena shrinkage, freeing unused memory after each Run() cycle. + # helps prevent excessive memory retention, especially for dynamic workloads. + # source: https://onnxruntime.ai/docs/get-started/with-c.html#features:~:text=Memory%20arena%20shrinkage: + run_options.add_run_config_entry( + "memory.enable_memory_arena_shrinkage", f"gpu:{device_id}" + ) + + model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input, run_options) # type: ignore[union-attr] return OnnxOutputContext( model_output=model_output[0], attention_mask=onnx_input.get("attention_mask", attention_mask), @@ -115,7 +130,9 @@ def _embed_documents( if not hasattr(self, "model") or self.model is None: self.load_onnx_model() for batch in iter_batch(documents, batch_size): - yield from self._post_process_onnx_output(self.onnx_embed(batch)) + yield from self._post_process_onnx_output( + self.onnx_embed(batch, cuda=cuda, providers=providers) + ) else: if parallel == 0: parallel = os.cpu_count() @@ -140,7 +157,9 @@ def _embed_documents( class TextEmbeddingWorker(EmbeddingWorker[T]): - def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, OnnxOutputContext]]: + def process( + self, items: Iterable[tuple[int, Any]], **kwargs: Any + ) -> Iterable[tuple[int, OnnxOutputContext]]: for idx, batch in items: - onnx_output = self.model.onnx_embed(batch) + onnx_output = self.model.onnx_embed(batch, **kwargs) yield idx, onnx_output diff --git a/pyproject.toml b/pyproject.toml index 4effb948e..fac3cafff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "fastembed" +name = "fastembed-gpu" version = "0.6.0" description = "Fast, light, accurate library built for retrieval embedding generation" authors = ["Qdrant Team ", "NirantK "] @@ -18,7 +18,7 @@ numpy = [ { version = ">=2.1.0", python = ">=3.13" }, { version = ">=1.21,<2.1.0", python = "<3.10" }, ] -onnxruntime = [ +onnxruntime-gpu = [ { version = ">1.20.0", python = ">=3.13" }, { version = ">=1.17.0,<1.20.0", python = "<3.10" }, { version = ">=1.17.0,!=1.20.0", python = ">=3.10,<3.13" },