File size: 3,460 Bytes
795547d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | from pathlib import Path
import os
import json
import unicodedata
import numpy as np
import sentencepiece as spm
import torch
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
ROOT = Path(__file__).resolve().parent
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
MODEL = torch.jit.load(str(ROOT / "splinet_embedder.ts"), map_location=DEVICE).eval()
SP = spm.SentencePieceProcessor(model_file=str(ROOT / "spiece.model"))
PAD_ID = 3
CLS_ID = 4
SEP_ID = 5
MAX_LENGTH = 512
DEFAULT_BATCH = int(os.environ.get("SPLINET_BATCH_SIZE", "256" if DEVICE.type == "cuda" else "8"))
def normalize_text(text):
text = unicodedata.normalize("NFKC", text or "").lower()
return " ".join(text.split())
def tokenize(texts):
ids = torch.full((len(texts), MAX_LENGTH), PAD_ID, dtype=torch.long)
mask = torch.zeros((len(texts), MAX_LENGTH), dtype=torch.long)
for i, text in enumerate(texts):
pieces = list(SP.encode(normalize_text(text), out_type=int))[:MAX_LENGTH - 2]
seq = [CLS_ID] + pieces + [SEP_ID]
n = len(seq)
ids[i, :n] = torch.tensor(seq, dtype=torch.long)
mask[i, :n] = 1
return ids, mask
def encode(texts, batch_size=None):
if isinstance(texts, str):
texts = [texts]
if not texts:
return np.empty((0, 768), dtype=np.float32)
bs = max(1, int(batch_size or DEFAULT_BATCH))
output = []
start = 0
while start < len(texts):
current = min(bs, len(texts) - start)
try:
ids, mask = tokenize(texts[start:start + current])
ids = ids.to(DEVICE)
mask = mask.to(DEVICE)
with torch.inference_mode():
if DEVICE.type == "cuda":
with torch.autocast(device_type="cuda", dtype=torch.float16):
z = MODEL(ids, mask)
else:
z = MODEL(ids, mask)
output.append(z.float().cpu().numpy())
start += current
except torch.OutOfMemoryError:
if DEVICE.type != "cuda" or current == 1:
raise
torch.cuda.empty_cache()
bs = max(1, current // 2)
return np.concatenate(output, axis=0).astype(np.float32)
def similarity(text_a, text_b):
z = encode([text_a, text_b])
return float(np.dot(z[0], z[1]))
class EmbedRequest(BaseModel):
texts: list[str]
batch_size: int | None = None
class SimilarityRequest(BaseModel):
text_a: str
text_b: str
app = FastAPI(title="SpliNet Embedder API", version="1.0.0")
@app.get("/health")
def health():
return {"status": "ok", "device": str(DEVICE)}
@app.get("/model-info")
def model_info():
with open(ROOT / "model_info.json", "r", encoding="utf-8") as f:
return json.load(f)
@app.post("/embed")
def embed_endpoint(req: EmbedRequest):
try:
z = encode(req.texts, req.batch_size)
return {"count": len(req.texts), "dimension": 768, "embeddings": z.tolist()}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/similarity")
def similarity_endpoint(req: SimilarityRequest):
try:
return {"cosine_similarity": similarity(req.text_a, req.text_b)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8000")))
|