Spaces:
Running
Running
| """deploy/modal_app.py — serve Ollama (LLM + embeddings) on a Modal GPU. | |
| Runs ``ollama serve`` on a Modal GPU container and exposes its HTTP API as an | |
| HTTPS endpoint protected by Modal **proxy auth**. The Fugee Gradio app (e.g. on a | |
| free Hugging Face Space) points ``OLLAMA_HOST`` at this endpoint and sends the | |
| ``Modal-Key`` / ``Modal-Secret`` headers (see agent/ollama_auth.py), so the same | |
| Ollama code path and the same models (lfm2.5:8b + nomic-embed-text) work unchanged | |
| — now on a rented GPU instead of a local box. | |
| Usage | |
| ----- | |
| 1. Authenticate the CLI once: modal token set --token-id … --token-secret … | |
| 2. Pull the models into the cache: modal run deploy/modal_app.py::download_models | |
| 3. Deploy the endpoint: modal deploy deploy/modal_app.py | |
| -> prints a URL like https://<workspace>--fugee-ollama-serve.modal.run | |
| 4. Create a Proxy Auth Token in the Modal dashboard (Tokens -> Proxy Auth Tokens) | |
| and give its id/secret to the Space as MODAL_KEY / MODAL_SECRET. | |
| Cost: scales to zero when idle (``scaledown_window``). Keep one warm only during a | |
| live demo by redeploying with MODAL_MIN_CONTAINERS=1. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import subprocess | |
| import time | |
| import urllib.request | |
| import modal | |
| # Models (override via env at deploy time if needed). These must match the app's | |
| # MODEL_ID / EMBED_MODEL. | |
| LLM_MODEL = os.environ.get("MODEL_ID", "lfm2.5:8b") | |
| EMBED_MODEL = os.environ.get("EMBED_MODEL", "nomic-embed-text") | |
| MODELS = [LLM_MODEL, EMBED_MODEL] | |
| GPU = os.environ.get("MODAL_GPU", "L4") # L4 (cheapest) | A10G | A100 | |
| MIN_CONTAINERS = int(os.environ.get("MODAL_MIN_CONTAINERS", "0")) # 1 = keep warm | |
| # Context window. Ollama's default (~4096) truncates our assessment prompt; load | |
| # the model at this size so it (and the app's per-request num_ctx) match -> no | |
| # reload on the first request. Must match the app's NUM_CTX. | |
| NUM_CTX = os.environ.get("NUM_CTX", "16384") | |
| OLLAMA_DIR = "/root/.ollama" # model cache (Volume mount) | |
| PORT = 11434 | |
| app = modal.App("fugee-ollama") | |
| # Ollama installed via its official script; models live in a persistent Volume so | |
| # cold starts don't re-download multi-GB weights. | |
| image = ( | |
| modal.Image.debian_slim() | |
| .apt_install("curl", "zstd") # ollama's installer needs zstd to extract | |
| .run_commands("curl -fsSL https://ollama.com/install.sh | sh") | |
| ) | |
| models_volume = modal.Volume.from_name("fugee-ollama-models", create_if_missing=True) | |
| def _start_ollama(bind: str = "0.0.0.0", keep_alive: str | None = None) -> None: | |
| """Start ``ollama serve`` (bound so Modal can reach it) and wait until ready. | |
| ``keep_alive="-1"`` tells Ollama never to unload the model from GPU while the | |
| container is warm, so a kept-warm endpoint answers in ~1s with no reload.""" | |
| env = {**os.environ, "OLLAMA_HOST": f"{bind}:{PORT}", "OLLAMA_CONTEXT_LENGTH": NUM_CTX} | |
| if keep_alive is not None: | |
| env["OLLAMA_KEEP_ALIVE"] = keep_alive | |
| subprocess.Popen(["ollama", "serve"], env=env) | |
| for _ in range(180): | |
| try: | |
| urllib.request.urlopen(f"http://127.0.0.1:{PORT}/api/version", timeout=2) | |
| return | |
| except Exception: | |
| time.sleep(1) | |
| raise RuntimeError("ollama serve did not become ready in time") | |
| def download_models(): | |
| """One-off: pull the models into the cached Volume (no GPU needed).""" | |
| _start_ollama() | |
| for m in MODELS: | |
| print(f"pulling {m} …", flush=True) | |
| subprocess.run( | |
| ["ollama", "pull", m], | |
| env={**os.environ, "OLLAMA_HOST": f"127.0.0.1:{PORT}"}, | |
| check=True, | |
| ) | |
| models_volume.commit() | |
| print("cached models:", MODELS, flush=True) | |
| def serve(): | |
| """GPU-backed Ollama HTTP endpoint, protected by Modal proxy auth.""" | |
| models_volume.reload() # pick up models pulled by download_models | |
| _start_ollama(keep_alive="-1") # never unload while the container is warm | |
| # Preload the LLM into GPU so the very first user request is instant (no 40s | |
| # load). Best paired with MODAL_MIN_CONTAINERS=1 to keep one container warm. | |
| try: | |
| subprocess.run( | |
| ["ollama", "run", LLM_MODEL, "ok"], | |
| env={**os.environ, "OLLAMA_HOST": f"127.0.0.1:{PORT}"}, | |
| timeout=180, | |
| check=False, | |
| ) | |
| except Exception: | |
| pass | |