Spaces:
Sleeping
Sleeping
chore(cleanup): purge stale narrative/tombstones/dead code β codebase reads as the current standard
23b8fad | """Local embeddings via sentence-transformers. | |
| Used as the v1 production embedder because Voyage's free-tier rate limit | |
| (3 RPM) makes ingestion of 75+ PDFs impractical in our time budget. | |
| Default model: BAAI/bge-small-en-v1.5 β 384-dim, ~110MB, top-of-class on | |
| MTEB English benchmark for its size. Faster than 1024-dim cloud models on | |
| CPU and trivially small on GPU. | |
| The interface matches EmbeddingsProvider exactly, so the RAG pipeline | |
| doesn't change β only the import in ingest/retrieve. | |
| """ | |
| from __future__ import annotations | |
| from typing import Literal, Optional | |
| from backend.providers.base import EmbeddingsProvider | |
| class LocalEmbeddings(EmbeddingsProvider): | |
| name = "local-bge" | |
| def __init__( | |
| self, | |
| model_name: str = "BAAI/bge-small-en-v1.5", | |
| device: Optional[str] = None, | |
| ): | |
| # Lazy import so this module loads fast even if model isn't downloaded | |
| import os | |
| from sentence_transformers import SentenceTransformer | |
| # Device autodetect: MPS on Apple Silicon when available (2-3x faster | |
| # than CPU on long chunks), CUDA if present, else CPU. Honor explicit | |
| # override via constructor arg OR EMBED_DEVICE env var so HF Space | |
| # (no MPS) and local Mac (with MPS) pick the right path. | |
| if device is None: | |
| device = os.environ.get("EMBED_DEVICE", "").strip() or None | |
| if device is None: | |
| try: | |
| import torch | |
| if torch.backends.mps.is_available(): | |
| device = "mps" | |
| elif torch.cuda.is_available(): | |
| device = "cuda" | |
| else: | |
| device = "cpu" | |
| except Exception: | |
| device = "cpu" | |
| self.model_name = model_name | |
| self.device = device | |
| self.model = SentenceTransformer(model_name, device=device) | |
| self.dimension = self.model.get_sentence_embedding_dimension() | |
| # Warm-up call on MPS β first kernel JIT compile is ~3-5s; doing it | |
| # in __init__ rather than first encode() makes the first user request | |
| # fast. CPU/CUDA skip this (their first call has no JIT penalty). | |
| if device == "mps": | |
| try: | |
| self.model.encode(["warmup"] * 2, batch_size=2, show_progress_bar=False) | |
| except Exception: | |
| pass | |
| async def embed( | |
| self, | |
| texts: list[str], | |
| input_type: Literal["document", "query"] = "document", | |
| ) -> list[list[float]]: | |
| if not texts: | |
| return [] | |
| # BGE recommends a small query-side instruction; not strictly required | |
| if input_type == "query": | |
| texts = [f"Represent this sentence for searching relevant passages: {t}" for t in texts] | |
| # Batch size scales by device: MPS / CUDA throughput benefits from | |
| # bigger batches; CPU prefers smaller to avoid memory pressure on M1. | |
| # MPS/CUDA use batch 128 to minimise GPU kernel launches during | |
| # bulk re-ingest (800-token chunks at 128 β 100 MB per batch, well | |
| # within Mac M-series unified memory). CPU stays at 32 to keep peak | |
| # RSS bounded on machines without a GPU. | |
| batch = 128 if self.device in ("mps", "cuda") else 32 | |
| vectors = self.model.encode( | |
| texts, | |
| batch_size=batch, | |
| show_progress_bar=False, | |
| convert_to_numpy=True, | |
| normalize_embeddings=True, | |
| ) | |
| return vectors.tolist() | |