the-only-ashutosh commited on
Commit
d23fe11
·
1 Parent(s): bbad332

run via raw onnxruntime, drop torch/optimum (version conflict)

Browse files
Files changed (5) hide show
  1. Dockerfile +13 -13
  2. README.md +2 -2
  3. __pycache__/app.cpython-312.pyc +0 -0
  4. app.py +22 -16
  5. requirements.txt +3 -1
Dockerfile CHANGED
@@ -8,27 +8,27 @@ ENV HOME=/home/user \
8
  HF_HOME=/home/user/.cache/huggingface
9
  WORKDIR /home/user/app
10
 
11
- # CPU-only torch first so transformers/optimum don't drag in the ~2 GB CUDA build.
12
- RUN pip install --user --no-cache-dir torch==2.* --index-url https://download.pytorch.org/whl/cpu
13
-
14
  COPY --chown=user requirements.txt .
15
  RUN pip install --user --no-cache-dir -r requirements.txt
16
 
17
  COPY --chown=user app.py .
18
 
19
- # Bake the int8 ONNX model into the image (no cold pull on first request) and
20
- # assert it loads, pools to 1024-dim, and outputs unit-norm vectors.
21
  RUN python -c "\
22
- import torch, torch.nn.functional as F; \
23
- from optimum.onnxruntime import ORTModelForFeatureExtraction; \
24
  from transformers import AutoTokenizer; \
25
  m='libryo-ai/BAAI-bge-m3-int8'; \
26
- tok=AutoTokenizer.from_pretrained(m); mdl=ORTModelForFeatureExtraction.from_pretrained(m); \
27
- e=tok(['ભાવ સમાચાર','mandi prices'],padding=True,truncation=True,max_length=512,return_tensors='pt'); \
28
- v=F.normalize(mdl(**e).last_hidden_state[:,0],p=2,dim=1); \
29
- assert v.shape==(2,1024), v.shape; \
30
- assert abs(float(v[0].norm())-1.0)<1e-3; \
31
- print('model ok', v.shape)"
 
 
 
32
 
33
  EXPOSE 7860
34
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
8
  HF_HOME=/home/user/.cache/huggingface
9
  WORKDIR /home/user/app
10
 
 
 
 
11
  COPY --chown=user requirements.txt .
12
  RUN pip install --user --no-cache-dir -r requirements.txt
13
 
14
  COPY --chown=user app.py .
15
 
16
+ # Bake the int8 ONNX model + tokenizer into the image (no cold pull on first
17
+ # request) and assert it loads, pools to 1024-dim, and emits unit-norm vectors.
18
  RUN python -c "\
19
+ import numpy as np, onnxruntime as ort; \
20
+ from huggingface_hub import hf_hub_download; \
21
  from transformers import AutoTokenizer; \
22
  m='libryo-ai/BAAI-bge-m3-int8'; \
23
+ tok=AutoTokenizer.from_pretrained(m); \
24
+ sess=ort.InferenceSession(hf_hub_download(m,'model.onnx'),providers=['CPUExecutionProvider']); \
25
+ names={i.name for i in sess.get_inputs()}; \
26
+ e=tok(['ભાવ સમાચાર','mandi prices'],padding=True,truncation=True,max_length=512,return_tensors='np'); \
27
+ h=sess.run(None,{k:v for k,v in e.items() if k in names})[0]; \
28
+ c=h[:,0]; c=c/np.linalg.norm(c,axis=1,keepdims=True); \
29
+ assert c.shape==(2,1024), c.shape; \
30
+ assert abs(float(np.linalg.norm(c[0]))-1.0)<1e-3; \
31
+ print('model ok', c.shape)"
32
 
33
  EXPOSE 7860
34
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -12,8 +12,8 @@ pinned: false
12
 
13
  FastAPI `/embed` service hosting **`libryo-ai/BAAI-bge-m3-int8`** — the int8
14
  ONNX, dense-only build of bge-m3 (1024-dim, multilingual incl. Hindi/Gujarati).
15
- ~3× faster + ~half the RAM of fp32, negligible accuracy loss. Served via
16
- optimum `ORTModelForFeatureExtraction` with **CLS pooling + L2-normalize**.
17
  Offloads embedding off the NestJS API box (see `../INFRA-DEPLOYMENT-PLAN.md`).
18
  Deployed on a **free CPU Basic** Hugging Face Space (2 vCPU / 16 GB).
19
 
 
12
 
13
  FastAPI `/embed` service hosting **`libryo-ai/BAAI-bge-m3-int8`** — the int8
14
  ONNX, dense-only build of bge-m3 (1024-dim, multilingual incl. Hindi/Gujarati).
15
+ ~3× faster + ~half the RAM of fp32, negligible accuracy loss. Served via raw
16
+ `onnxruntime` (no torch/optimum) with **CLS pooling + L2-normalize**.
17
  Offloads embedding off the NestJS API box (see `../INFRA-DEPLOYMENT-PLAN.md`).
18
  Deployed on a **free CPU Basic** Hugging Face Space (2 vCPU / 16 GB).
19
 
__pycache__/app.cpython-312.pyc CHANGED
Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ
 
app.py CHANGED
@@ -1,28 +1,33 @@
1
  import os
2
  from contextlib import asynccontextmanager
3
 
4
- import torch
5
- import torch.nn.functional as F
6
  from fastapi import FastAPI, Header, HTTPException
 
7
  from pydantic import BaseModel
8
- from optimum.onnxruntime import ORTModelForFeatureExtraction
9
  from transformers import AutoTokenizer
10
 
11
- # int8 ONNX bge-m3, dense-only. ~3x faster + ~half RAM vs fp32 sentence-transformers.
 
12
  MODEL_NAME = os.getenv("EMBED_MODEL", "libryo-ai/BAAI-bge-m3-int8")
 
13
  EMBED_TOKEN = os.getenv("EMBED_TOKEN") # optional shared secret
14
  MAX_TOKENS = int(os.getenv("EMBED_MAX_TOKENS", "512"))
15
  BATCH = int(os.getenv("EMBED_BATCH", "32"))
16
 
17
- _model: ORTModelForFeatureExtraction | None = None
18
  _tokenizer = None
 
19
 
20
 
21
  @asynccontextmanager
22
  async def lifespan(_: FastAPI):
23
- global _model, _tokenizer
24
  _tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
25
- _model = ORTModelForFeatureExtraction.from_pretrained(MODEL_NAME)
 
 
26
  yield
27
 
28
 
@@ -41,7 +46,7 @@ class EmbedResponse(BaseModel):
41
 
42
  @app.get("/")
43
  def health():
44
- return {"status": "ok", "model": MODEL_NAME, "ready": _model is not None}
45
 
46
 
47
  def _encode(texts: list[str]) -> list[list[float]]:
@@ -50,14 +55,15 @@ def _encode(texts: list[str]) -> list[list[float]]:
50
  chunk = texts[i : i + BATCH]
51
  enc = _tokenizer(
52
  chunk, padding=True, truncation=True,
53
- max_length=MAX_TOKENS, return_tensors="pt",
54
  )
55
- with torch.no_grad():
56
- hidden = _model(**enc).last_hidden_state
57
- # bge-m3 dense embedding = CLS token (position 0), then L2-normalize
58
- # so cosine == dot product downstream.
59
- cls = F.normalize(hidden[:, 0], p=2, dim=1)
60
- out.extend(cls.tolist())
 
61
  return out
62
 
63
 
@@ -67,7 +73,7 @@ def _encode(texts: list[str]) -> list[list[float]]:
67
  def embed(req: EmbedRequest, authorization: str | None = Header(default=None)):
68
  if EMBED_TOKEN and authorization != f"Bearer {EMBED_TOKEN}":
69
  raise HTTPException(status_code=401, detail="unauthorized")
70
- if _model is None:
71
  raise HTTPException(status_code=503, detail="model loading")
72
  if not req.texts:
73
  raise HTTPException(status_code=400, detail="texts required")
 
1
  import os
2
  from contextlib import asynccontextmanager
3
 
4
+ import numpy as np
5
+ import onnxruntime as ort
6
  from fastapi import FastAPI, Header, HTTPException
7
+ from huggingface_hub import hf_hub_download
8
  from pydantic import BaseModel
 
9
  from transformers import AutoTokenizer
10
 
11
+ # int8 ONNX bge-m3, dense-only. Run raw via onnxruntime no torch/optimum, so
12
+ # no export-path version conflicts. ~3x faster + ~half RAM vs fp32.
13
  MODEL_NAME = os.getenv("EMBED_MODEL", "libryo-ai/BAAI-bge-m3-int8")
14
+ ONNX_FILE = os.getenv("EMBED_ONNX_FILE", "model.onnx")
15
  EMBED_TOKEN = os.getenv("EMBED_TOKEN") # optional shared secret
16
  MAX_TOKENS = int(os.getenv("EMBED_MAX_TOKENS", "512"))
17
  BATCH = int(os.getenv("EMBED_BATCH", "32"))
18
 
19
+ _session: ort.InferenceSession | None = None
20
  _tokenizer = None
21
+ _input_names: set[str] = set()
22
 
23
 
24
  @asynccontextmanager
25
  async def lifespan(_: FastAPI):
26
+ global _session, _tokenizer, _input_names
27
  _tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
28
+ path = hf_hub_download(MODEL_NAME, ONNX_FILE)
29
+ _session = ort.InferenceSession(path, providers=["CPUExecutionProvider"])
30
+ _input_names = {i.name for i in _session.get_inputs()}
31
  yield
32
 
33
 
 
46
 
47
  @app.get("/")
48
  def health():
49
+ return {"status": "ok", "model": MODEL_NAME, "ready": _session is not None}
50
 
51
 
52
  def _encode(texts: list[str]) -> list[list[float]]:
 
55
  chunk = texts[i : i + BATCH]
56
  enc = _tokenizer(
57
  chunk, padding=True, truncation=True,
58
+ max_length=MAX_TOKENS, return_tensors="np",
59
  )
60
+ # bge-m3 (xlm-roberta) has no token_type_ids; feed only what the graph wants.
61
+ feed = {k: v for k, v in enc.items() if k in _input_names}
62
+ hidden = _session.run(None, feed)[0] # (B, T, 1024) last_hidden_state
63
+ # dense embedding = CLS token (position 0), then L2-normalize so cosine == dot.
64
+ cls = hidden[:, 0]
65
+ cls = cls / np.clip(np.linalg.norm(cls, axis=1, keepdims=True), 1e-12, None)
66
+ out.extend(cls.astype(np.float32).tolist())
67
  return out
68
 
69
 
 
73
  def embed(req: EmbedRequest, authorization: str | None = Header(default=None)):
74
  if EMBED_TOKEN and authorization != f"Bearer {EMBED_TOKEN}":
75
  raise HTTPException(status_code=401, detail="unauthorized")
76
+ if _session is None:
77
  raise HTTPException(status_code=503, detail="model loading")
78
  if not req.texts:
79
  raise HTTPException(status_code=400, detail="texts required")
requirements.txt CHANGED
@@ -1,4 +1,6 @@
1
  fastapi==0.115.*
2
  uvicorn[standard]==0.32.*
3
- optimum[onnxruntime]==1.*
4
  transformers==4.*
 
 
 
1
  fastapi==0.115.*
2
  uvicorn[standard]==0.32.*
3
+ onnxruntime==1.*
4
  transformers==4.*
5
+ huggingface_hub==0.*
6
+ numpy<2