HIMANSHUKUMARJHA's picture
Cleaner UI (strip duplicated speaker-name labels, no third-person self-naming) + faster (shorter one-sentence lines, max_new_tokens 140)
9ee9374 verified
Raw
History Blame Contribute Delete
4.02 kB
"""
The real text-model boundary for Thousand-Token Theater.
Runs openbmb/MiniCPM5-1B on the Space's ZeroGPU (A10G). Exposes:
MODEL_ID, count_tokens(text), generate(messages), generate_stream(messages)
Why MiniCPM5-1B: it is OpenBMB's current-generation *tiny* model (1B params,
llama-architecture). At ~1B it loads fast, leaves the 24GB A10G almost entirely
free for the VoxCPM2 voice model to live alongside it, and is genuinely a "tiny
titan" β€” a small model carrying the whole show.
ZeroGPU pattern: the model is placed on cuda at module level (CUDA emulation at
startup); the GPU is actually attached only inside @spaces.GPU functions, which
may be generators that yield. No mock, no fallback.
"""
from __future__ import annotations
import os
# VoxCPM2 (loaded in voice.py, same process) torch.compiles a submodule that
# crashes TorchDynamo on this stack ("Cannot construct ConstantVariable for
# torch.device"). Disable compilation process-wide so everything runs eager.
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
os.environ.setdefault("TORCH_COMPILE_DISABLE", "1")
import threading
import spaces
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
MODEL_ID = "openbmb/MiniCPM5-1B"
print(f"[theater] loading tokenizer for {MODEL_ID} ...", flush=True)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
print(f"[theater] loading {MODEL_ID} onto GPU ...", flush=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
trust_remote_code=True,
torch_dtype=torch.bfloat16,
low_cpu_mem_usage=True,
).to("cuda")
model.eval()
print("[theater] model ready.", flush=True)
# Official MiniCPM5 "No-Think" sampling (model card): temperature 0.7, top_p 0.95.
# Reasoning is disabled per-call via the chat template (enable_thinking=False) so
# the actors fire off snappy stage lines instead of long deliberations.
GEN = dict(do_sample=True, temperature=0.7, top_p=0.95, repetition_penalty=1.05)
def count_tokens(text: str) -> int:
"""Exact token length under MiniCPM's own tokenizer β€” this defines the cap."""
if not text:
return 0
return len(tokenizer(text, add_special_tokens=False).input_ids)
def _model_inputs(messages):
"""Tokenize chat messages into model inputs.
transformers 5.x `apply_chat_template(return_dict=True, return_tensors="pt")`
returns a dict (input_ids + attention_mask) β€” matching MiniCPM5's official
snippet β€” which is then splatted into `model.generate(**inputs, ...)`.
"""
kw = dict(tokenize=True, add_generation_prompt=True,
return_dict=True, return_tensors="pt")
try:
enc = tokenizer.apply_chat_template(messages, enable_thinking=False, **kw)
except TypeError:
enc = tokenizer.apply_chat_template(messages, **kw)
return enc.to(model.device)
@spaces.GPU(duration=120)
def generate(messages, max_new_tokens: int = 140) -> str:
"""One full chat completion (used by the blocking path / tests)."""
inputs = _model_inputs(messages)
in_len = inputs["input_ids"].shape[-1]
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=max_new_tokens,
pad_token_id=tokenizer.eos_token_id, **GEN)
return tokenizer.decode(out[0][in_len:], skip_special_tokens=True).strip()
@spaces.GPU(duration=120)
def generate_stream(messages, max_new_tokens: int = 140):
"""Generator: yields the cumulative line as MiniCPM writes it (live theatre)."""
inputs = _model_inputs(messages)
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
kwargs = dict(**inputs, streamer=streamer, max_new_tokens=max_new_tokens,
pad_token_id=tokenizer.eos_token_id, **GEN)
def _run():
with torch.no_grad():
model.generate(**kwargs)
threading.Thread(target=_run, daemon=True).start()
acc = ""
for piece in streamer:
acc += piece
yield acc