cloudunity commited on
Commit
4c220a5
·
verified ·
1 Parent(s): 27c130a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +177 -0
app.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.responses import JSONResponse
3
+ import numpy as np
4
+ from sentence_transformers import SentenceTransformer
5
+ import asyncio
6
+ import logging
7
+ from typing import List
8
+ from pydantic import BaseModel
9
+ import time
10
+
11
+ # Setup logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ app = FastAPI(title="Embedding API")
16
+
17
+ # Load model on startup
18
+ try:
19
+ model = SentenceTransformer("all-MiniLM-L12-v2")
20
+ logger.info("Model loaded successfully")
21
+ except Exception as e:
22
+ logger.error(f"Failed to load model: {e}")
23
+ model = None
24
+
25
+ # Request batching queue
26
+ class EmbedRequest(BaseModel):
27
+ texts: List[str]
28
+
29
+ class BatchItem:
30
+ def __init__(self, texts: List[str]):
31
+ self.texts = texts
32
+ self.future: asyncio.Future = asyncio.Future()
33
+
34
+ pending_batch: List[BatchItem] = []
35
+ batch_lock = asyncio.Lock()
36
+ batch_event = asyncio.Event()
37
+
38
+ BATCH_TIMEOUT = 0.05 # 50ms window to collect requests
39
+ MAX_BATCH_SIZE = 32
40
+
41
+
42
+ async def batch_processor():
43
+ """Continuously process batches of requests"""
44
+ global pending_batch
45
+
46
+ while True:
47
+ try:
48
+ # Wait for requests or timeout
49
+ try:
50
+ await asyncio.wait_for(batch_event.wait(), timeout=BATCH_TIMEOUT)
51
+ batch_event.clear()
52
+ except asyncio.TimeoutError:
53
+ pass
54
+
55
+ async with batch_lock:
56
+ if not pending_batch:
57
+ continue
58
+
59
+ # Take up to MAX_BATCH_SIZE items
60
+ batch_to_process = pending_batch[:MAX_BATCH_SIZE]
61
+ pending_batch = pending_batch[MAX_BATCH_SIZE:]
62
+
63
+ if not batch_to_process:
64
+ continue
65
+
66
+ # Flatten all texts
67
+ all_texts = []
68
+ text_counts = []
69
+ for item in batch_to_process:
70
+ all_texts.extend(item.texts)
71
+ text_counts.append(len(item.texts))
72
+
73
+ logger.info(f"Processing batch of {len(batch_to_process)} requests, {len(all_texts)} texts total")
74
+
75
+ # Compute embeddings
76
+ start = time.time()
77
+ embeddings = model.encode(all_texts, convert_to_numpy=True)
78
+ elapsed = time.time() - start
79
+ logger.info(f"Embedding computed in {elapsed:.2f}s")
80
+
81
+ # Split embeddings back to individual requests
82
+ idx = 0
83
+ for item, count in zip(batch_to_process, text_counts):
84
+ item_embeddings = embeddings[idx:idx+count].tolist()
85
+ item.future.set_result(item_embeddings)
86
+ idx += count
87
+
88
+ except Exception as e:
89
+ logger.error(f"Error in batch processor: {e}")
90
+ async with batch_lock:
91
+ for item in pending_batch:
92
+ if not item.future.done():
93
+ item.future.set_exception(e)
94
+ pending_batch.clear()
95
+ await asyncio.sleep(1)
96
+
97
+
98
+ @app.on_event("startup")
99
+ async def startup():
100
+ """Start batch processor on app startup"""
101
+ if model is None:
102
+ raise RuntimeError("Model failed to load")
103
+ asyncio.create_task(batch_processor())
104
+ logger.info("Batch processor started")
105
+
106
+
107
+ @app.get("/health")
108
+ async def health():
109
+ """Health check endpoint"""
110
+ if model is None:
111
+ return JSONResponse({"status": "unhealthy", "reason": "model not loaded"}, status_code=503)
112
+ return {"status": "healthy"}
113
+
114
+
115
+ @app.post("/embed")
116
+ async def embed(request: EmbedRequest):
117
+ """
118
+ Embed texts. Automatically batches with concurrent requests.
119
+
120
+ Example:
121
+ ```
122
+ POST /embed
123
+ {
124
+ "texts": ["hello world", "foo bar"]
125
+ }
126
+ ```
127
+ """
128
+ if model is None:
129
+ raise HTTPException(status_code=503, detail="Model not ready")
130
+
131
+ if not request.texts:
132
+ raise HTTPException(status_code=400, detail="texts cannot be empty")
133
+
134
+ if len(request.texts) > 512:
135
+ raise HTTPException(status_code=400, detail="Cannot embed more than 512 texts at once")
136
+
137
+ # Create request item
138
+ batch_item = BatchItem(request.texts)
139
+
140
+ async with batch_lock:
141
+ pending_batch.append(batch_item)
142
+
143
+ # Signal processor if we hit batch size
144
+ if len(pending_batch) >= MAX_BATCH_SIZE:
145
+ batch_event.set()
146
+
147
+ # Signal processor that there's work (in case it's idle)
148
+ batch_event.set()
149
+
150
+ # Wait for result with timeout
151
+ try:
152
+ embeddings = await asyncio.wait_for(batch_item.future, timeout=30.0)
153
+ return {
154
+ "embeddings": embeddings,
155
+ "model": "all-MiniLM-L12-v2",
156
+ "count": len(request.texts)
157
+ }
158
+ except asyncio.TimeoutError:
159
+ raise HTTPException(status_code=504, detail="Embedding request timed out")
160
+
161
+
162
+ @app.get("/")
163
+ async def root():
164
+ """API info"""
165
+ return {
166
+ "name": "Embedding API",
167
+ "model": "all-MiniLM-L12-v2",
168
+ "endpoints": {
169
+ "POST /embed": "Embed texts",
170
+ "GET /health": "Health check"
171
+ }
172
+ }
173
+
174
+
175
+ if __name__ == "__main__":
176
+ import uvicorn
177
+ uvicorn.run(app, host="0.0.0.0", port=7860)