File size: 5,852 Bytes
802006f
 
 
 
 
 
 
d46761f
 
 
 
802006f
 
 
f0e980a
802006f
 
 
 
49129a1
802006f
16b5c29
802006f
 
49129a1
 
f0e980a
802006f
f0e980a
49129a1
802006f
f276b69
802006f
d46761f
f276b69
802006f
d46761f
802006f
16b5c29
802006f
 
f0e980a
d46761f
 
802006f
d46761f
 
802006f
 
d46761f
802006f
 
 
 
 
 
d46761f
802006f
f276b69
49129a1
802006f
f276b69
802006f
 
16b5c29
802006f
f276b69
 
 
802006f
f276b69
d46761f
 
 
f276b69
d46761f
 
 
f276b69
802006f
f0e980a
802006f
d46761f
f276b69
f0e980a
802006f
 
 
 
d46761f
49129a1
802006f
 
 
 
 
16b5c29
802006f
 
 
 
 
 
 
 
 
 
 
 
 
d46761f
802006f
d46761f
 
16b5c29
 
802006f
16b5c29
 
802006f
 
 
 
d46761f
16b5c29
802006f
 
 
 
 
 
16b5c29
802006f
 
d46761f
f276b69
d46761f
f276b69
 
 
 
f0e980a
 
d46761f
802006f
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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"},
    )