| """Serve the Rune Goblin vision model (``ASHu2/goblinV1``) on Modal. |
| |
| One-time deploy of a serverless **T4** GPU that runs the merged MiniCPM-V-4.6 |
| fine-tune behind an **OpenAI-compatible** endpoint (vLLM). The game talks to it |
| exactly like the dialogue model: ``POST /v1/chat/completions`` with a system |
| prompt, an ``image_url`` data-URI, and ``response_format=json_object``. |
| |
| Behavior (per docs/MODAL_DEPLOYMENT.md): |
| * GPU: T4 ($0.000164/sec while warm) |
| * scaledown_window = 5 min -> scales to zero after 5 min idle |
| * @modal.concurrent(max_inputs=10) -> <=10 in-flight requests per replica |
| * HF_TOKEN via the existing Modal secret ``huggingface-secret`` (fast pulls) |
| * Endpoint auth via ``RG_VISION_API_KEY`` (Modal secret ``rg-vision``) |
| |
| ---------------------------------------------------------------------------- |
| ONE-TIME SETUP (run once, locally): |
| |
| # 1. Authenticate the Modal CLI (token id/secret are in .env) |
| modal token set --token-id "$MODAL_TOKEN_ID" --token-secret "$MODAL_SECRET" |
| |
| # 2. The HF token secret already exists as `huggingface-secret`. |
| # Create the endpoint API key (any long random string the game will send): |
| modal secret create rg-vision RG_VISION_API_KEY="$(openssl rand -hex 32)" |
| # -> copy that value into the game's RG_VISION_API_KEY (.env / Space secret) |
| |
| # 3. Deploy: |
| modal deploy deploy/modal_vision.py |
| |
| Modal prints a URL like: |
| https://<workspace>--goblin-vision-serve.modal.run |
| The game's RG_VISION_API_URL is that URL + "/v1/chat/completions". |
| |
| Smoke test: |
| curl -s https://<workspace>--goblin-vision-serve.modal.run/v1/models \ |
| -H "Authorization: Bearer $RG_VISION_API_KEY" |
| ---------------------------------------------------------------------------- |
| |
| CLIENT NOTE (Phase 2): the vLLM recipe recommends sending |
| ``"stop_token_ids": [248044, 248046]`` on each request to prevent runaway |
| generations. Add that to RemoteVisionSpellModel's request body alongside |
| ``response_format={"type": "json_object"}``. |
| |
| Requires vLLM >= 0.22.0 (first release with MiniCPMV4_6 support) and |
| transformers >= 5.7.0 — see VLLM_VERSION below. |
| Ref: https://recipes.vllm.ai/openbmb/MiniCPM-V-4.6 |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import subprocess |
|
|
| import modal |
|
|
| |
| MODEL_ID = "ASHu2/goblinV1" |
| |
| MODEL_REVISION = os.environ.get("RG_VISION_MODEL_REVISION", "main") |
|
|
| |
| VLLM_PORT = 8000 |
| MINUTES = 60 |
| N_GPU = 1 |
| GPU_TYPE = "L4" |
| |
| |
| MAX_MODEL_LEN = 8192 |
|
|
| |
| |
| |
| |
| |
| |
| |
| VLLM_VERSION = "vllm==0.22.0" |
| TRANSFORMERS_VERSION = "transformers>=5.7.0" |
| |
|
|
| |
| vllm_image = ( |
| modal.Image.debian_slim(python_version="3.12") |
| .pip_install( |
| VLLM_VERSION, |
| TRANSFORMERS_VERSION, |
| "huggingface_hub[hf_transfer]", |
| ) |
| .env( |
| { |
| "HF_HUB_ENABLE_HF_TRANSFER": "1", |
| |
| |
| |
| |
| |
| "VLLM_USE_FLASHINFER_SAMPLER": "0", |
| "VLLM_ATTENTION_BACKEND": "XFORMERS", |
|
|
| "HF_XET_HIGH_PERFORMANCE": "1", |
| "VLLM_LOG_STATS_INTERVAL": "1", |
| } |
| ) |
| ) |
|
|
| |
| |
| hf_cache = modal.Volume.from_name("goblin-hf-cache", create_if_missing=True) |
| vllm_cache = modal.Volume.from_name("goblin-vllm-cache", create_if_missing=True) |
|
|
| app = modal.App("goblin-vision-gpu") |
| snapshot_key = "v1" |
|
|
|
|
| @app.function( |
| image=vllm_image, |
| gpu=f"{GPU_TYPE}:{N_GPU}", |
| scaledown_window=5 * MINUTES, |
| timeout=10 * MINUTES, |
| volumes={ |
| "/root/.cache/huggingface": hf_cache, |
| "/root/.cache/vllm": vllm_cache, |
| }, |
| secrets=[ |
| |
| modal.Secret.from_name("huggingface-secret"), |
| |
| |
| modal.Secret.from_name("rg-vision"), |
| ], |
| experimental_options={"enable_gpu_snapshot": True} |
| ) |
|
|
| @modal.concurrent(max_inputs=10) |
| @modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES) |
| def serve(): |
| print(f"snapshotting {snapshot_key}") |
| """Launch vLLM's OpenAI-compatible server inside the container.""" |
| cmd = [ |
| "vllm", |
| "serve", |
| MODEL_ID, |
| "--revision", |
| MODEL_REVISION, |
| "--served-model-name", |
| MODEL_ID, |
| "--trust-remote-code", |
| "--dtype", |
| "float16", |
| "--max-model-len", |
| str(MAX_MODEL_LEN), |
| "--gpu-memory-utilization", |
| "0.90", |
| "--limit-mm-per-prompt", |
| '{"image": 1}', |
| "--enforce-eager", |
| "--host", |
| "0.0.0.0", |
| "--port", |
| str(VLLM_PORT), |
| ] |
|
|
| |
| api_key = os.environ.get("RG_VISION_API_KEY") |
| if api_key: |
| cmd += ["--api-key", api_key] |
| else: |
| print("[modal_vision] WARNING: RG_VISION_API_KEY unset — endpoint is OPEN") |
|
|
| print("[modal_vision] launching:", " ".join(cmd)) |
| subprocess.Popen(cmd) |
|
|