Spaces:
Running on Zero
Running on Zero
Cleaner UI (strip duplicated speaker-name labels, no third-person self-naming) + faster (shorter one-sentence lines, max_new_tokens 140)
9ee9374 verified | """ | |
| 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) | |
| 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() | |
| 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 | |