Embeddings / app.py
cloudunity's picture
Update app.py
e58161b verified
Raw
History Blame Contribute Delete
6.31 kB
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
import numpy as np
from sentence_transformers import SentenceTransformer
import asyncio
import logging
from typing import List, Optional, Union
from pydantic import BaseModel
import time
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Embedding API")
MODEL_NAME = "all-MiniLM-L12-v2"
# Load model on startup
try:
model = SentenceTransformer(MODEL_NAME)
logger.info("Model loaded successfully")
except Exception as e:
logger.error(f"Failed to load model: {e}")
model = None
# Request batching queue
class EmbeddingRequest(BaseModel):
input: Union[str, List[str]]
model: str = MODEL_NAME
encoding_format: Optional[str] = None
class BatchItem:
def __init__(self, texts: List[str]):
self.texts = texts
self.future: asyncio.Future = asyncio.Future()
pending_batch: List[BatchItem] = []
batch_lock = asyncio.Lock()
batch_event = asyncio.Event()
BATCH_TIMEOUT = 0.05 # 50ms window to collect requests
MAX_BATCH_SIZE = 32
async def batch_processor():
"""Continuously process batches of requests"""
global pending_batch
while True:
try:
# Wait for requests or timeout
try:
await asyncio.wait_for(batch_event.wait(), timeout=BATCH_TIMEOUT)
batch_event.clear()
except asyncio.TimeoutError:
pass
async with batch_lock:
if not pending_batch:
continue
# Take up to MAX_BATCH_SIZE items
batch_to_process = pending_batch[:MAX_BATCH_SIZE]
pending_batch = pending_batch[MAX_BATCH_SIZE:]
if not batch_to_process:
continue
# Flatten all texts
all_texts = []
text_counts = []
for item in batch_to_process:
all_texts.extend(item.texts)
text_counts.append(len(item.texts))
logger.info(f"Processing batch of {len(batch_to_process)} requests, {len(all_texts)} texts total")
# Compute embeddings
start = time.time()
embeddings = model.encode(all_texts, convert_to_numpy=True)
elapsed = time.time() - start
logger.info(f"Embedding computed in {elapsed:.2f}s")
# Split embeddings back to individual requests
idx = 0
for item, count in zip(batch_to_process, text_counts):
item_embeddings = embeddings[idx:idx+count].tolist()
item.future.set_result(item_embeddings)
idx += count
except Exception as e:
logger.error(f"Error in batch processor: {e}")
async with batch_lock:
for item in pending_batch:
if not item.future.done():
item.future.set_exception(e)
pending_batch.clear()
await asyncio.sleep(1)
@app.on_event("startup")
async def startup():
"""Start batch processor on app startup"""
if model is None:
raise RuntimeError("Model failed to load")
asyncio.create_task(batch_processor())
logger.info("Batch processor started")
@app.get("/v1/models")
async def list_models():
"""OpenAI-compatible models endpoint"""
if model is None:
raise HTTPException(status_code=503, detail="Model not ready")
return {
"object": "list",
"data": [
{
"id": MODEL_NAME,
"object": "model",
"created": 0,
"owned_by": "huggingface",
"permission": [],
"root": MODEL_NAME,
"parent": None
}
]
}
@app.post("/v1/embeddings")
async def create_embeddings(request: EmbeddingRequest):
if model is None:
raise HTTPException(status_code=503, detail="Model not ready")
# Normalize input to list
if isinstance(request.input, str):
texts = [request.input]
else:
texts = request.input
if not texts:
raise HTTPException(status_code=400, detail="input cannot be empty")
if len(texts) > 512:
raise HTTPException(status_code=400, detail="Cannot embed more than 512 texts at once")
# Create batch item
batch_item = BatchItem(texts)
async with batch_lock:
pending_batch.append(batch_item)
# Signal processor if we hit batch size
if len(pending_batch) >= MAX_BATCH_SIZE:
batch_event.set()
# Signal processor that there's work
batch_event.set()
# Wait for result with timeout
try:
embeddings_list = await asyncio.wait_for(batch_item.future, timeout=30.0)
except asyncio.TimeoutError:
raise HTTPException(status_code=504, detail="Embedding request timed out")
# Format response as OpenAI-compatible
data = []
for idx, embedding in enumerate(embeddings_list):
data.append({
"object": "embedding",
"embedding": embedding,
"index": idx
})
return {
"object": "list",
"data": data,
"model": MODEL_NAME,
"usage": {
"prompt_tokens": sum(len(text.split()) for text in texts),
"total_tokens": sum(len(text.split()) for text in texts)
}
}
@app.get("/health")
async def health():
"""Health check endpoint"""
if model is None:
return JSONResponse({"status": "unhealthy", "reason": "model not loaded"}, status_code=503)
return {"status": "healthy"}
@app.get("/")
async def root():
"""API info"""
return {
"name": "OpenAI-Compatible Embedding API",
"model": MODEL_NAME,
"endpoints": {
"GET /v1/models": "List available models",
"POST /v1/embeddings": "Create embeddings (OpenAI-compatible)",
"GET /health": "Health check"
}
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)