-
Notifications
You must be signed in to change notification settings - Fork 0
Add GPU support and update Docker configuration and documentation #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b44f5f1
722fb91
8dec3cb
2f8cd77
2be903e
14e8c09
6e34855
d7dbd64
1ac4daf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||
| # We use the lean NVIDIA CUDA runtime as the base so your GPU actually works | ||||||
| FROM nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04 | ||||||
|
|
||||||
| # Python environment variables | ||||||
| ENV PYTHONDONTWRITEBYTECODE=1 | ||||||
| ENV PYTHONUNBUFFERED=1 | ||||||
|
|
||||||
| # Magic trick: Copy the 'uv' binary directly from Astral's official image | ||||||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ | ||||||
|
||||||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ | |
| COPY --from=ghcr.io/astral-sh/uv:0.4.30 /uv /uvx /bin/ |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |||||
|  | ||||||
|  | ||||||
|  | ||||||
|  | ||||||
|
|
||||||
|
|
||||||
| A lightweight text embedding API designed as a drop-in replacement for the OpenAI embeddings endpoint. | ||||||
|
|
||||||
|
|
@@ -14,13 +16,15 @@ Built with FastAPI and `fastembed`, LocalEmbed is optimized for running local do | |||||
| * **Privacy First:** 100% local execution. No data ever leaves your network. | ||||||
| * **Zero-Latency Starts:** Automatically pre-loads your default model into memory on server boot. | ||||||
| * **Container-Native:** Multi-stage Docker build utilizing `uv` for a minimal, highly optimized runtime footprint. | ||||||
| * **CPU + GPU Ready:** Published Docker images for both CPU (`latest`) and NVIDIA GPU (`latest-gpu`) deployments. | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## Getting Started | ||||||
|
|
||||||
| ### Prerequisites | ||||||
| - **Docker** (Recommended) | ||||||
| - **For GPU deployment:** NVIDIA GPU + drivers + NVIDIA Container Toolkit | ||||||
| - Python 3.12+ (for local development) | ||||||
|
|
||||||
| ### Configuration | ||||||
|
|
@@ -33,17 +37,32 @@ LocalEmbed uses optional environment variables for configuration. Create a `.env | |||||
| ``` | ||||||
| 2. Open the `.env` file and set your desired configurations (like `DEFAULT_EMBEDDING_MODEL` or `HF_TOKEN`). | ||||||
|
|
||||||
| Environment variables: | ||||||
|
|
||||||
| - `DEFAULT_EMBEDDING_MODEL`: model to preload on startup | ||||||
| - `HF_TOKEN`: optional, useful to avoid model download rate limits | ||||||
| - `MODEL_CACHE_LIMIT`: max number of models kept in memory (LRU eviction) | ||||||
| - `EMBEDDING_THREADS`: CPU threads for embedding computation | ||||||
| - `BATCH_SIZE`: number of inputs processed per batch | ||||||
| - `USE_GPU`: set `true` to force CUDA provider in local/non-GPU-image runs | ||||||
|
||||||
| - `USE_GPU`: set `true` to force CUDA provider in local/non-GPU-image runs | |
| - `USE_GPU`: set `true` to request the CUDA provider in local/non-GPU-image runs. This requires the GPU extra to be installed (`uv sync --extra gpu` or `fastembed-gpu`) and a working CUDA-capable environment; otherwise model initialization may fail. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -41,7 +41,10 @@ async def lifespan(app: FastAPI): | |||||
|
|
||||||
| @app.get("/") | ||||||
| def read_root(): | ||||||
| return {"Project": "LocalEmbed", "description": "LocalEmbed"} | ||||||
| return { | ||||||
| "Project": "LocalEmbed", | ||||||
| "description": "A lightweight text embedding API designed as a drop-in replacement for the OpenAI embeddings endpoint. ", | ||||||
|
||||||
| "description": "A lightweight text embedding API designed as a drop-in replacement for the OpenAI embeddings endpoint. ", | |
| "description": "A lightweight text embedding API designed as a drop-in replacement for the OpenAI embeddings endpoint.", |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,21 +1,53 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from collections import OrderedDict | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from threading import RLock | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Iterable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import BaseModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from fastembed import TextEmbedding | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from loguru import logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.config import settings | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_cache: dict[str, TextEmbedding] = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_cache: OrderedDict[str, TextEmbedding] = OrderedDict() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_cache_lock = RLock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _evict_lru_models_if_needed() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cache_limit = max(1, settings.MODEL_CACHE_LIMIT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while len(model_cache) > cache_limit: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| evicted_model_id, _ = model_cache.popitem(last=False) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Evicting least recently used embedding model from memory: {evicted_model_id}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_model(model_id: str) -> TextEmbedding: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Fetch the model from cache, or load it if not present.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if model_id not in model_cache: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with model_cache_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cached_model = model_cache.get(model_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if cached_model is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_cache.move_to_end(model_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cached_model | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"Loading embedding model into memory: {model_id}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_cache[model_id] = TextEmbedding( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_id, threads=settings.EMBEDDING_THREADS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Configure providers based on GPU setting | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providers = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if settings.USE_GPU: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providers = ["CUDAExecutionProvider"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("GPU acceleration (CUDAExecutionProvider) enabled.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model = TextEmbedding( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_id, threads=settings.EMBEDDING_THREADS, providers=providers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+41
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providers = ["CUDAExecutionProvider"] | |
| logger.info("GPU acceleration (CUDAExecutionProvider) enabled.") | |
| model = TextEmbedding( | |
| model_id, threads=settings.EMBEDDING_THREADS, providers=providers | |
| ) | |
| providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] | |
| logger.info( | |
| "GPU acceleration preferred (CUDAExecutionProvider) with CPUExecutionProvider fallback enabled." | |
| ) | |
| try: | |
| model = TextEmbedding( | |
| model_id, threads=settings.EMBEDDING_THREADS, providers=providers | |
| ) | |
| except Exception as e: | |
| if settings.USE_GPU: | |
| logger.warning( | |
| f"Failed to initialize model {model_id} with GPU-enabled providers {providers}: {e}. Retrying with CPUExecutionProvider only." | |
| ) | |
| model = TextEmbedding( | |
| model_id, | |
| threads=settings.EMBEDDING_THREADS, | |
| providers=["CPUExecutionProvider"], | |
| ) | |
| else: | |
| raise |
Copilot
AI
Apr 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved_providers = model.model.model.get_providers() relies on internal/private attributes of fastembed’s TextEmbedding implementation (multiple nested .model). This is brittle across library versions and can break startup even if embedding works. Prefer a public API for provider reporting (if available), or guard this log line so provider introspection failures don’t prevent the model from loading.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| services: | ||
| localembed: | ||
| image: heshinth/localembed:latest-gpu | ||
| pull_policy: always | ||
| container_name: localembed-gpu | ||
| ports: | ||
| - "8000:8000" | ||
| environment: | ||
| - DEFAULT_EMBEDDING_MODEL=BAAI/bge-small-en-v1.5 | ||
| # - HF_TOKEN=your_token_here | ||
| # - MODEL_CACHE_LIMIT=2 | ||
| # - EMBEDDING_THREADS=8 | ||
| # - BATCH_SIZE=256 | ||
| gpus: all |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor formatting: the comment
#Number of threads...is missing a space after#, and there are trailing spaces on these comment lines. Cleaning this up improves readability in the sample env file.