Spaces:
Sleeping
Sleeping
File size: 8,617 Bytes
b56a23b 63b7cac b56a23b 5b817d6 a9f3eee b56a23b 63b7cac a9f3eee 63b7cac b56a23b 63b7cac b56a23b 63b7cac b56a23b 63b7cac b56a23b ea87153 a9f3eee b56a23b 86ce376 b56a23b ea87153 b56a23b 63b7cac b56a23b a9f3eee 63b7cac b56a23b 63b7cac b56a23b 5b817d6 | 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 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | """Chat UI + OpenAI-compatible API for a nanochat d20 model trained from scratch.
Same file runs in two places:
- Hugging Face Space (CPU): Gradio chat at / (API disabled unless API_KEY is set)
- GPU box behind llm-api.skdev.one: Gradio + /v1/models, /v1/chat/completions
Env: MODEL_REPO (weights), API_KEY (enables /v1), APP_PORT, MAX_TOKENS.
"""
import json
import os
import threading
import time
import uuid
import torch
BASE_DIR = os.environ.setdefault("NANOCHAT_BASE_DIR", os.path.abspath("nanochat_data"))
MODEL_REPO = os.environ.get("MODEL_REPO", "sumitkClasses/nanochat-d20-chat")
MODEL_ID = "nanochat-d20"
API_KEY = os.environ.get("API_KEY", "")
DEFAULT_MAX_TOKENS = int(os.environ.get("MAX_TOKENS", "512"))
if not os.path.exists(os.path.join(BASE_DIR, "chatsft_checkpoints")):
from huggingface_hub import snapshot_download
snapshot_download(MODEL_REPO, local_dir=BASE_DIR, allow_patterns=["chatsft_checkpoints/*", "tokenizer/*"])
from nanochat.checkpoint_manager import load_model # noqa: E402 (needs NANOCHAT_BASE_DIR first)
from nanochat.common import autodetect_device_type, compute_init # noqa: E402
from nanochat.engine import Engine # noqa: E402
device_type = os.environ.get("DEVICE") or autodetect_device_type()
if device_type == "cpu":
torch.set_num_threads(int(os.environ.get("THREADS", "0")) or os.cpu_count() or 2)
_, _, _, _, device = compute_init(device_type)
model, tokenizer, _ = load_model("sft", device, phase="eval")
engine = Engine(model, tokenizer)
gen_lock = threading.Lock()
BOS = tokenizer.get_bos_token_id()
U0, U1 = tokenizer.encode_special("<|user_start|>"), tokenizer.encode_special("<|user_end|>")
A0, A1 = tokenizer.encode_special("<|assistant_start|>"), tokenizer.encode_special("<|assistant_end|>")
MAX_CTX = model.config.sequence_len
def render(messages):
"""OpenAI-style messages -> nanochat token ids, ending with <|assistant_start|>.
The model was never trained on a system role, so system text is prepended to the first user turn."""
system = "\n\n".join(m["content"] for m in messages if m["role"] == "system" and m["content"])
toks, first_user = [BOS], True
for m in messages:
text = m["content"] if isinstance(m["content"], str) else " ".join(
p.get("text", "") for p in m["content"] if isinstance(p, dict))
if m["role"] == "user":
if first_user and system:
text = f"{system}\n\n{text}"
first_user = False
toks += [U0, *tokenizer.encode(text), U1]
elif m["role"] == "assistant":
toks += [A0, *tokenizer.encode(text), A1]
toks.append(A0)
return toks
def stream(messages, max_tokens=DEFAULT_MAX_TOKENS, temperature=0.6, top_k=50):
"""Yield text deltas. Decodes the running sequence so multi-byte characters never split."""
toks = render(messages)
max_tokens = max(1, min(int(max_tokens), MAX_CTX - len(toks)))
if len(toks) >= MAX_CTX:
yield "(conversation too long for this model's 2048-token context; start a new chat)"
return
out, sent = [], ""
with gen_lock:
for column, _ in engine.generate(toks, num_samples=1, max_tokens=max_tokens,
temperature=float(temperature), top_k=int(top_k)):
t = column[0]
if t in (A1, BOS):
break
out.append(t)
text = tokenizer.decode(out)
if not text.endswith("�"):
yield text[len(sent):]
sent = text
# ------------------------------------------------------------------ Gradio UI
import gradio as gr # noqa: E402
def chat_fn(message, history, max_tokens, temperature):
msgs = [{"role": h["role"], "content": h["content"]} for h in history
if h.get("role") in ("user", "assistant") and isinstance(h.get("content"), str)]
msgs.append({"role": "user", "content": message})
reply = ""
for d in stream(msgs, max_tokens=max_tokens, temperature=temperature):
reply += d
yield reply
ABOUT = """
**A 20-layer chat model (~390M transformer + ~500M embedding parameters) trained completely from scratch** — tokenizer, pretraining and chat fine-tuning,
no pretrained weights anywhere — using [karpathy/nanochat](https://github.com/karpathy/nanochat) on a single H100:
**~5 hours of pretraining** on 3.5B tokens plus **47 minutes of chat fine-tuning**.
It writes fluent English, holds a conversation, writes poems and explains things simply, but it **confidently makes up
facts** (it is ~200x smaller than GPT-3.5). This free CPU Space is slow: replies stream at a few words per second.
Benchmarks: ARC-Easy 55.2% · MMLU 35.2% · HumanEval 11.0% · CORE 0.236.
Weights: [sumitkClasses/nanochat-d20-chat](https://huggingface.co/sumitkClasses/nanochat-d20-chat) · Code, training logs and scripts: [github.com/rsumit123/private-llm](https://github.com/rsumit123/private-llm)
"""
demo = gr.ChatInterface(
chat_fn,
type="messages",
title="nanochat-d20 · a chat model trained from scratch",
description=ABOUT,
additional_inputs=[
gr.Slider(32, 1024, value=min(DEFAULT_MAX_TOKENS, 1024), step=32, label="Max new tokens"),
gr.Slider(0.1, 1.2, value=0.6, step=0.05, label="Temperature"),
],
examples=[["Write a short poem about the monsoon."],
["Explain photosynthesis to a 10 year old."],
["Give me three tips for a job interview."],
["Why is the sky blue?"]],
cache_examples=False,
)
# ------------------------------------------------------------------ OpenAI-compatible API
from fastapi import FastAPI, Request # noqa: E402
from fastapi.concurrency import run_in_threadpool # noqa: E402
from fastapi.responses import JSONResponse, StreamingResponse # noqa: E402
app = FastAPI()
def _authorized(req: Request):
return API_KEY and req.headers.get("authorization", "") == f"Bearer {API_KEY}"
@app.get("/health")
def health():
return {"ok": True, "model": MODEL_ID, "device": device_type}
@app.get("/v1/models")
def models(req: Request):
if not _authorized(req):
return JSONResponse({"error": {"message": "unauthorized"}}, status_code=401)
return {"object": "list", "data": [{"id": MODEL_ID, "object": "model", "owned_by": "sumitkClasses"}]}
@app.post("/v1/chat/completions")
async def completions(req: Request):
if not _authorized(req):
return JSONResponse({"error": {"message": "unauthorized"}}, status_code=401)
body = await req.json()
kw = dict(max_tokens=body.get("max_tokens") or body.get("max_completion_tokens") or DEFAULT_MAX_TOKENS,
temperature=body.get("temperature", 0.6) or 0.6)
cid, created = f"chatcmpl-{uuid.uuid4().hex[:24]}", int(time.time())
msgs = body.get("messages", [])
if body.get("stream"):
def sse():
yield "data: " + json.dumps({"id": cid, "object": "chat.completion.chunk", "created": created, "model": MODEL_ID,
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}]}) + "\n\n"
for d in stream(msgs, **kw):
yield "data: " + json.dumps({"id": cid, "object": "chat.completion.chunk", "created": created, "model": MODEL_ID,
"choices": [{"index": 0, "delta": {"content": d}, "finish_reason": None}]}) + "\n\n"
yield "data: " + json.dumps({"id": cid, "object": "chat.completion.chunk", "created": created, "model": MODEL_ID,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}) + "\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(sse(), media_type="text/event-stream")
text = await run_in_threadpool(lambda: "".join(stream(msgs, **kw)))
return {"id": cid, "object": "chat.completion", "created": created, "model": MODEL_ID,
"choices": [{"index": 0, "message": {"role": "assistant", "content": text}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": len(render(msgs)), "completion_tokens": len(tokenizer.encode(text)),
"total_tokens": len(render(msgs)) + len(tokenizer.encode(text))}}
app = gr.mount_gradio_app(app, demo, path="/")
if __name__ == "__main__":
import uvicorn
# On HF Spaces, PORT belongs to the platform; Gradio apps must serve GRADIO_SERVER_PORT (7860).
port = os.environ.get("GRADIO_SERVER_PORT") or os.environ.get("APP_PORT") or "7860"
uvicorn.run(app, host="0.0.0.0", port=int(port))
|