Gaston895 commited on
Commit
4e6a553
·
1 Parent(s): 251fc50

fix: lifespan handler, dtype= for transformers 5.x, remove UI, update README

Browse files
Files changed (2) hide show
  1. README.md +49 -25
  2. app.py +24 -33
README.md CHANGED
@@ -8,64 +8,88 @@ pinned: false
8
  app_port: 7860
9
  ---
10
 
11
- # LFM2-700M — FastAPI Inference Space
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` | `/` | Interactive HTML docs page |
20
- | `GET` | `/health` | Liveness check |
21
- | `POST` | `/chat` | Generate a response |
22
 
23
- ## POST /chat — request body
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  ```json
26
  {
27
  "messages": [
28
- {"role": "user", "content": "What is C. elegans?"}
 
29
  ],
30
- "max_new_tokens": 512,
31
  "temperature": 0.3,
32
  "min_p": 0.15,
33
  "repetition_penalty": 1.05,
34
- "stream": false
35
  }
36
  ```
37
 
38
- Set `"stream": true` to receive a streaming `text/plain` response via chunked transfer.
 
 
 
 
 
 
 
39
 
40
- ## POST /chat — response body (non-streaming)
41
 
42
  ```json
43
  {
44
- "response": "C. elegans, also known as Caenorhabditis elegans, is a small ..."
 
 
45
  }
46
  ```
47
 
 
 
 
 
 
 
 
 
48
  ## Local development
49
 
50
  ```bash
51
  pip install -r requirements.txt
52
- uvicorn app:app --reload --port 7860
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 HTMLResponse, StreamingResponse, JSONResponse
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 is not reliably supported on all CPUs fall back to float32
39
- _dtype = torch.bfloat16 if torch.backends.cpu.get_cpu_capability() >= "avx512" else torch.float32
40
-
41
- # ---------------------------------------------------------------------------
42
- # App — model is loaded in the startup event so FastAPI controls timing
43
- # and any exception is visible in logs rather than killing the process silently
44
- # ---------------------------------------------------------------------------
45
- app = FastAPI(title="LFM2-700M Inference API")
46
 
47
  model = None
48
  tokenizer = None
49
  _model_ready = False
50
 
51
 
52
- @app.on_event("startup")
53
- async def load_model():
 
 
 
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: {_dtype} threads: {_num_threads} authenticated: {bool(_hf_token)}")
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
- torch_dtype=_dtype,
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
- # Don't crash uvicorn — /health will report not ready
 
87
 
88
 
89
  # ---------------------------------------------------------------------------
90
- # Validation error handler
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 # "user" | "assistant" | "system"
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 # default to streaming on CPU
122
 
123
 
124
  class ChatResponse(BaseModel):
@@ -134,7 +135,7 @@ class AskRequest(BaseModel):
134
 
135
 
136
  # ---------------------------------------------------------------------------
137
- # Shared helpers
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 {