| """Stand up a local Ollama server inside a Hugging Face GPU Space — no Docker. |
| |
| Why this exists |
| --------------- |
| A *paid* GPU Space (e.g. Nvidia L4/A10G — **not** ZeroGPU) keeps a GPU attached |
| to the container for its whole lifetime. That means we can run an ordinary |
| `ollama serve` as a background process and talk to it over `localhost:11434`, |
| exactly like on the dev Mac. So none of the agent/config code changes — only |
| this file is Space-specific. |
| |
| Why a runtime bootstrap instead of a Dockerfile: it keeps this a plain Gradio |
| SDK Space. On first boot we download the Ollama runtime and pull the model; |
| everything runs as a background thread so Gradio opens its port immediately and |
| the Space passes its health check while the (large) model is still downloading. |
| |
| Turning it on (Space → Settings → Variables and secrets): |
| MINIFAM_BOOTSTRAP_OLLAMA=1 # the on switch; leave UNSET locally (no-op) |
| OLLAMA_MODELS=/data/ollama # optional: persist the ~18 GB model across |
| MINIFAM_OLLAMA_DIR=/data/runtime # restarts (needs persistent storage at /data) |
| The model tag comes from MINIFAM_MODEL (config.MODEL, default qwen3:30b) and the |
| app already points MINIFAM_LLM_BASE_URL at localhost:11434 — leave both default. |
| """ |
|
|
| import os |
| import subprocess |
| import tarfile |
| import threading |
| import time |
| import urllib.request |
| from pathlib import Path |
|
|
| from . import config |
|
|
| |
| _OLLAMA_TGZ = "https://ollama.com/download/ollama-linux-amd64.tgz" |
| |
| |
| _PREFIX = Path( |
| os.environ.get("MINIFAM_OLLAMA_DIR", str(Path.home() / ".cache" / "minifam" / "ollama")) |
| ) |
| _BIN = _PREFIX / "bin" / "ollama" |
| _HOST = "127.0.0.1:11434" |
|
|
| |
| |
| _status = "off" |
| _status_detail = "" |
| _lock = threading.Lock() |
|
|
|
|
| def enabled() -> bool: |
| """True only when explicitly switched on in the Space (off locally).""" |
| |
| if os.environ.get("MINIFAM_BACKEND", "").strip().lower() == "zerogpu": |
| return False |
| return os.environ.get("MINIFAM_BOOTSTRAP_OLLAMA", "").strip().lower() in { |
| "1", "true", "yes", "on", |
| } |
|
|
|
|
| def _set(status: str, detail: str = "") -> None: |
| global _status, _status_detail |
| _status, _status_detail = status, detail |
|
|
|
|
| def _env() -> dict: |
| """Environment for the ollama subprocesses (inherits Space vars like |
| OLLAMA_MODELS) with our host + the bundled CUDA libs on the search path.""" |
| env = dict(os.environ) |
| env["OLLAMA_HOST"] = _HOST |
| |
| env.setdefault("OLLAMA_KEEP_ALIVE", "-1") |
| lib = str(_PREFIX / "lib" / "ollama") |
| env["LD_LIBRARY_PATH"] = f"{lib}:{env.get('LD_LIBRARY_PATH', '')}".rstrip(":") |
| return env |
|
|
|
|
| def _server_up(timeout: float = 2.0) -> bool: |
| try: |
| with urllib.request.urlopen(f"http://{_HOST}/api/tags", timeout=timeout): |
| return True |
| except Exception: |
| return False |
|
|
|
|
| def _install_binary() -> None: |
| if _BIN.exists(): |
| return |
| _set("installing", "downloading the Ollama runtime (~1.5 GB)") |
| _PREFIX.mkdir(parents=True, exist_ok=True) |
| tgz = _PREFIX / "ollama.tgz" |
| urllib.request.urlretrieve(_OLLAMA_TGZ, tgz) |
| with tarfile.open(tgz) as tar: |
| tar.extractall(_PREFIX, filter="fully_trusted") |
| tgz.unlink(missing_ok=True) |
| _BIN.chmod(0o755) |
|
|
|
|
| def _start_server() -> None: |
| if _server_up(): |
| return |
| _set("starting", "launching ollama serve") |
| subprocess.Popen( |
| [str(_BIN), "serve"], |
| env=_env(), |
| stdout=subprocess.DEVNULL, |
| stderr=subprocess.DEVNULL, |
| ) |
| for _ in range(60): |
| if _server_up(): |
| return |
| time.sleep(1) |
| raise RuntimeError("ollama serve did not come up within 60s") |
|
|
|
|
| def _pull_model() -> None: |
| model = config.MODEL |
| |
| if not _model_present(model): |
| _set("pulling", f"downloading {model} — first boot takes a few minutes") |
| proc = subprocess.run([str(_BIN), "pull", model], env=_env()) |
| if proc.returncode != 0: |
| raise RuntimeError(f"`ollama pull {model}` failed (exit {proc.returncode})") |
| _set("ready", model) |
|
|
|
|
| def _model_present(model: str) -> bool: |
| try: |
| out = subprocess.run( |
| [str(_BIN), "list"], env=_env(), capture_output=True, text=True, timeout=20 |
| ).stdout |
| except Exception: |
| return False |
| |
| base = model.split(":")[0] |
| return any(base in line for line in out.splitlines()[1:]) |
|
|
|
|
| def ensure_ollama() -> None: |
| """Kick off the install → serve → pull sequence in the background. |
| |
| No-op unless MINIFAM_BOOTSTRAP_OLLAMA is set, so importing app.py on the dev |
| machine does nothing and your own Ollama is used as before. Returns at once; |
| progress is reported via status()/warmup_message(). |
| """ |
| if not enabled(): |
| return |
| with _lock: |
| if _status not in {"off", "error"}: |
| return |
| _set("installing") |
|
|
| def _run() -> None: |
| try: |
| _install_binary() |
| _start_server() |
| _pull_model() |
| except Exception as e: |
| _set("error", str(e)) |
|
|
| threading.Thread(target=_run, daemon=True, name="ollama-bootstrap").start() |
|
|
|
|
| def warmup_message() -> str | None: |
| """A friendly 'still warming up' line for the chat box, or None. |
| |
| None means 'not bootstrapping, or already ready' — callers then fall back to |
| their normal error handling. Locally this is always None. |
| """ |
| if not enabled() or _status == "ready": |
| return None |
| if _status == "error": |
| return ( |
| f"The model failed to start in this Space ({_status_detail}). " |
| "Check the Space logs." |
| ) |
| friendly = { |
| "installing": "Setting up the model runtime in the Space…", |
| "starting": "Starting the model server…", |
| "pulling": ( |
| f"Downloading the model ({config.MODEL}, ~18 GB) — the first boot " |
| "takes a few minutes. Try again shortly." |
| ), |
| } |
| return friendly.get(_status, "Warming up — try again in a moment…") |
|
|