Mike0021's picture
Set default generation tokens to 4096
32cf6fd verified
Raw
History Blame Contribute Delete
17.9 kB
import json
import os
import subprocess
import time
import uuid
from pathlib import Path
from threading import Lock, Thread
from typing import Any, Dict, Iterable, List
os.environ.setdefault("HF_HOME", "/tmp/hf_home")
os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
os.environ.setdefault("GRADIO_SSR_MODE", "false")
for _path in (os.environ["HF_HOME"], os.environ["HF_MODULES_CACHE"], os.environ["MPLCONFIGDIR"]):
os.makedirs(_path, exist_ok=True)
import spaces # noqa: E402
import httpx # noqa: E402
import gradio as gr # noqa: E402
from fastapi import Request # noqa: E402
from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse # noqa: E402
from starlette.background import BackgroundTask # noqa: E402
from huggingface_hub import snapshot_download # noqa: E402
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer # noqa: E402
import torch # noqa: E402
MODEL_ID = "WeiboAI/VibeThinker-3B"
MODEL_REVISION = "main"
DEFAULT_MAX_NEW_TOKENS = 4096
MAX_NEW_TOKENS = 4096
ROOT_DIR = Path(__file__).resolve().parent
CHAT_UI_DIR = ROOT_DIR / "chat-ui"
CHAT_UI_PORT = int(os.environ.get("CHAT_UI_PORT", "3000"))
CHAT_UI_URL = f"http://127.0.0.1:{CHAT_UI_PORT}"
CHAT_UI_BUILD = CHAT_UI_DIR / "build" / "index.js"
CHAT_UI_DB = Path(os.environ.get("CHAT_UI_DB", "/tmp/vibethinker-chat-ui-db"))
_model_lock = Lock()
_model = None
_tokenizer = None
_model_device = "cpu"
_chat_ui_lock = Lock()
_chat_ui_process: subprocess.Popen | None = None
def _download_model() -> None:
print(f"Downloading {MODEL_ID} to the local Hub cache...", flush=True)
snapshot_download(
repo_id=MODEL_ID,
revision=MODEL_REVISION,
ignore_patterns=["*.msgpack", "*.h5", "*.ot", "*.onnx"],
)
print("Model files are present in the local Hub cache.", flush=True)
def _load_model_cpu() -> None:
global _model, _tokenizer
if _model is not None and _tokenizer is not None:
return
with _model_lock:
if _model is not None and _tokenizer is not None:
return
_download_model()
print("Loading tokenizer...", flush=True)
_tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
revision=MODEL_REVISION,
trust_remote_code=True,
)
if _tokenizer.pad_token_id is None and _tokenizer.eos_token_id is not None:
_tokenizer.pad_token = _tokenizer.eos_token
print("Loading model on CPU...", flush=True)
_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
revision=MODEL_REVISION,
torch_dtype=torch.bfloat16,
low_cpu_mem_usage=True,
trust_remote_code=True,
attn_implementation="sdpa",
).eval()
print("Model loaded on CPU.", flush=True)
def _ensure_model_on_cuda() -> None:
global _model_device
_load_model_cpu()
if _model_device == "cuda":
return
if not torch.cuda.is_available():
raise RuntimeError("CUDA is not available. Confirm the Space is running on zero-a10g.")
print("Moving model to CUDA...", flush=True)
_model.to("cuda")
_model_device = "cuda"
print("Model is ready on CUDA.", flush=True)
def _clean_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, str]]:
cleaned: List[Dict[str, str]] = []
for item in messages:
if not isinstance(item, dict):
continue
role = str(item.get("role", "")).strip()
content = str(item.get("content", "")).strip()
if role in {"system", "user", "assistant"} and content:
cleaned.append({"role": role, "content": content})
return cleaned
def _messages_from_json(history_json: str) -> List[Dict[str, str]]:
try:
raw = json.loads(history_json)
except json.JSONDecodeError as exc:
raise ValueError("history_json must be valid JSON") from exc
if not isinstance(raw, list):
raise ValueError("history_json must encode a list of chat messages")
messages = _clean_messages(raw)
if not messages or messages[-1]["role"] != "user":
raise ValueError("The final message must be a user message")
return messages
def _coerce_generation_args(
max_new_tokens: int,
temperature: float,
top_p: float,
repetition_penalty: float,
) -> Dict[str, Any]:
max_new_tokens = max(1, min(int(max_new_tokens or DEFAULT_MAX_NEW_TOKENS), MAX_NEW_TOKENS))
temperature = max(0.0, min(float(temperature), 2.0))
top_p = max(0.05, min(float(top_p), 1.0))
repetition_penalty = max(0.8, min(float(repetition_penalty), 1.5))
return {
"max_new_tokens": max_new_tokens,
"do_sample": temperature > 0,
"temperature": max(temperature, 1e-5),
"top_p": top_p,
"repetition_penalty": repetition_penalty,
"pad_token_id": _tokenizer.pad_token_id,
"eos_token_id": _tokenizer.eos_token_id,
}
def _format_prompt(messages: List[Dict[str, str]]) -> str:
return _tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
def _estimate_duration(
history_json: str,
max_new_tokens: int,
temperature: float,
top_p: float,
repetition_penalty: float,
*args: Any,
**kwargs: Any,
) -> int:
del history_json, temperature, top_p, repetition_penalty, args, kwargs
return min(240, max(60, 40 + int(max_new_tokens or DEFAULT_MAX_NEW_TOKENS) // 12))
@spaces.GPU(duration=1)
def _zerogpu_probe() -> str:
return "ready"
def _generate_stream(
messages: List[Dict[str, str]],
max_new_tokens: int,
temperature: float,
top_p: float,
repetition_penalty: float,
) -> Iterable[str]:
_ensure_model_on_cuda()
prompt = _format_prompt(messages)
inputs = _tokenizer([prompt], return_tensors="pt").to(_model.device)
generation_args = _coerce_generation_args(
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
repetition_penalty=repetition_penalty,
)
streamer = TextIteratorStreamer(
_tokenizer,
skip_prompt=True,
skip_special_tokens=True,
timeout=180,
)
generation_kwargs = {
**inputs,
**generation_args,
"streamer": streamer,
}
worker = Thread(target=_model.generate, kwargs=generation_kwargs, daemon=True)
worker.start()
partial = ""
for token in streamer:
partial += token
yield partial
worker.join(timeout=1)
@spaces.GPU(duration=_estimate_duration)
def _gpu_chat_stream(
history_json: str,
max_new_tokens: int,
temperature: float,
top_p: float,
repetition_penalty: float,
) -> Iterable[str]:
messages = _messages_from_json(history_json)
yield from _generate_stream(
messages=messages,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
repetition_penalty=repetition_penalty,
)
app = gr.Server()
def _chat_ui_model_config() -> str:
return json.dumps(
[
{
"id": MODEL_ID,
"name": MODEL_ID,
"displayName": "VibeThinker-3B",
"description": "Reasoning-focused 3B model hosted on ZeroGPU.",
"modelUrl": f"https://huggingface.co/{MODEL_ID}",
"parameters": {
"temperature": 1.0,
"top_p": 0.95,
"max_tokens": DEFAULT_MAX_NEW_TOKENS,
},
"supportsReasoning": True,
}
],
separators=(",", ":"),
)
def _chat_ui_env() -> Dict[str, str]:
env = os.environ.copy()
env.update(
{
"OPENAI_BASE_URL": "http://127.0.0.1:7860/v1",
"OPENAI_API_KEY": "sk-local",
"USE_USER_TOKEN": "false",
"AUTOMATIC_LOGIN": "false",
"ALLOW_IFRAME": "true",
"PUBLIC_APP_NAME": "VibeThinker",
"PUBLIC_APP_ASSETS": "chatui",
"PUBLIC_APP_DESCRIPTION": "VibeThinker-3B hosted on ZeroGPU.",
"PUBLIC_ORIGIN": os.environ.get(
"PUBLIC_ORIGIN",
"https://mike0021-vibethinker-3b-zerogpu.hf.space",
),
"MONGO_STORAGE_PATH": str(CHAT_UI_DB),
"MONGODB_DB_NAME": "chat-ui",
"MONGODB_DIRECT_CONNECTION": "false",
"COOKIE_NAME": "vibethinker-chat-session",
"HUSKY": "0",
"ENABLE_CONFIG_MANAGER": "false",
"LLM_SUMMARIZATION": "false",
"TASK_MODEL": MODEL_ID,
"MODELS": _chat_ui_model_config(),
"PORT": str(CHAT_UI_PORT),
"HOST": "127.0.0.1",
"BODY_SIZE_LIMIT": os.environ.get("BODY_SIZE_LIMIT", "15728640"),
}
)
return env
def _run_chat_ui_command(command: List[str], env: Dict[str, str]) -> None:
print(f"[chat-ui] running: {' '.join(command)}", flush=True)
subprocess.run(command, cwd=CHAT_UI_DIR, env=env, check=True)
def _ensure_chat_ui_build(env: Dict[str, str]) -> None:
if not CHAT_UI_DIR.exists():
raise RuntimeError("chat-ui source directory is missing from the Space.")
node_modules = CHAT_UI_DIR / "node_modules"
if not node_modules.exists():
_run_chat_ui_command(["npm", "ci"], env)
if not CHAT_UI_BUILD.exists():
_run_chat_ui_command(["npm", "run", "build"], env)
def _chat_ui_is_ready() -> bool:
try:
with httpx.Client(timeout=2.0) as client:
response = client.get(f"{CHAT_UI_URL}/healthcheck")
return response.status_code < 500
except Exception:
return False
def _start_chat_ui() -> None:
global _chat_ui_process
with _chat_ui_lock:
if _chat_ui_process is not None and _chat_ui_process.poll() is None:
return
env = _chat_ui_env()
_ensure_chat_ui_build(env)
print(f"[chat-ui] starting on {CHAT_UI_URL}", flush=True)
_chat_ui_process = subprocess.Popen(
[
"node",
"--dns-result-order=ipv4first",
str(CHAT_UI_BUILD),
"--",
"--host",
"127.0.0.1",
"--port",
str(CHAT_UI_PORT),
],
cwd=CHAT_UI_DIR,
env=env,
)
def _start_chat_ui_background() -> None:
try:
time.sleep(3)
_start_chat_ui()
except Exception as exc:
print(f"[chat-ui] failed to start: {exc}", flush=True)
async def _wait_for_chat_ui(timeout: float = 240.0) -> None:
if _chat_ui_process is None or _chat_ui_process.poll() is not None:
Thread(target=_start_chat_ui_background, daemon=True).start()
deadline = time.time() + timeout
while time.time() < deadline:
if _chat_ui_is_ready():
return
await anyio_sleep(1.0)
raise RuntimeError("chat-ui did not become ready before the timeout.")
async def anyio_sleep(seconds: float) -> None:
import anyio
await anyio.sleep(seconds)
@app.on_event("startup")
async def start_chat_ui_on_startup() -> None:
Thread(target=_start_chat_ui_background, daemon=True).start()
@app.api(name="zerogpu_probe", concurrency_limit=1, time_limit=30)
def zerogpu_probe() -> str:
return _zerogpu_probe()
@app.api(name="chat", concurrency_limit=1, time_limit=300, stream_every=0.25)
def chat(
history_json: str,
max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
temperature: float = 1.0,
top_p: float = 0.95,
repetition_penalty: float = 1.0,
) -> Iterable[str]:
yield from _gpu_chat_stream(
history_json,
max_new_tokens,
temperature,
top_p,
repetition_penalty,
)
@app.get("/health")
async def health() -> Dict[str, str]:
return {"status": "ok", "model": MODEL_ID}
@app.get("/v1/models")
async def openai_models() -> Dict[str, Any]:
created = int(time.time())
return {
"object": "list",
"data": [
{
"id": MODEL_ID,
"object": "model",
"created": created,
"owned_by": "WeiboAI",
}
],
}
def _messages_for_openai(payload: Dict[str, Any]) -> List[Dict[str, str]]:
raw_messages = payload.get("messages")
if not isinstance(raw_messages, list):
raise ValueError("messages must be a list")
return _clean_messages(raw_messages)
@app.post("/v1/chat/completions", response_model=None)
async def openai_chat_completions(request: Request):
try:
payload = await request.json()
messages = _messages_for_openai(payload)
if not messages:
raise ValueError("messages cannot be empty")
max_new_tokens = int(payload.get("max_tokens") or payload.get("max_new_tokens") or DEFAULT_MAX_NEW_TOKENS)
temperature = float(payload.get("temperature", 1.0))
top_p = float(payload.get("top_p", 0.95))
repetition_penalty = float(payload.get("repetition_penalty", 1.0))
stream = bool(payload.get("stream", False))
except Exception as exc:
return JSONResponse({"error": {"message": str(exc), "type": "invalid_request_error"}}, status_code=400)
completion_id = f"chatcmpl-{uuid.uuid4().hex}"
created = int(time.time())
if stream:
def events() -> Iterable[bytes]:
last_text = ""
for text in _gpu_chat_stream(
json.dumps(messages),
max_new_tokens,
temperature,
top_p,
repetition_penalty,
):
delta = text[len(last_text) :]
last_text = text
chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": MODEL_ID,
"choices": [{"index": 0, "delta": {"content": delta}, "finish_reason": None}],
}
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode("utf-8")
final = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": MODEL_ID,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
}
yield f"data: {json.dumps(final, ensure_ascii=False)}\n\n".encode("utf-8")
yield b"data: [DONE]\n\n"
return StreamingResponse(events(), media_type="text/event-stream")
final_text = ""
for final_text in _gpu_chat_stream(
json.dumps(messages),
max_new_tokens,
temperature,
top_p,
repetition_penalty,
):
pass
return JSONResponse(
{
"id": completion_id,
"object": "chat.completion",
"created": created,
"model": MODEL_ID,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": final_text},
"finish_reason": "stop",
}
],
}
)
@app.get("/gradio_api/startup-events")
async def gradio_startup_events() -> Dict[str, str]:
return {"status": "ok"}
HOP_BY_HOP_HEADERS = {
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailers",
"transfer-encoding",
"upgrade",
}
async def _close_upstream(response: httpx.Response, client: httpx.AsyncClient) -> None:
await response.aclose()
await client.aclose()
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
async def proxy_chat_ui(path: str, request: Request):
host = request.headers.get("host", "")
if path == "" and host.startswith(("localhost:", "127.0.0.1:", "0.0.0.0:")) and not _chat_ui_is_ready():
return PlainTextResponse("chat-ui is starting", status_code=200)
try:
await _wait_for_chat_ui()
except Exception as exc:
return PlainTextResponse(f"chat-ui is starting or failed to start: {exc}", status_code=503)
target_url = f"{CHAT_UI_URL}/{path}"
if request.url.query:
target_url = f"{target_url}?{request.url.query}"
excluded_request_headers = HOP_BY_HOP_HEADERS | {"host", "content-length"}
headers = {
key: value
for key, value in request.headers.items()
if key.lower() not in excluded_request_headers
}
headers["x-forwarded-host"] = request.headers.get("host", "")
headers["x-forwarded-proto"] = request.url.scheme
client = httpx.AsyncClient(timeout=None, follow_redirects=False)
try:
upstream_request = client.build_request(
request.method,
target_url,
headers=headers,
content=await request.body(),
)
upstream_response = await client.send(upstream_request, stream=True)
except Exception as exc:
await client.aclose()
return PlainTextResponse(f"chat-ui proxy error: {exc}", status_code=502)
excluded_response_headers = HOP_BY_HOP_HEADERS | {"content-length"}
response_headers = {
key: value
for key, value in upstream_response.headers.items()
if key.lower() not in excluded_response_headers
}
return StreamingResponse(
upstream_response.aiter_raw(),
status_code=upstream_response.status_code,
headers=response_headers,
background=BackgroundTask(_close_upstream, upstream_response, client),
)
demo = app
if __name__ == "__main__":
app.launch()