Spaces:
Running
Running
| """Modal deployment for Pinch's reasoning model (Mellum 2). | |
| Vision (MiniCPM-V) uses OpenBMB's free API and the image uses HF Inference, so | |
| Modal only serves Mellum 2 β which has no public API and must run on a GPU. | |
| Cost design: | |
| - min_containers=0 β $0 when idle (scales to zero). | |
| - Weights are cached in a Modal Volume, so a cold start mounts them (~30-60s) | |
| instead of re-downloading 24GB from HF (~2-3 min) β ~6x cheaper per cold start. | |
| - scaledown_window=120 β only ~2 min of warm idle after the last request. | |
| Deploy: modal deploy modal_app.py | |
| Endpoint URL is unchanged, so the Space's MODAL_REASON_URL stays valid. | |
| """ | |
| import modal | |
| REASON_MODEL = "JetBrains/Mellum2-12B-A2.5B-Instruct" | |
| CACHE_DIR = "/cache" | |
| # Persists HF weights across cold starts: downloaded once, mounted thereafter. | |
| hf_cache = modal.Volume.from_name("pinch-hf-cache", create_if_missing=True) | |
| image = ( | |
| modal.Image.debian_slim(python_version="3.12") | |
| .uv_pip_install("torch", "transformers", "accelerate", "fastapi[standard]") | |
| .env({"HF_HOME": CACHE_DIR}) # transformers caches into the mounted Volume | |
| ) | |
| app = modal.App("epicurean-simmer") | |
| class Reasoner: | |
| """JetBrains Mellum 2 β the flavour-planning brain (12B MoE, bf16 β 24GB).""" | |
| def load(self): | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| self.tokenizer = AutoTokenizer.from_pretrained(REASON_MODEL) | |
| self.model = AutoModelForCausalLM.from_pretrained( | |
| REASON_MODEL, dtype=torch.bfloat16, device_map="cuda" | |
| ) | |
| hf_cache.commit() # persist freshly-downloaded weights for future cold starts | |
| def generate(self, item: dict): | |
| """POST {"messages": [...], "max_tokens": int} -> {"text": str}.""" | |
| enc = self.tokenizer.apply_chat_template( | |
| item["messages"], add_generation_prompt=True, | |
| return_tensors="pt", return_dict=True, | |
| ).to("cuda") | |
| input_len = enc["input_ids"].shape[-1] | |
| output = self.model.generate( | |
| **enc, max_new_tokens=item.get("max_tokens", 400), | |
| temperature=0.3, do_sample=True, | |
| pad_token_id=self.tokenizer.eos_token_id, | |
| ) | |
| text = self.tokenizer.decode(output[0][input_len:], skip_special_tokens=True) | |
| return {"text": text} | |