nova-1-chat / app.py
Bc-AI's picture
SPEED FIX: remove int8 quant (slower for bs=1 CPU), fix input_ids= AttributeError, truncate context to last 2 msgs, default max_new_tokens=128
802006f verified
Raw
History Blame Contribute Delete
5.85 kB
import os, json, threading, time
# ─── set thread env BEFORE torch import ───────────────────────────
_nt = str(os.cpu_count() or 4)
os.environ.setdefault("OMP_NUM_THREADS", _nt)
os.environ.setdefault("MKL_NUM_THREADS", _nt)
import torch
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
# ─── CPU thread opts ────────────────────────────────────────────────
torch.set_num_threads(os.cpu_count() or 4)
try:
torch.set_num_interop_threads(max(1, (os.cpu_count() or 4) // 2))
except RuntimeError:
pass # can only set once per process
log = lambda m: print(m, flush=True)
# ─── Globals ─────────────────────────────────────────────────────────
MODEL_ID = "Smilyai-labs/Nova-1-Standard-1.3B-Preview"
model = None
tokenizer = None
model_status = "loading"
model_error = ""
load_stage = "starting"
# ─── Background loader ──────────────────────────────────────────────
def load_model():
global model, tokenizer, model_status, model_error, load_stage
try:
load_stage = "tokenizer"
log("[nova] loading tokenizer …")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
log("[nova] tokenizer ready βœ“")
load_stage = "weights"
log("[nova] loading model weights (float32, no quant) …")
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
trust_remote_code=True,
dtype=torch.float32, # float32 β€” safest/fastest on CPU
low_cpu_mem_usage=True,
)
n_params = sum(p.numel() for p in model.parameters()) / 1e9
log(f"[nova] weights loaded βœ“ ({n_params:.2f}B params)")
# ── NO int8 quant ──
# For batch_size=1 on CPU the per-op quantize/dequantize overhead
# makes dynamic int8 SLOWER than plain float32.
# The bottleneck is compute (use_cache=False) not memory bandwidth,
# so quant doesn't help β€” it just adds latency.
log("[nova] skipping int8 quant (slower for bs=1 CPU inference)")
model.eval()
model_status = "ready"
load_stage = "ready"
log("[nova] βœ… MODEL READY β€” go ahead and chat!")
except Exception as e:
model_status = "error"
model_error = str(e)
load_stage = "error"
log(f"[nova] ❌ FATAL: {e}")
threading.Thread(target=load_model, daemon=True).start()
# ─── FastAPI app ────────────────────────────────────────────────────
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
async def root():
return FileResponse("static/index.html")
@app.get("/api/health")
async def health():
return {"status": model_status, "stage": load_stage, "error": model_error}
# ─── Chat endpoint (SSE streaming) ──────────────────────────────────
@app.post("/api/chat")
async def chat(request: Request):
if model_status != "ready":
return JSONResponse(
{"error": f"Model not ready (stage={load_stage})"},
status_code=503,
)
body = await request.json()
# ── BRUTAL context truncation ──
# Nova-1 has use_cache=False baked in β†’ every token recomputes the
# ENTIRE context. Shorter context = exponentially faster.
# Keep only last 2 user turns.
messages = body.get("messages", [])[-2:]
temperature = float(body.get("temperature", 0.7))
max_new_tokens = int(body.get("max_new_tokens", 128))
top_p = float(body.get("top_p", 0.9))
chat_messages = [{"role": "system",
"content": "You are Nova, a helpful, honest AI assistant. Keep answers concise."}]
chat_messages.extend(messages)
input_ids = tokenizer.apply_chat_template(
chat_messages,
add_generation_prompt=True,
return_tensors="pt",
)
streamer = TextIteratorStreamer(
tokenizer, skip_prompt=True, skip_special_tokens=True,
)
do_sample = temperature > 0
gen_kwargs = dict(
input_ids=input_ids, # ← FIXED: was inputs= (caused AttributeError)
max_new_tokens=max_new_tokens,
streamer=streamer,
use_cache=False, # nova1 architecture β€” no KV cache possible
do_sample=do_sample,
num_beams=1, # greedy/sample only β€” no beam search
pad_token_id=tokenizer.eos_token_id,
)
if do_sample:
gen_kwargs["temperature"] = max(temperature, 0.01)
gen_kwargs["top_p"] = top_p
@torch.inference_mode()
def _generate():
model.generate(**gen_kwargs)
thread = threading.Thread(target=_generate, daemon=True)
thread.start()
def event_stream():
try:
for token in streamer:
if token:
yield f"data: {json.dumps({'token': token})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'error': str(e)})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)