SeaWolf-AI commited on
Commit
95738ea
·
verified ·
1 Parent(s): e876fc5

serving: manual load + GenerationMixin + use_cache=False + streaming

Browse files
Files changed (1) hide show
  1. app.py +146 -52
app.py CHANGED
@@ -1,48 +1,92 @@
1
  # -*- coding: utf-8 -*-
2
- """Aether-7B-5Attn serving Space — FastAPI backend + static index.html.
3
 
4
- Serves the fully-open Aether model. The model is lazy-loaded on the first /api/generate call so
5
- the landing page (SEO/AEO) is instant. batch_size=1 is enforced (NSA branches ignore padding
6
- masks). Runs on GPU when available; on CPU it still works but is slow, so a clear status is
7
- returned. If the model can't be loaded (private + no token, or OOM), the page still serves and
8
- the API returns a friendly, honest message instead of crashing.
 
 
 
 
 
 
 
 
9
  """
10
- import os, threading, time
11
  from fastapi import FastAPI
12
- from fastapi.responses import HTMLResponse, JSONResponse
13
  from pydantic import BaseModel
14
 
15
  MODEL_ID = os.environ.get("MODEL_ID", "FINAL-Bench/Aether-7B-5Attn-it")
16
- HF_TOKEN = os.environ.get("HF_TOKEN") # Space secret; needed while the model repo is private
17
- MAX_NEW = int(os.environ.get("MAX_NEW_TOKENS", "256"))
18
 
19
  app = FastAPI(title="Aether-7B-5Attn — Sovereign Open-Source AI")
20
 
21
  _state = {"model": None, "tok": None, "device": None, "status": "not_loaded", "error": None}
22
- _lock = threading.Lock()
 
 
 
 
23
 
24
 
25
  def _load():
26
- """Lazy, thread-safe model load. Sets _state; never raises to the request path."""
27
  if _state["model"] is not None or _state["status"] == "loading":
28
  return
29
- with _lock:
30
  if _state["model"] is not None:
31
  return
32
  _state["status"] = "loading"
33
  try:
34
- import torch
35
- from transformers import AutoTokenizer, AutoModelForCausalLM
 
 
 
 
 
 
 
 
 
 
 
36
  dev = "cuda" if torch.cuda.is_available() else "cpu"
37
- dtype = torch.bfloat16 if dev == "cuda" else torch.float32
38
- tok = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN, trust_remote_code=True)
39
- model = AutoModelForCausalLM.from_pretrained(
40
- MODEL_ID, token=HF_TOKEN, trust_remote_code=True,
41
- torch_dtype=dtype, low_cpu_mem_usage=True,
42
- ).to(dev).eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  _state.update(model=model, tok=tok, device=dev, status="ready", error=None)
44
- except Exception as e: # honest degrade — page still works
45
- _state.update(status="error", error=str(e)[:400])
 
 
 
 
 
46
 
47
 
48
  class GenReq(BaseModel):
@@ -51,47 +95,97 @@ class GenReq(BaseModel):
51
  temperature: float | None = 0.7
52
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  @app.get("/api/health")
55
  def health():
56
  return {"status": _state["status"], "device": _state["device"], "model": MODEL_ID,
57
  "error": _state["error"]}
58
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  @app.post("/api/generate")
61
  def generate(req: GenReq):
62
  if _state["status"] in ("not_loaded", "loading"):
63
  _load()
64
  if _state["status"] != "ready":
65
- return JSONResponse(status_code=503, content={
66
- "ok": False,
67
- "status": _state["status"],
68
- "message": ("Model is still loading — try again in a moment."
69
- if _state["status"] == "loading" else
70
- "Model is not available on this hardware yet. "
71
- "Serving Aether-7B-5Attn (6.59B) needs a GPU, and the repo must be public "
72
- "or a HF_TOKEN secret must be set. Details: " + (_state["error"] or "")),
73
- })
74
- import torch
75
- tok, model, dev = _state["tok"], _state["model"], _state["device"]
76
- prompt = (req.prompt or "").strip()[:4000]
77
- if not prompt:
78
  return JSONResponse(status_code=400, content={"ok": False, "message": "empty prompt"})
79
- # batch_size=1 ONLY — NSA branches ignore padding masks.
80
- ids = tok(prompt, return_tensors="pt").input_ids.to(dev)
81
- t0 = time.time()
82
- with torch.no_grad():
83
- out = model.generate(
84
- ids, max_new_tokens=min(req.max_new_tokens or MAX_NEW, 512),
85
- do_sample=(req.temperature or 0) > 0, temperature=max(req.temperature or 0.7, 0.01),
86
- top_p=0.9, repetition_penalty=1.1,
87
- pad_token_id=getattr(tok, "eos_token_id", None),
88
- )
89
- text = tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True)
90
- dt = time.time() - t0
91
- ntok = out.shape[1] - ids.shape[1]
92
- return {"ok": True, "completion": text, "tokens": int(ntok),
93
- "seconds": round(dt, 2), "tok_per_s": round(ntok / dt, 1) if dt else None,
94
- "device": dev}
95
 
96
 
97
  @app.get("/", response_class=HTMLResponse)
 
1
  # -*- coding: utf-8 -*-
2
+ """Aether-7B-5Attn serving Space — FastAPI + static index.html, with live token streaming.
3
 
4
+ Loading notes (this is a custom `aether_v2_7way` architecture, validated end-to-end on a T4):
5
+ * The repo ships the architecture in `aether_pkg/`; we snapshot it, import it, and subclass with
6
+ `GenerationMixin` (Transformers 5.x no longer gives `PreTrainedModel` a `.generate`).
7
+ * Random weight init is skipped (it is overwritten by the checkpoint) to keep cold-start sane.
8
+ * Weights load straight onto the GPU and the model moves over, so a 6.59B bf16 (~13.3 GB) model
9
+ fits a single T4 (16 GB) without a 2x memory spike.
10
+ * `use_cache=False`: the NSA attention branch uses custom KV-cache indices that stock
11
+ `DynamicCache` does not provide (fast KV-caching is a separate serving build), so generation
12
+ runs without a cache — correct but not fast. Output is streamed so tokens appear as produced;
13
+ `max_new_tokens` is capped and inference is batch_size = 1 (NSA ignores padding masks).
14
+
15
+ The model pre-warms on startup. If it cannot load, the landing page still serves and the API
16
+ returns an honest message instead of crashing.
17
  """
18
+ import os, sys, threading, time
19
  from fastapi import FastAPI
20
+ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
21
  from pydantic import BaseModel
22
 
23
  MODEL_ID = os.environ.get("MODEL_ID", "FINAL-Bench/Aether-7B-5Attn-it")
24
+ HF_TOKEN = os.environ.get("HF_TOKEN") # optional now the repo is public
25
+ MAX_NEW = min(int(os.environ.get("MAX_NEW_TOKENS", "64")), 200) # no KV cache → keep it short
26
 
27
  app = FastAPI(title="Aether-7B-5Attn — Sovereign Open-Source AI")
28
 
29
  _state = {"model": None, "tok": None, "device": None, "status": "not_loaded", "error": None}
30
+ _load_lock = threading.Lock()
31
+ _gen_lock = threading.Lock() # single GPU demo → one generation at a time
32
+
33
+ NOT_READY = ("Model is not available on this hardware yet. Serving Aether-7B-5Attn (6.59B) needs a "
34
+ "GPU. Details: ")
35
 
36
 
37
  def _load():
38
+ """Lazy, thread-safe load. Never raises to the request path."""
39
  if _state["model"] is not None or _state["status"] == "loading":
40
  return
41
+ with _load_lock:
42
  if _state["model"] is not None:
43
  return
44
  _state["status"] = "loading"
45
  try:
46
+ import torch, torch.nn as nn
47
+ from huggingface_hub import snapshot_download
48
+ from safetensors.torch import load_file
49
+ from transformers import AutoTokenizer, GenerationMixin
50
+ local = snapshot_download(MODEL_ID, token=HF_TOKEN)
51
+ if local not in sys.path:
52
+ sys.path.insert(0, local)
53
+ from aether_pkg.configuration_aether_v2_7way import AETHERV27wayConfig
54
+ from aether_pkg.modeling_aether_v2_7way import AETHERV27wayForCausalLM
55
+
56
+ class _AetherGen(AETHERV27wayForCausalLM, GenerationMixin):
57
+ pass
58
+
59
  dev = "cuda" if torch.cuda.is_available() else "cpu"
60
+ tok = AutoTokenizer.from_pretrained(local, trust_remote_code=True)
61
+ cfg = AETHERV27wayConfig.from_pretrained(local)
62
+ cfg.use_cache = False
63
+ # skip pointless random init (overwritten by the checkpoint) → sane cold start
64
+ _sv = (nn.Linear.reset_parameters, nn.Embedding.reset_parameters)
65
+ nn.Linear.reset_parameters = lambda self: None
66
+ nn.Embedding.reset_parameters = lambda self: None
67
+ try:
68
+ torch.set_default_dtype(torch.bfloat16)
69
+ model = _AetherGen(cfg)
70
+ finally:
71
+ torch.set_default_dtype(torch.float32)
72
+ nn.Linear.reset_parameters, nn.Embedding.reset_parameters = _sv
73
+ sd = load_file(os.path.join(local, "model.safetensors"),
74
+ device=(dev if dev == "cuda" else "cpu"))
75
+ model.load_state_dict(sd, strict=False)
76
+ del sd
77
+ if dev == "cuda":
78
+ torch.cuda.empty_cache()
79
+ model = model.to("cuda")
80
+ model.eval()
81
+ model.config.use_cache = False
82
  _state.update(model=model, tok=tok, device=dev, status="ready", error=None)
83
+ except Exception as e: # honest degrade — landing page still works
84
+ _state.update(status="error", error=str(e)[:500])
85
+
86
+
87
+ @app.on_event("startup")
88
+ def _prewarm():
89
+ threading.Thread(target=_load, daemon=True).start()
90
 
91
 
92
  class GenReq(BaseModel):
 
95
  temperature: float | None = 0.7
96
 
97
 
98
+ def _prep(req: "GenReq"):
99
+ tok, model, dev = _state["tok"], _state["model"], _state["device"]
100
+ prompt = (req.prompt or "").strip()[:4000]
101
+ try: # use the instruct chat template when available
102
+ ids = tok.apply_chat_template([{"role": "user", "content": prompt}],
103
+ add_generation_prompt=True, return_tensors="pt").to(dev)
104
+ except Exception:
105
+ ids = tok(prompt, return_tensors="pt").input_ids.to(dev) # batch_size = 1 ONLY
106
+ temp = float(req.temperature if req.temperature is not None else 0.7)
107
+ gen = dict(
108
+ max_new_tokens=min(int(req.max_new_tokens or MAX_NEW), MAX_NEW),
109
+ do_sample=temp > 0, temperature=max(temp, 0.01), top_p=0.9,
110
+ repetition_penalty=1.3, no_repeat_ngram_size=3, use_cache=False,
111
+ pad_token_id=getattr(tok, "eos_token_id", None),
112
+ )
113
+ return tok, model, dev, ids, gen
114
+
115
+
116
  @app.get("/api/health")
117
  def health():
118
  return {"status": _state["status"], "device": _state["device"], "model": MODEL_ID,
119
  "error": _state["error"]}
120
 
121
 
122
+ def _not_ready_message():
123
+ return ("Model is still warming up — try again in a moment."
124
+ if _state["status"] in ("loading", "not_loaded") else NOT_READY + (_state["error"] or ""))
125
+
126
+
127
+ @app.post("/api/stream")
128
+ def stream(req: GenReq):
129
+ if _state["status"] in ("not_loaded", "loading"):
130
+ _load()
131
+ if _state["status"] != "ready":
132
+ return StreamingResponse(iter([_not_ready_message()]),
133
+ media_type="text/plain; charset=utf-8", status_code=503)
134
+ if (req.prompt or "").strip() == "":
135
+ return StreamingResponse(iter(["(empty prompt)"]), media_type="text/plain")
136
+ if not _gen_lock.acquire(blocking=False):
137
+ return StreamingResponse(
138
+ iter(["The demo is busy generating for another visitor — please retry in a moment."]),
139
+ media_type="text/plain; charset=utf-8", status_code=429)
140
+
141
+ from transformers import TextIteratorStreamer
142
+
143
+ def run():
144
+ try:
145
+ tok, model, dev, ids, gen = _prep(req)
146
+ streamer = TextIteratorStreamer(tok, skip_prompt=True, skip_special_tokens=True,
147
+ timeout=300)
148
+ th = threading.Thread(target=_safe_generate,
149
+ args=(model, dict(input_ids=ids, streamer=streamer, **gen)))
150
+ th.start()
151
+ for chunk in streamer:
152
+ yield chunk
153
+ th.join(timeout=5)
154
+ finally:
155
+ _gen_lock.release()
156
+
157
+ return StreamingResponse(run(), media_type="text/plain; charset=utf-8")
158
+
159
+
160
+ def _safe_generate(model, kwargs):
161
+ import torch
162
+ try:
163
+ with torch.no_grad():
164
+ model.generate(**kwargs)
165
+ except Exception:
166
+ pass # the streamer ends; the client sees whatever was produced
167
+
168
+
169
  @app.post("/api/generate")
170
  def generate(req: GenReq):
171
  if _state["status"] in ("not_loaded", "loading"):
172
  _load()
173
  if _state["status"] != "ready":
174
+ return JSONResponse(status_code=503, content={"ok": False, "status": _state["status"],
175
+ "message": _not_ready_message()})
176
+ if (req.prompt or "").strip() == "":
 
 
 
 
 
 
 
 
 
 
177
  return JSONResponse(status_code=400, content={"ok": False, "message": "empty prompt"})
178
+ import torch
179
+ with _gen_lock:
180
+ tok, model, dev, ids, gen = _prep(req)
181
+ t0 = time.time()
182
+ with torch.no_grad():
183
+ out = model.generate(input_ids=ids, **gen)
184
+ text = tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True)
185
+ dt = time.time() - t0
186
+ ntok = int(out.shape[1] - ids.shape[1])
187
+ return {"ok": True, "completion": text, "tokens": ntok, "seconds": round(dt, 2),
188
+ "tok_per_s": round(ntok / dt, 1) if dt else None, "device": dev}
 
 
 
 
 
189
 
190
 
191
  @app.get("/", response_class=HTMLResponse)