fix: lifespan handler, dtype= for transformers 5.x, remove UI, update README
Browse files
README.md
CHANGED
|
@@ -8,64 +8,88 @@ pinned: false
|
|
| 8 |
app_port: 7860
|
| 9 |
---
|
| 10 |
|
| 11 |
-
# LFM2-700M — FastAPI Inference
|
| 12 |
|
| 13 |
-
Runs [gsstec/LFM2-700M](https://huggingface.co/gsstec/LFM2-700M) behind a lightweight **FastAPI** server.
|
| 14 |
|
| 15 |
## Endpoints
|
| 16 |
|
| 17 |
| Method | Path | Description |
|
| 18 |
|--------|------|-------------|
|
| 19 |
-
| `GET` | `/`
|
| 20 |
-
| `
|
| 21 |
-
| `POST` | `/
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
```json
|
| 26 |
{
|
| 27 |
"messages": [
|
| 28 |
-
{"role": "
|
|
|
|
| 29 |
],
|
| 30 |
-
"max_new_tokens":
|
| 31 |
"temperature": 0.3,
|
| 32 |
"min_p": 0.15,
|
| 33 |
"repetition_penalty": 1.05,
|
| 34 |
-
"stream":
|
| 35 |
}
|
| 36 |
```
|
| 37 |
|
| 38 |
-
Set `"stream": true` to receive a
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
## POST /
|
| 41 |
|
| 42 |
```json
|
| 43 |
{
|
| 44 |
-
"
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
```
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
## Local development
|
| 49 |
|
| 50 |
```bash
|
| 51 |
pip install -r requirements.txt
|
| 52 |
-
|
| 53 |
```
|
| 54 |
|
| 55 |
## Docker
|
| 56 |
|
| 57 |
```bash
|
| 58 |
docker build -t lfm2-700m .
|
| 59 |
-
docker run -p 7860:7860 lfm2-700m
|
| 60 |
```
|
| 61 |
-
|
| 62 |
-
## Deploy to HuggingFace Spaces
|
| 63 |
-
|
| 64 |
-
1. Create a new Space at https://huggingface.co/new-space → choose **Docker** SDK
|
| 65 |
-
2. Push all files to the Space repo:
|
| 66 |
-
```bash
|
| 67 |
-
git remote add space https://huggingface.co/spaces/<your-org>/<space-name>
|
| 68 |
-
git push space main
|
| 69 |
-
```
|
| 70 |
-
|
| 71 |
-
> **GPU tip:** Upgrade the Space hardware to a GPU tier and uncomment `attn_implementation="flash_attention_2"` in `app.py` for faster inference.
|
|
|
|
| 8 |
app_port: 7860
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# LFM2-700M — FastAPI Inference API
|
| 12 |
|
| 13 |
+
Runs [gsstec/LFM2-700M](https://huggingface.co/gsstec/LFM2-700M) behind a lightweight **FastAPI** server on CPU.
|
| 14 |
|
| 15 |
## Endpoints
|
| 16 |
|
| 17 |
| Method | Path | Description |
|
| 18 |
|--------|------|-------------|
|
| 19 |
+
| `GET` | `/health` | Liveness + readiness check |
|
| 20 |
+
| `POST` | `/chat` | Multi-turn chat (supports streaming) |
|
| 21 |
+
| `POST` | `/ask` | Single-turn question |
|
| 22 |
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## GET /health
|
| 26 |
+
|
| 27 |
+
```json
|
| 28 |
+
{
|
| 29 |
+
"status": "ok",
|
| 30 |
+
"model": "gsstec/LFM2-700M",
|
| 31 |
+
"ready": true,
|
| 32 |
+
"dtype": "torch.float32",
|
| 33 |
+
"cpu_threads": 16,
|
| 34 |
+
"authenticated": true
|
| 35 |
+
}
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## POST /chat
|
| 41 |
|
| 42 |
```json
|
| 43 |
{
|
| 44 |
"messages": [
|
| 45 |
+
{"role": "system", "content": "You are a helpful e-commerce assistant."},
|
| 46 |
+
{"role": "user", "content": "How do I reduce cart abandonment?"}
|
| 47 |
],
|
| 48 |
+
"max_new_tokens": 256,
|
| 49 |
"temperature": 0.3,
|
| 50 |
"min_p": 0.15,
|
| 51 |
"repetition_penalty": 1.05,
|
| 52 |
+
"stream": true
|
| 53 |
}
|
| 54 |
```
|
| 55 |
|
| 56 |
+
Set `"stream": true` to receive a `text/plain` chunked streaming response.
|
| 57 |
+
Set `"stream": false` to receive:
|
| 58 |
+
|
| 59 |
+
```json
|
| 60 |
+
{ "response": "Here are some tips to reduce cart abandonment..." }
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
|
| 65 |
+
## POST /ask
|
| 66 |
|
| 67 |
```json
|
| 68 |
{
|
| 69 |
+
"question": "What is a good return policy for an online store?",
|
| 70 |
+
"max_new_tokens": 128,
|
| 71 |
+
"temperature": 0.3
|
| 72 |
}
|
| 73 |
```
|
| 74 |
|
| 75 |
+
Response:
|
| 76 |
+
|
| 77 |
+
```json
|
| 78 |
+
{ "response": "A good return policy should be..." }
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
## Local development
|
| 84 |
|
| 85 |
```bash
|
| 86 |
pip install -r requirements.txt
|
| 87 |
+
HF_TOKEN=hf_... python app.py
|
| 88 |
```
|
| 89 |
|
| 90 |
## Docker
|
| 91 |
|
| 92 |
```bash
|
| 93 |
docker build -t lfm2-700m .
|
| 94 |
+
docker run -e HF_TOKEN=hf_... -p 7860:7860 lfm2-700m
|
| 95 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -3,6 +3,7 @@ import json
|
|
| 3 |
import torch
|
| 4 |
import logging
|
| 5 |
import traceback
|
|
|
|
| 6 |
|
| 7 |
# ---------------------------------------------------------------------------
|
| 8 |
# Environment must be configured BEFORE importing transformers/huggingface_hub
|
|
@@ -10,15 +11,13 @@ import traceback
|
|
| 10 |
os.environ.setdefault("HF_HOME", "/app/model_cache")
|
| 11 |
os.environ.setdefault("TRANSFORMERS_CACHE", "/app/model_cache")
|
| 12 |
|
| 13 |
-
# Propagate HF_TOKEN so authenticated requests are made automatically
|
| 14 |
_hf_token = os.getenv("HF_TOKEN")
|
| 15 |
if _hf_token:
|
| 16 |
os.environ["HUGGING_FACE_HUB_TOKEN"] = _hf_token
|
| 17 |
-
os.environ["HF_TOKEN"] = _hf_token
|
| 18 |
|
| 19 |
from fastapi import FastAPI, HTTPException, Request
|
| 20 |
from fastapi.exceptions import RequestValidationError
|
| 21 |
-
from fastapi.responses import
|
| 22 |
from pydantic import BaseModel
|
| 23 |
from typing import List
|
| 24 |
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
|
|
@@ -30,32 +29,32 @@ logging.getLogger("uvicorn.access").setLevel(logging.INFO)
|
|
| 30 |
|
| 31 |
MODEL_ID = os.getenv("MODEL_ID", "gsstec/LFM2-700M")
|
| 32 |
|
| 33 |
-
# Max out CPU threads for faster matrix ops
|
| 34 |
_num_threads = int(os.getenv("NUM_THREADS", os.cpu_count() or 4))
|
| 35 |
torch.set_num_threads(_num_threads)
|
| 36 |
torch.set_num_interop_threads(max(1, _num_threads // 2))
|
| 37 |
|
| 38 |
-
# BF16
|
| 39 |
-
_dtype =
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# ---------------------------------------------------------------------------
|
| 45 |
-
app = FastAPI(title="LFM2-700M Inference API")
|
| 46 |
|
| 47 |
model = None
|
| 48 |
tokenizer = None
|
| 49 |
_model_ready = False
|
| 50 |
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
| 54 |
global model, tokenizer, _model_ready
|
| 55 |
_token_kwarg = {"token": _hf_token} if _hf_token else {}
|
| 56 |
|
| 57 |
print(f"MODEL: {MODEL_ID}")
|
| 58 |
-
print(f"dtype
|
| 59 |
|
| 60 |
try:
|
| 61 |
print("Loading tokenizer...")
|
|
@@ -72,9 +71,8 @@ async def load_model():
|
|
| 72 |
print("Loading model...")
|
| 73 |
model = AutoModelForCausalLM.from_pretrained(
|
| 74 |
MODEL_ID,
|
| 75 |
-
|
| 76 |
low_cpu_mem_usage=True,
|
| 77 |
-
# attn_implementation="flash_attention_2", # uncomment on GPU
|
| 78 |
**_token_kwarg,
|
| 79 |
)
|
| 80 |
model.eval()
|
|
@@ -83,12 +81,16 @@ async def load_model():
|
|
| 83 |
|
| 84 |
except Exception:
|
| 85 |
traceback.print_exc()
|
| 86 |
-
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
# ---------------------------------------------------------------------------
|
| 90 |
-
#
|
| 91 |
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
| 92 |
@app.exception_handler(RequestValidationError)
|
| 93 |
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
| 94 |
body = await request.body()
|
|
@@ -97,7 +99,6 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
|
| 97 |
for k, v in err.items()}
|
| 98 |
for err in exc.errors()
|
| 99 |
]
|
| 100 |
-
logger.error(f"422 {request.method} {request.url} | {safe_errors}")
|
| 101 |
return JSONResponse(
|
| 102 |
status_code=422,
|
| 103 |
content={"detail": safe_errors, "body_received": body.decode(errors="replace")},
|
|
@@ -108,7 +109,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
|
| 108 |
# Schemas
|
| 109 |
# ---------------------------------------------------------------------------
|
| 110 |
class Message(BaseModel):
|
| 111 |
-
role: str
|
| 112 |
content: str
|
| 113 |
|
| 114 |
|
|
@@ -118,7 +119,7 @@ class ChatRequest(BaseModel):
|
|
| 118 |
temperature: float = 0.3
|
| 119 |
min_p: float = 0.15
|
| 120 |
repetition_penalty: float = 1.05
|
| 121 |
-
stream: bool = True
|
| 122 |
|
| 123 |
|
| 124 |
class ChatResponse(BaseModel):
|
|
@@ -134,7 +135,7 @@ class AskRequest(BaseModel):
|
|
| 134 |
|
| 135 |
|
| 136 |
# ---------------------------------------------------------------------------
|
| 137 |
-
#
|
| 138 |
# ---------------------------------------------------------------------------
|
| 139 |
def _check_ready():
|
| 140 |
if not _model_ready:
|
|
@@ -172,16 +173,6 @@ def _generate(input_ids: torch.Tensor, req, streamer=None) -> torch.Tensor:
|
|
| 172 |
# ---------------------------------------------------------------------------
|
| 173 |
# Routes
|
| 174 |
# ---------------------------------------------------------------------------
|
| 175 |
-
_UI_HTML = open(os.path.join(os.path.dirname(__file__), "static", "index.html"), encoding="utf-8").read()
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
@app.get("/", response_class=HTMLResponse)
|
| 179 |
-
@app.get("/ui", response_class=HTMLResponse)
|
| 180 |
-
def ui():
|
| 181 |
-
"""Chat UI — served at both / and /ui"""
|
| 182 |
-
return _UI_HTML
|
| 183 |
-
|
| 184 |
-
|
| 185 |
@app.get("/health")
|
| 186 |
def health():
|
| 187 |
return {
|
|
|
|
| 3 |
import torch
|
| 4 |
import logging
|
| 5 |
import traceback
|
| 6 |
+
from contextlib import asynccontextmanager
|
| 7 |
|
| 8 |
# ---------------------------------------------------------------------------
|
| 9 |
# Environment must be configured BEFORE importing transformers/huggingface_hub
|
|
|
|
| 11 |
os.environ.setdefault("HF_HOME", "/app/model_cache")
|
| 12 |
os.environ.setdefault("TRANSFORMERS_CACHE", "/app/model_cache")
|
| 13 |
|
|
|
|
| 14 |
_hf_token = os.getenv("HF_TOKEN")
|
| 15 |
if _hf_token:
|
| 16 |
os.environ["HUGGING_FACE_HUB_TOKEN"] = _hf_token
|
|
|
|
| 17 |
|
| 18 |
from fastapi import FastAPI, HTTPException, Request
|
| 19 |
from fastapi.exceptions import RequestValidationError
|
| 20 |
+
from fastapi.responses import StreamingResponse, JSONResponse
|
| 21 |
from pydantic import BaseModel
|
| 22 |
from typing import List
|
| 23 |
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
|
|
|
|
| 29 |
|
| 30 |
MODEL_ID = os.getenv("MODEL_ID", "gsstec/LFM2-700M")
|
| 31 |
|
|
|
|
| 32 |
_num_threads = int(os.getenv("NUM_THREADS", os.cpu_count() or 4))
|
| 33 |
torch.set_num_threads(_num_threads)
|
| 34 |
torch.set_num_interop_threads(max(1, _num_threads // 2))
|
| 35 |
|
| 36 |
+
# BF16 needs AVX-512; HF CPU Spaces are typically AVX2 → use float32
|
| 37 |
+
_dtype = (
|
| 38 |
+
torch.bfloat16
|
| 39 |
+
if torch.backends.cpu.get_cpu_capability() >= "avx512"
|
| 40 |
+
else torch.float32
|
| 41 |
+
)
|
|
|
|
|
|
|
| 42 |
|
| 43 |
model = None
|
| 44 |
tokenizer = None
|
| 45 |
_model_ready = False
|
| 46 |
|
| 47 |
|
| 48 |
+
# ---------------------------------------------------------------------------
|
| 49 |
+
# Lifespan — loads model after uvicorn binds the port
|
| 50 |
+
# ---------------------------------------------------------------------------
|
| 51 |
+
@asynccontextmanager
|
| 52 |
+
async def lifespan(app: FastAPI):
|
| 53 |
global model, tokenizer, _model_ready
|
| 54 |
_token_kwarg = {"token": _hf_token} if _hf_token else {}
|
| 55 |
|
| 56 |
print(f"MODEL: {MODEL_ID}")
|
| 57 |
+
print(f"dtype={_dtype} threads={_num_threads} authenticated={bool(_hf_token)}")
|
| 58 |
|
| 59 |
try:
|
| 60 |
print("Loading tokenizer...")
|
|
|
|
| 71 |
print("Loading model...")
|
| 72 |
model = AutoModelForCausalLM.from_pretrained(
|
| 73 |
MODEL_ID,
|
| 74 |
+
dtype=_dtype,
|
| 75 |
low_cpu_mem_usage=True,
|
|
|
|
| 76 |
**_token_kwarg,
|
| 77 |
)
|
| 78 |
model.eval()
|
|
|
|
| 81 |
|
| 82 |
except Exception:
|
| 83 |
traceback.print_exc()
|
| 84 |
+
|
| 85 |
+
yield # server is running
|
| 86 |
|
| 87 |
|
| 88 |
# ---------------------------------------------------------------------------
|
| 89 |
+
# App
|
| 90 |
# ---------------------------------------------------------------------------
|
| 91 |
+
app = FastAPI(title="LFM2-700M Inference API", lifespan=lifespan)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
@app.exception_handler(RequestValidationError)
|
| 95 |
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
| 96 |
body = await request.body()
|
|
|
|
| 99 |
for k, v in err.items()}
|
| 100 |
for err in exc.errors()
|
| 101 |
]
|
|
|
|
| 102 |
return JSONResponse(
|
| 103 |
status_code=422,
|
| 104 |
content={"detail": safe_errors, "body_received": body.decode(errors="replace")},
|
|
|
|
| 109 |
# Schemas
|
| 110 |
# ---------------------------------------------------------------------------
|
| 111 |
class Message(BaseModel):
|
| 112 |
+
role: str
|
| 113 |
content: str
|
| 114 |
|
| 115 |
|
|
|
|
| 119 |
temperature: float = 0.3
|
| 120 |
min_p: float = 0.15
|
| 121 |
repetition_penalty: float = 1.05
|
| 122 |
+
stream: bool = True
|
| 123 |
|
| 124 |
|
| 125 |
class ChatResponse(BaseModel):
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
# ---------------------------------------------------------------------------
|
| 138 |
+
# Helpers
|
| 139 |
# ---------------------------------------------------------------------------
|
| 140 |
def _check_ready():
|
| 141 |
if not _model_ready:
|
|
|
|
| 173 |
# ---------------------------------------------------------------------------
|
| 174 |
# Routes
|
| 175 |
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
@app.get("/health")
|
| 177 |
def health():
|
| 178 |
return {
|