File size: 4,955 Bytes
10795db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# syntax=docker/dockerfile:1.7
# ---------------------------------------------------------------------------
# BioPatch backend — Hugging Face Spaces + Fly.io compatible.
#
# IMPORTANT — LLM backend on this image:
#   Default: JARVIS_LLM_BACKEND=local  → llama-cpp-python in-process
#   Default model: BioMistral-7B Q4_K_M (~4.4 GB weights, ~6 GB RAM with
#   8K context). Tuned for the HF Spaces cpu-upgrade tier (8 vCPU /
#   32 GB RAM). On the free tier this OOMs at load time — fall back to
#   a smaller model by overriding JARVIS_LLM_REPO / JARVIS_LLM_FILE /
#   JARVIS_LLM_CHAT_FORMAT in the Space Variables.
#
#   Why local and not HF Inference on a Space:
#   HF Spaces sandbox blocks outbound DNS to api-inference.huggingface.co
#   (anti-loop policy), so the hosted Inference API is unreachable from
#   inside a Space. Local llama-cpp works.
#
#   Set JARVIS_LLM_BACKEND=hf if you deploy this image OUTSIDE a Space
#   (e.g. Fly.io) and prefer the hosted API; you'll also need HF_TOKEN.
#
# Persistent storage:
#   When the Space has persistent storage enabled, /data is mounted as
#   a writable volume. The env vars below default to /data paths so the
#   SQLite DB, wound photos and the GGUF model cache survive restarts.
#   Without persistent storage, override these to /app paths via the
#   Space Variables.
#
# Thread safety: every BLAS / OpenMP / sklearn parallel pool is capped
# at 1 thread + the RF model is forced to n_jobs=1 in code. This
# eliminates the GGML_ASSERT / 'Failed joining thread' cascade that
# crashed the container under simulator load (joblib pool corrupted
# llama's GGML workspace).
# ---------------------------------------------------------------------------

ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim AS base

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_PREFER_BINARY=1

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libopenblas-dev \
    ca-certificates \
    curl \
    && rm -rf /var/lib/apt/lists/*

# HF Spaces runs as UID 1000.
RUN useradd -m -u 1000 user

WORKDIR /app

# Core deps first.
COPY backend/requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt

# LLM stack — pull pre-built CPU wheels from abetlen's dedicated index.
# llama-cpp-python intentionally publishes only source dists to PyPI;
# the binary wheels (built per Python version × platform) live at
# https://abetlen.github.io/llama-cpp-python/whl/cpu. With --only-binary
# pip refuses to compile from source, so if the wheel is genuinely
# missing for our Python it errors out in seconds with a clear message
# instead of OOMing 8 minutes into a C++ compile on the HF free tier.
COPY backend/requirements-llm.txt /app/requirements-llm.txt
RUN pip install --only-binary=:all: \
      --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu \
      -r requirements-llm.txt

COPY backend /app

RUN python -m scripts.seed_clinical_data || true

# Thread caps — keep sklearn / OpenBLAS / OpenMP / MKL single-threaded.
# Combined with the n_jobs=1 forced on the loaded RandomForest model,
# this guarantees the joblib threadpool never spawns and never collides
# with llama-cpp's GGML workspace.
ENV OMP_NUM_THREADS=1 \
    OPENBLAS_NUM_THREADS=1 \
    MKL_NUM_THREADS=1 \
    BLIS_NUM_THREADS=1 \
    NUMEXPR_MAX_THREADS=1 \
    JARVIS_LLM_BACKEND=local \
    JARVIS_LLM_REPO=MaziyarPanahi/BioMistral-7B-GGUF \
    JARVIS_LLM_FILE=BioMistral-7B.Q4_K_M.gguf \
    JARVIS_LLM_CHAT_FORMAT=mistral-instruct \
    JARVIS_LLM_CACHE=/data/models \
    JARVIS_LLM_CTX=4096 \
    JARVIS_LLM_THREADS=8 \
    JARVIS_HF_MODEL=meta-llama/Llama-3.2-3B-Instruct \
    DATABASE_URL=sqlite:////data/biopatch.db \
    WOUND_PHOTO_DIR=/data/wound_photos \
    HF_HOME=/data/.cache/huggingface \
    PORT=7860 \
    ENABLE_SIMULATOR=true \
    SIMULATOR_INTERVAL_SECONDS=15.0 \
    AUTO_SEED=true

# Pre-create fallback dirs under /app for hosts WITHOUT persistent /data
# (Fly.io, local docker run, etc.). On a HF Space with persistent storage
# enabled, /data is mounted at runtime and the env vars above override
# these paths.
RUN mkdir -p /app/ai/artifacts/models /app/data/wound_photos /app/.cache/huggingface \
    && chown -R user:user /app

# Stay as root for the runtime. HF Spaces persistent /data is mounted
# with FUSE-style perms that silently reject chown from inside the
# container, so dropping to uid 1000 leaves the volume unwritable
# (sqlite OperationalError: unable to open database file). Running
# uvicorn as root sidesteps the perm check; the HF sandbox confines
# the container regardless. The threading caps + RF n_jobs=1 already
# neutralise the previous GGML / joblib collision, so non-root is no
# longer required for stability.

EXPOSE 8000 7860

CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000} --workers 1"]