Spaces:
Running on Zero
Running on Zero
Codex Codex commited on
Commit ·
f9ab250
1
Parent(s): f6c987a
Select llama.cpp GGUF on CPU Basic Spaces
Browse filesCo-authored-by: Codex <chatgpt-codex-connector[bot]@users.noreply.github.com>
- DEPLOY.md +32 -15
- README.md +1 -1
- RUNBOOK.md +29 -13
- requirements.txt +2 -2
- src/extraction/__init__.py +2 -2
- src/extraction/factory.py +24 -4
- src/extraction/llamacpp_gpu.py +57 -4
- src/space_runtime.py +47 -0
- tests/test_extraction_factory.py +32 -0
- tests/test_llamacpp_gpu.py +25 -0
DEPLOY.md
CHANGED
|
@@ -1,17 +1,20 @@
|
|
| 1 |
-
# Deploying the Space
|
| 2 |
|
| 3 |
-
The active Hugging Face deployment is a **Gradio
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
This workflow is intentionally fixed:
|
| 6 |
|
| 7 |
1. The Space must stay a Gradio Space, not a Docker Space.
|
| 8 |
-
2. Runtime extraction should use
|
| 9 |
-
3.
|
| 10 |
4. Model files must not be committed to the Space git repo.
|
| 11 |
5. When the fine-tuned model is ready, replace only the model variables for the active lanes.
|
| 12 |
-
6. The llama.cpp lane is
|
| 13 |
|
| 14 |
-
Do not change this architecture unless the project intentionally gives up
|
| 15 |
|
| 16 |
## 1. Space Metadata
|
| 17 |
|
|
@@ -33,12 +36,25 @@ pinned: false
|
|
| 33 |
|
| 34 |
ZeroGPU is Gradio-only on Hugging Face. It is not available for Docker Spaces, which is why the previous Docker + `llama-server` deployment was replaced.
|
| 35 |
|
| 36 |
-
## 2. Default Model Serving
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
|
| 40 |
```text
|
| 41 |
-
EXTRACTOR_BACKEND=transformers
|
| 42 |
ZEROGPU_MODEL_ID=build-small-hackathon/blood-test-minicpmv-4_6-medreason
|
| 43 |
```
|
| 44 |
|
|
@@ -61,11 +77,11 @@ transformers.AutoModelForImageTextToText
|
|
| 61 |
|
| 62 |
This is the correct runtime for PDF/image blood-test uploads on ZeroGPU because the GPU is allocated only inside the decorated worker.
|
| 63 |
|
| 64 |
-
Aliases `
|
| 65 |
|
| 66 |
-
## 3.
|
| 67 |
|
| 68 |
-
The app ships a second extraction lane for hackathon badges and GGUF deployment experiments. It is
|
| 69 |
|
| 70 |
### Why it exists
|
| 71 |
|
|
@@ -73,9 +89,9 @@ The app ships a second extraction lane for hackathon badges and GGUF deployment
|
|
| 73 |
- **Fine-tuned GGUF swap** — deploy a quantized model without changing the Gradio app structure.
|
| 74 |
- **Text-only fallback** — lighter lane for plain-text lab exports when vision is not needed.
|
| 75 |
|
| 76 |
-
For normal PDF/image uploads, keep `EXTRACTOR_BACKEND=
|
| 77 |
|
| 78 |
-
###
|
| 79 |
|
| 80 |
Set these variables in the Space settings:
|
| 81 |
|
|
@@ -127,9 +143,10 @@ The Docker path failed on free CPU hardware with `OOMKilled` during build. ZeroG
|
|
| 127 |
This architecture keeps:
|
| 128 |
|
| 129 |
- Free ZeroGPU eligibility.
|
|
|
|
| 130 |
- No external hosted inference API calls.
|
| 131 |
- The fine-tuned Transformers runtime on ZeroGPU for PDF/image lab reports.
|
| 132 |
-
-
|
| 133 |
- A clean model swap by changing `ZEROGPU_MODEL_ID` / `DEFAULT_HF_REPO` and optional `LLAMACPP_*` variables.
|
| 134 |
|
| 135 |
## 6. Local Development
|
|
|
|
| 1 |
+
# Deploying the Space
|
| 2 |
|
| 3 |
+
The active Hugging Face deployment is a **Gradio Space** with hardware-aware extraction:
|
| 4 |
+
|
| 5 |
+
- **CPU Basic:** llama.cpp with the base MiniCPM-V GGUF model.
|
| 6 |
+
- **ZeroGPU / GPU:** Transformers vision with the fine-tuned MiniCPM-V checkpoint.
|
| 7 |
|
| 8 |
This workflow is intentionally fixed:
|
| 9 |
|
| 10 |
1. The Space must stay a Gradio Space, not a Docker Space.
|
| 11 |
+
2. Runtime extraction should use `EXTRACTOR_BACKEND=auto` unless a lane is being forced for testing.
|
| 12 |
+
3. ZeroGPU extraction calls must run behind `@spaces.GPU`; CPU Basic llama.cpp calls must not require ZeroGPU.
|
| 13 |
4. Model files must not be committed to the Space git repo.
|
| 14 |
5. When the fine-tuned model is ready, replace only the model variables for the active lanes.
|
| 15 |
+
6. The llama.cpp lane is automatic on CPU Basic and can still be enabled explicitly with environment variables.
|
| 16 |
|
| 17 |
+
Do not change this architecture unless the project intentionally gives up hardware-aware deployment. To swap models, change `ZEROGPU_MODEL_ID` (or `DEFAULT_HF_REPO` in `src/model_paths.py`) and optional `LLAMACPP_*` for the llama.cpp lane.
|
| 18 |
|
| 19 |
## 1. Space Metadata
|
| 20 |
|
|
|
|
| 36 |
|
| 37 |
ZeroGPU is Gradio-only on Hugging Face. It is not available for Docker Spaces, which is why the previous Docker + `llama-server` deployment was replaced.
|
| 38 |
|
| 39 |
+
## 2. Default Model Serving
|
| 40 |
+
|
| 41 |
+
Leave `EXTRACTOR_BACKEND` unset or set it to:
|
| 42 |
+
|
| 43 |
+
```text
|
| 44 |
+
EXTRACTOR_BACKEND=auto
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
On **CPU Basic**, `auto` detects `cpu-basic` and selects llama.cpp vision with the base GGUF model:
|
| 48 |
+
|
| 49 |
+
```text
|
| 50 |
+
LLAMACPP_GGUF_REPO=openbmb/MiniCPM-V-4.6-gguf
|
| 51 |
+
LLAMACPP_MODEL_FILE=MiniCPM-V-4_6-Q4_K_M.gguf
|
| 52 |
+
LLAMACPP_MMPROJ_FILE=mmproj-model-f16.gguf
|
| 53 |
+
```
|
| 54 |
|
| 55 |
+
On **ZeroGPU/GPU**, `auto` selects the fine-tuned Transformers repo:
|
| 56 |
|
| 57 |
```text
|
|
|
|
| 58 |
ZEROGPU_MODEL_ID=build-small-hackathon/blood-test-minicpmv-4_6-medreason
|
| 59 |
```
|
| 60 |
|
|
|
|
| 77 |
|
| 78 |
This is the correct runtime for PDF/image blood-test uploads on ZeroGPU because the GPU is allocated only inside the decorated worker.
|
| 79 |
|
| 80 |
+
Aliases `zerogpu` and `zero-gpu` force the Transformers path in `src/extraction/factory.py`; `auto` is hardware-aware.
|
| 81 |
|
| 82 |
+
## 3. llama.cpp Lane
|
| 83 |
|
| 84 |
+
The app ships a second extraction lane for CPU Basic, hackathon badges, and GGUF deployment experiments. It is automatic on CPU Basic.
|
| 85 |
|
| 86 |
### Why it exists
|
| 87 |
|
|
|
|
| 89 |
- **Fine-tuned GGUF swap** — deploy a quantized model without changing the Gradio app structure.
|
| 90 |
- **Text-only fallback** — lighter lane for plain-text lab exports when vision is not needed.
|
| 91 |
|
| 92 |
+
For normal CPU Basic PDF/image uploads, keep `EXTRACTOR_BACKEND=auto` and let the code select llama.cpp vision.
|
| 93 |
|
| 94 |
+
### Force vision llama.cpp on the Space
|
| 95 |
|
| 96 |
Set these variables in the Space settings:
|
| 97 |
|
|
|
|
| 143 |
This architecture keeps:
|
| 144 |
|
| 145 |
- Free ZeroGPU eligibility.
|
| 146 |
+
- A CPU Basic fallback that uses llama.cpp + base GGUF instead of Transformers.
|
| 147 |
- No external hosted inference API calls.
|
| 148 |
- The fine-tuned Transformers runtime on ZeroGPU for PDF/image lab reports.
|
| 149 |
+
- A llama.cpp / GGUF lane for CPU Basic, badges, and fine-tuned GGUF deployment.
|
| 150 |
- A clean model swap by changing `ZEROGPU_MODEL_ID` / `DEFAULT_HF_REPO` and optional `LLAMACPP_*` variables.
|
| 151 |
|
| 152 |
## 6. Local Development
|
README.md
CHANGED
|
@@ -42,7 +42,7 @@ The breakthrough was to stop teaching the model our schema and teach it general
|
|
| 42 |
|
| 43 |
## 🦙 Llama Champion
|
| 44 |
|
| 45 |
-
The app ships two interchangeable backends behind one interface.
|
| 46 |
|
| 47 |
## 🎨 Off-Brand
|
| 48 |
|
|
|
|
| 42 |
|
| 43 |
## 🦙 Llama Champion
|
| 44 |
|
| 45 |
+
The app ships two interchangeable backends behind one interface. On CPU Basic Spaces, the default `auto` backend runs the base MiniCPM-V 4.6 GGUF through **llama.cpp** on CPU. On ZeroGPU/GPU, the same `auto` setting uses the fine-tuned Transformers model. We install the prebuilt llama.cpp wheel so the Space builds without a slow source compile, and the operator can still force the GGUF lane explicitly with `EXTRACTOR_BACKEND=llamacpp-gpu` and `LLAMACPP_VISION=1`.
|
| 46 |
|
| 47 |
## 🎨 Off-Brand
|
| 48 |
|
RUNBOOK.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
# Runbook — Extraction Backends + Fine-Tuned Model Swap
|
| 2 |
|
| 3 |
-
The active deployment path is **
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
This replaced the Docker + `llama-server` path because ZeroGPU is only available for Gradio SDK Spaces. The Docker build was also failing on free CPU hardware with `OOMKilled`.
|
| 6 |
|
|
@@ -9,9 +12,9 @@ This replaced the Docker + `llama-server` path because ZeroGPU is only available
|
|
| 9 |
| Area | Current choice |
|
| 10 |
|---|---|
|
| 11 |
| Space SDK | `gradio` |
|
| 12 |
-
| Default extraction |
|
| 13 |
| ZeroGPU worker | `@spaces.GPU` in `src/extraction/zerogpu_transformers.py` |
|
| 14 |
-
|
|
| 15 |
| Transformers variables | `ZEROGPU_MODEL_ID`, `ZEROGPU_MAX_NEW_TOKENS`, `ZEROGPU_DOWNSAMPLE_MODE` |
|
| 16 |
| llama.cpp variables | `LLAMACPP_GGUF_REPO`, `LLAMACPP_MODEL_FILE`, `LLAMACPP_MMPROJ_FILE`, `LLAMACPP_VISION` |
|
| 17 |
| Extraction backends | `src/extraction/factory.py`, `src/extraction/zerogpu_transformers.py`, `src/extraction/llamacpp_gpu.py` |
|
|
@@ -25,8 +28,8 @@ Do not switch the Space back to Docker unless the project intentionally gives up
|
|
| 25 |
|
| 26 |
| Value | Behavior |
|
| 27 |
|---|---|
|
| 28 |
-
| `
|
| 29 |
-
| `
|
| 30 |
| `llamacpp-gpu`, `llama-champion` | GGUF through `llama-cpp-python` |
|
| 31 |
| `local`, `server` | Local `llama-server` HTTP backend |
|
| 32 |
| `llamacpp` | In-process local GGUF + mmproj |
|
|
@@ -35,15 +38,23 @@ Do not switch the Space back to Docker unless the project intentionally gives up
|
|
| 35 |
### Default path (production)
|
| 36 |
|
| 37 |
```bash
|
| 38 |
-
EXTRACTOR_BACKEND
|
| 39 |
-
|
| 40 |
```
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
### Optional llama.cpp path
|
| 45 |
|
| 46 |
-
The llama.cpp lane is
|
| 47 |
|
| 48 |
**Why keep it:**
|
| 49 |
|
|
@@ -99,7 +110,7 @@ transformers[torch]==5.7.0
|
|
| 99 |
llama-cpp-python
|
| 100 |
```
|
| 101 |
|
| 102 |
-
Transformers runs on ZeroGPU through `@spaces.GPU(duration=120)` (or longer for cold starts).
|
| 103 |
|
| 104 |
On Linux x86_64 Spaces, `llama-cpp-python` comes from the prebuilt CPU manylinux wheel:
|
| 105 |
|
|
@@ -111,10 +122,15 @@ This avoids both the CUDA runtime mismatch that was causing the Space to abort o
|
|
| 111 |
|
| 112 |
## Current Model Defaults
|
| 113 |
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
```bash
|
| 117 |
-
EXTRACTOR_BACKEND=transformers
|
| 118 |
ZEROGPU_MODEL_ID=build-small-hackathon/blood-test-minicpmv-4_6-medreason
|
| 119 |
```
|
| 120 |
|
|
@@ -181,4 +197,4 @@ python3 -m py_compile app.py src/*.py src/extraction/*.py
|
|
| 181 |
.venv/bin/python -m pytest tests/test_report_pipeline.py tests/test_llamacpp_gpu.py
|
| 182 |
```
|
| 183 |
|
| 184 |
-
Then verify the Space build uses Gradio, not Docker
|
|
|
|
| 1 |
# Runbook — Extraction Backends + Fine-Tuned Model Swap
|
| 2 |
|
| 3 |
+
The active deployment path is **hardware-aware Gradio**:
|
| 4 |
+
|
| 5 |
+
- **CPU Basic:** llama.cpp with the base MiniCPM-V GGUF model.
|
| 6 |
+
- **ZeroGPU / GPU:** Transformers vision with the fine-tuned MiniCPM-V checkpoint.
|
| 7 |
|
| 8 |
This replaced the Docker + `llama-server` path because ZeroGPU is only available for Gradio SDK Spaces. The Docker build was also failing on free CPU hardware with `OOMKilled`.
|
| 9 |
|
|
|
|
| 12 |
| Area | Current choice |
|
| 13 |
|---|---|
|
| 14 |
| Space SDK | `gradio` |
|
| 15 |
+
| Default extraction | `auto`: CPU Basic uses base GGUF through llama.cpp; ZeroGPU/GPU uses fine-tuned Transformers |
|
| 16 |
| ZeroGPU worker | `@spaces.GPU` in `src/extraction/zerogpu_transformers.py` |
|
| 17 |
+
| llama.cpp lane | Automatic on CPU Basic, or forced with `EXTRACTOR_BACKEND=llamacpp-gpu` (+ `LLAMACPP_VISION=1` for PDF/images) |
|
| 18 |
| Transformers variables | `ZEROGPU_MODEL_ID`, `ZEROGPU_MAX_NEW_TOKENS`, `ZEROGPU_DOWNSAMPLE_MODE` |
|
| 19 |
| llama.cpp variables | `LLAMACPP_GGUF_REPO`, `LLAMACPP_MODEL_FILE`, `LLAMACPP_MMPROJ_FILE`, `LLAMACPP_VISION` |
|
| 20 |
| Extraction backends | `src/extraction/factory.py`, `src/extraction/zerogpu_transformers.py`, `src/extraction/llamacpp_gpu.py` |
|
|
|
|
| 28 |
|
| 29 |
| Value | Behavior |
|
| 30 |
|---|---|
|
| 31 |
+
| unset / `auto` (default) | Hardware-aware: CPU Basic -> llama.cpp base GGUF; otherwise Transformers |
|
| 32 |
+
| `transformers`, `zerogpu`, `zero-gpu` | Force fine-tuned MiniCPM-V through Transformers vision |
|
| 33 |
| `llamacpp-gpu`, `llama-champion` | GGUF through `llama-cpp-python` |
|
| 34 |
| `local`, `server` | Local `llama-server` HTTP backend |
|
| 35 |
| `llamacpp` | In-process local GGUF + mmproj |
|
|
|
|
| 38 |
### Default path (production)
|
| 39 |
|
| 40 |
```bash
|
| 41 |
+
# Usually leave EXTRACTOR_BACKEND unset, or set:
|
| 42 |
+
EXTRACTOR_BACKEND=auto
|
| 43 |
```
|
| 44 |
|
| 45 |
+
On the current CPU Basic Space, `auto` selects llama.cpp vision with the base GGUF defaults:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
LLAMACPP_GGUF_REPO=openbmb/MiniCPM-V-4.6-gguf
|
| 49 |
+
LLAMACPP_MODEL_FILE=MiniCPM-V-4_6-Q4_K_M.gguf
|
| 50 |
+
LLAMACPP_MMPROJ_FILE=mmproj-model-f16.gguf
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
When the Space is moved to ZeroGPU/GPU, `auto` selects Transformers and uses `ZEROGPU_MODEL_ID` or the default fine-tuned repo in `src/model_paths.py`.
|
| 54 |
|
| 55 |
### Optional llama.cpp path
|
| 56 |
|
| 57 |
+
The llama.cpp lane is selected automatically on CPU Basic. It can also be forced explicitly.
|
| 58 |
|
| 59 |
**Why keep it:**
|
| 60 |
|
|
|
|
| 110 |
llama-cpp-python
|
| 111 |
```
|
| 112 |
|
| 113 |
+
Transformers runs on ZeroGPU through `@spaces.GPU(duration=120)` (or longer for cold starts). llama.cpp bypasses `@spaces.GPU` on CPU Basic and runs as CPU inference; when forced on GPU/ZeroGPU it uses `@spaces.GPU(duration=600)`.
|
| 114 |
|
| 115 |
On Linux x86_64 Spaces, `llama-cpp-python` comes from the prebuilt CPU manylinux wheel:
|
| 116 |
|
|
|
|
| 122 |
|
| 123 |
## Current Model Defaults
|
| 124 |
|
| 125 |
+
Auto lane:
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
EXTRACTOR_BACKEND=auto
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
Transformers lane:
|
| 132 |
|
| 133 |
```bash
|
|
|
|
| 134 |
ZEROGPU_MODEL_ID=build-small-hackathon/blood-test-minicpmv-4_6-medreason
|
| 135 |
```
|
| 136 |
|
|
|
|
| 197 |
.venv/bin/python -m pytest tests/test_report_pipeline.py tests/test_llamacpp_gpu.py
|
| 198 |
```
|
| 199 |
|
| 200 |
+
Then verify the Space build uses Gradio, not Docker. On CPU Basic, the default backend should report `llamacpp-cpu-vision`; on ZeroGPU/GPU it should report the Transformers backend.
|
requirements.txt
CHANGED
|
@@ -4,13 +4,13 @@ requests==2.32.5
|
|
| 4 |
pillow==12.0.0
|
| 5 |
pymupdf==1.26.6
|
| 6 |
json-repair==0.60.1
|
| 7 |
-
#
|
| 8 |
torch==2.9.1 ; sys_platform == "darwin" or (sys_platform == "linux" and platform_machine == "x86_64")
|
| 9 |
transformers[torch]==5.7.0
|
| 10 |
accelerate==1.12.0
|
| 11 |
bitsandbytes==0.48.2 ; sys_platform == "linux" and platform_machine == "x86_64"
|
| 12 |
torchvision==0.24.1 ; sys_platform == "darwin" or (sys_platform == "linux" and platform_machine == "x86_64")
|
| 13 |
av==16.0.1 ; sys_platform == "linux" and platform_machine == "x86_64"
|
| 14 |
-
# CPU
|
| 15 |
llama-cpp-python @ https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.28/llama_cpp_python-0.3.28-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl ; sys_platform == "linux" and platform_machine == "x86_64"
|
| 16 |
llama-cpp-python==0.3.28 ; sys_platform != "linux"
|
|
|
|
| 4 |
pillow==12.0.0
|
| 5 |
pymupdf==1.26.6
|
| 6 |
json-repair==0.60.1
|
| 7 |
+
# Transformers vision path (Mac + Linux GPU, and non-CPU-Basic Space auto mode).
|
| 8 |
torch==2.9.1 ; sys_platform == "darwin" or (sys_platform == "linux" and platform_machine == "x86_64")
|
| 9 |
transformers[torch]==5.7.0
|
| 10 |
accelerate==1.12.0
|
| 11 |
bitsandbytes==0.48.2 ; sys_platform == "linux" and platform_machine == "x86_64"
|
| 12 |
torchvision==0.24.1 ; sys_platform == "darwin" or (sys_platform == "linux" and platform_machine == "x86_64")
|
| 13 |
av==16.0.1 ; sys_platform == "linux" and platform_machine == "x86_64"
|
| 14 |
+
# CPU Basic / llama.cpp path: install the prebuilt manylinux wheel directly to avoid a source build on Spaces.
|
| 15 |
llama-cpp-python @ https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.28/llama_cpp_python-0.3.28-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl ; sys_platform == "linux" and platform_machine == "x86_64"
|
| 16 |
llama-cpp-python==0.3.28 ; sys_platform != "linux"
|
src/extraction/__init__.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
"""Extraction backends behind one interface.
|
| 2 |
|
| 3 |
`build_extractor()` returns the right backend for the environment:
|
| 4 |
-
- **
|
| 5 |
-
- **
|
| 6 |
- **llamacpp-gpu** / **llama-champion**: force GGUF through llama.cpp. Set `LLAMACPP_VISION=1`
|
| 7 |
to run the same PDF/image vision pipeline as Transformers (requires mmproj).
|
| 8 |
- **local**: local llama-server / llama.cpp backends for local experimentation.
|
|
|
|
| 1 |
"""Extraction backends behind one interface.
|
| 2 |
|
| 3 |
`build_extractor()` returns the right backend for the environment:
|
| 4 |
+
- unset / **auto** (default): CPU Basic Spaces use llama.cpp + base GGUF; other runtimes use Transformers.
|
| 5 |
+
- **transformers**: force OpenBMB MiniCPM-V through Transformers.
|
| 6 |
- **llamacpp-gpu** / **llama-champion**: force GGUF through llama.cpp. Set `LLAMACPP_VISION=1`
|
| 7 |
to run the same PDF/image vision pipeline as Transformers (requires mmproj).
|
| 8 |
- **local**: local llama-server / llama.cpp backends for local experimentation.
|
src/extraction/factory.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
"""Backend selection.
|
| 2 |
|
| 3 |
`EXTRACTOR_BACKEND` env:
|
| 4 |
-
- `
|
| 5 |
-
|
|
|
|
| 6 |
- `zerogpu` / `zero-gpu`: alias for `transformers`.
|
| 7 |
- `llamacpp-gpu` / `llama-champion`: llama.cpp GGUF badge path. Add `LLAMACPP_VISION=1`
|
| 8 |
for PDF/image vision via mmproj.
|
|
@@ -21,8 +22,9 @@ from src.extraction.auto import AutoExtractor
|
|
| 21 |
from src.extraction.llamacpp_gpu import LlamaCppGPUExtractor
|
| 22 |
from src.extraction.local_minicpmv import LocalMiniCPMVExtractor
|
| 23 |
from src.extraction.local_server import LocalServerExtractor
|
|
|
|
| 24 |
|
| 25 |
-
_DEFAULT_BACKEND = "
|
| 26 |
_DISABLED_BACKENDS = {"api", "openbmb", "hosted"}
|
| 27 |
|
| 28 |
|
|
@@ -35,7 +37,25 @@ def build_extractor(model: str | None = None) -> Extractor:
|
|
| 35 |
"Use EXTRACTOR_BACKEND=transformers for local MiniCPM-V extraction."
|
| 36 |
)
|
| 37 |
|
| 38 |
-
if backend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
return AutoExtractor(model_id=model)
|
| 40 |
if backend in ("llamacpp-gpu", "gpu-llamacpp", "llama-champion"):
|
| 41 |
from src.extraction.llamacpp_vision import llamacpp_vision_enabled
|
|
|
|
| 1 |
"""Backend selection.
|
| 2 |
|
| 3 |
`EXTRACTOR_BACKEND` env:
|
| 4 |
+
- unset / `auto` (default): hardware-aware Space selection.
|
| 5 |
+
CPU Basic Spaces use llama.cpp + base MiniCPM-V GGUF; other runtimes use Transformers.
|
| 6 |
+
- `transformers`: local/OpenBMB MiniCPM-V through Transformers.
|
| 7 |
- `zerogpu` / `zero-gpu`: alias for `transformers`.
|
| 8 |
- `llamacpp-gpu` / `llama-champion`: llama.cpp GGUF badge path. Add `LLAMACPP_VISION=1`
|
| 9 |
for PDF/image vision via mmproj.
|
|
|
|
| 22 |
from src.extraction.llamacpp_gpu import LlamaCppGPUExtractor
|
| 23 |
from src.extraction.local_minicpmv import LocalMiniCPMVExtractor
|
| 24 |
from src.extraction.local_server import LocalServerExtractor
|
| 25 |
+
from src.space_runtime import configured_space_hardware, is_cpu_basic_space, is_huggingface_space
|
| 26 |
|
| 27 |
+
_DEFAULT_BACKEND = "auto"
|
| 28 |
_DISABLED_BACKENDS = {"api", "openbmb", "hosted"}
|
| 29 |
|
| 30 |
|
|
|
|
| 37 |
"Use EXTRACTOR_BACKEND=transformers for local MiniCPM-V extraction."
|
| 38 |
)
|
| 39 |
|
| 40 |
+
if backend == "auto":
|
| 41 |
+
if is_cpu_basic_space():
|
| 42 |
+
hardware = configured_space_hardware()
|
| 43 |
+
if hardware:
|
| 44 |
+
os.environ.setdefault("BTE_SPACE_HARDWARE", hardware)
|
| 45 |
+
os.environ.setdefault("LLAMACPP_VISION", "1")
|
| 46 |
+
print(
|
| 47 |
+
"[Blood Test Explainer] CPU Basic Space detected; using llama.cpp "
|
| 48 |
+
"(base MiniCPM-V GGUF)",
|
| 49 |
+
flush=True,
|
| 50 |
+
)
|
| 51 |
+
return LlamaCppGPUExtractor()
|
| 52 |
+
if is_huggingface_space():
|
| 53 |
+
print(
|
| 54 |
+
"[Blood Test Explainer] non-CPU-Basic Space detected; using Transformers extractor",
|
| 55 |
+
flush=True,
|
| 56 |
+
)
|
| 57 |
+
return AutoExtractor(model_id=model)
|
| 58 |
+
if backend in ("zerogpu", "zero-gpu", "transformers"):
|
| 59 |
return AutoExtractor(model_id=model)
|
| 60 |
if backend in ("llamacpp-gpu", "gpu-llamacpp", "llama-champion"):
|
| 61 |
from src.extraction.llamacpp_vision import llamacpp_vision_enabled
|
src/extraction/llamacpp_gpu.py
CHANGED
|
@@ -39,6 +39,7 @@ from src.openbmb_client import (
|
|
| 39 |
_parse_json_response,
|
| 40 |
summarize_document_parts,
|
| 41 |
)
|
|
|
|
| 42 |
|
| 43 |
DEFAULT_GGUF_REPO = "openbmb/MiniCPM-V-4.6-gguf"
|
| 44 |
DEFAULT_MODEL_FILE = "MiniCPM-V-4_6-Q4_K_M.gguf"
|
|
@@ -69,6 +70,7 @@ class LlamaCppGPUExtractor:
|
|
| 69 |
self.n_ctx = int(os.getenv("LLAMACPP_N_CTX", "8192"))
|
| 70 |
self.n_gpu_layers = int(os.getenv("LLAMACPP_N_GPU_LAYERS", "0"))
|
| 71 |
self.vision_enabled = llamacpp_vision_enabled()
|
|
|
|
| 72 |
if self.vision_enabled and not self.mmproj_file:
|
| 73 |
raise ValueError("LLAMACPP_VISION=1 requires LLAMACPP_MMPROJ_FILE.")
|
| 74 |
|
|
@@ -76,7 +78,12 @@ class LlamaCppGPUExtractor:
|
|
| 76 |
parts = document_to_payload_parts(file_path, max_pages=max_pages)
|
| 77 |
started = time.perf_counter()
|
| 78 |
if self.vision_enabled:
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
parts=parts,
|
| 81 |
repo=self.repo,
|
| 82 |
model_file=self.model_file,
|
|
@@ -86,11 +93,16 @@ class LlamaCppGPUExtractor:
|
|
| 86 |
n_ctx=self.n_ctx,
|
| 87 |
n_gpu_layers=self.n_gpu_layers,
|
| 88 |
)
|
| 89 |
-
backend = "llamacpp-gpu-vision"
|
| 90 |
composed_prompt = None
|
| 91 |
else:
|
| 92 |
prompt_text = _compose_prompt(parts)
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
prompt_text=prompt_text,
|
| 95 |
repo=self.repo,
|
| 96 |
model_file=self.model_file,
|
|
@@ -98,7 +110,7 @@ class LlamaCppGPUExtractor:
|
|
| 98 |
n_ctx=self.n_ctx,
|
| 99 |
n_gpu_layers=self.n_gpu_layers,
|
| 100 |
)
|
| 101 |
-
backend = "llamacpp-gpu"
|
| 102 |
composed_prompt = prompt_text
|
| 103 |
|
| 104 |
duration_ms = int((time.perf_counter() - started) * 1000)
|
|
@@ -108,6 +120,7 @@ class LlamaCppGPUExtractor:
|
|
| 108 |
"repo": self.repo,
|
| 109 |
"model": self.model_file,
|
| 110 |
"vision_enabled": self.vision_enabled,
|
|
|
|
| 111 |
"document_parts": len(parts),
|
| 112 |
"max_pages": max_pages,
|
| 113 |
"extraction_prompt": EXTRACTION_PROMPT,
|
|
@@ -177,6 +190,28 @@ def _run_llamacpp_vision_generation(
|
|
| 177 |
max_tokens: int,
|
| 178 |
n_ctx: int,
|
| 179 |
n_gpu_layers: int,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
) -> str:
|
| 181 |
try:
|
| 182 |
model_path = download_hf_file(repo, model_file)
|
|
@@ -217,6 +252,24 @@ def _run_llamacpp_generation(
|
|
| 217 |
max_tokens: int,
|
| 218 |
n_ctx: int,
|
| 219 |
n_gpu_layers: int,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
) -> str:
|
| 221 |
try:
|
| 222 |
model_path = download_hf_file(repo, model_file)
|
|
|
|
| 39 |
_parse_json_response,
|
| 40 |
summarize_document_parts,
|
| 41 |
)
|
| 42 |
+
from src.space_runtime import is_cpu_basic_space
|
| 43 |
|
| 44 |
DEFAULT_GGUF_REPO = "openbmb/MiniCPM-V-4.6-gguf"
|
| 45 |
DEFAULT_MODEL_FILE = "MiniCPM-V-4_6-Q4_K_M.gguf"
|
|
|
|
| 70 |
self.n_ctx = int(os.getenv("LLAMACPP_N_CTX", "8192"))
|
| 71 |
self.n_gpu_layers = int(os.getenv("LLAMACPP_N_GPU_LAYERS", "0"))
|
| 72 |
self.vision_enabled = llamacpp_vision_enabled()
|
| 73 |
+
self.use_spaces_gpu = not is_cpu_basic_space()
|
| 74 |
if self.vision_enabled and not self.mmproj_file:
|
| 75 |
raise ValueError("LLAMACPP_VISION=1 requires LLAMACPP_MMPROJ_FILE.")
|
| 76 |
|
|
|
|
| 78 |
parts = document_to_payload_parts(file_path, max_pages=max_pages)
|
| 79 |
started = time.perf_counter()
|
| 80 |
if self.vision_enabled:
|
| 81 |
+
runner = (
|
| 82 |
+
_run_llamacpp_vision_generation
|
| 83 |
+
if self.use_spaces_gpu
|
| 84 |
+
else _run_llamacpp_vision_generation_cpu
|
| 85 |
+
)
|
| 86 |
+
raw = runner(
|
| 87 |
parts=parts,
|
| 88 |
repo=self.repo,
|
| 89 |
model_file=self.model_file,
|
|
|
|
| 93 |
n_ctx=self.n_ctx,
|
| 94 |
n_gpu_layers=self.n_gpu_layers,
|
| 95 |
)
|
| 96 |
+
backend = "llamacpp-gpu-vision" if self.use_spaces_gpu else "llamacpp-cpu-vision"
|
| 97 |
composed_prompt = None
|
| 98 |
else:
|
| 99 |
prompt_text = _compose_prompt(parts)
|
| 100 |
+
runner = (
|
| 101 |
+
_run_llamacpp_generation
|
| 102 |
+
if self.use_spaces_gpu
|
| 103 |
+
else _run_llamacpp_generation_cpu
|
| 104 |
+
)
|
| 105 |
+
raw = runner(
|
| 106 |
prompt_text=prompt_text,
|
| 107 |
repo=self.repo,
|
| 108 |
model_file=self.model_file,
|
|
|
|
| 110 |
n_ctx=self.n_ctx,
|
| 111 |
n_gpu_layers=self.n_gpu_layers,
|
| 112 |
)
|
| 113 |
+
backend = "llamacpp-gpu" if self.use_spaces_gpu else "llamacpp-cpu"
|
| 114 |
composed_prompt = prompt_text
|
| 115 |
|
| 116 |
duration_ms = int((time.perf_counter() - started) * 1000)
|
|
|
|
| 120 |
"repo": self.repo,
|
| 121 |
"model": self.model_file,
|
| 122 |
"vision_enabled": self.vision_enabled,
|
| 123 |
+
"spaces_gpu": self.use_spaces_gpu,
|
| 124 |
"document_parts": len(parts),
|
| 125 |
"max_pages": max_pages,
|
| 126 |
"extraction_prompt": EXTRACTION_PROMPT,
|
|
|
|
| 190 |
max_tokens: int,
|
| 191 |
n_ctx: int,
|
| 192 |
n_gpu_layers: int,
|
| 193 |
+
) -> str:
|
| 194 |
+
return _run_llamacpp_vision_generation_cpu(
|
| 195 |
+
parts=parts,
|
| 196 |
+
repo=repo,
|
| 197 |
+
model_file=model_file,
|
| 198 |
+
mmproj_file=mmproj_file,
|
| 199 |
+
chat_handler=chat_handler,
|
| 200 |
+
max_tokens=max_tokens,
|
| 201 |
+
n_ctx=n_ctx,
|
| 202 |
+
n_gpu_layers=n_gpu_layers,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _run_llamacpp_vision_generation_cpu(
|
| 207 |
+
parts: list[dict[str, Any]],
|
| 208 |
+
repo: str,
|
| 209 |
+
model_file: str,
|
| 210 |
+
mmproj_file: str,
|
| 211 |
+
chat_handler: str,
|
| 212 |
+
max_tokens: int,
|
| 213 |
+
n_ctx: int,
|
| 214 |
+
n_gpu_layers: int,
|
| 215 |
) -> str:
|
| 216 |
try:
|
| 217 |
model_path = download_hf_file(repo, model_file)
|
|
|
|
| 252 |
max_tokens: int,
|
| 253 |
n_ctx: int,
|
| 254 |
n_gpu_layers: int,
|
| 255 |
+
) -> str:
|
| 256 |
+
return _run_llamacpp_generation_cpu(
|
| 257 |
+
prompt_text=prompt_text,
|
| 258 |
+
repo=repo,
|
| 259 |
+
model_file=model_file,
|
| 260 |
+
max_tokens=max_tokens,
|
| 261 |
+
n_ctx=n_ctx,
|
| 262 |
+
n_gpu_layers=n_gpu_layers,
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _run_llamacpp_generation_cpu(
|
| 267 |
+
prompt_text: str,
|
| 268 |
+
repo: str,
|
| 269 |
+
model_file: str,
|
| 270 |
+
max_tokens: int,
|
| 271 |
+
n_ctx: int,
|
| 272 |
+
n_gpu_layers: int,
|
| 273 |
) -> str:
|
| 274 |
try:
|
| 275 |
model_path = download_hf_file(repo, model_file)
|
src/space_runtime.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helpers for hardware-aware Hugging Face Space behavior."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from functools import lru_cache
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
_HARDWARE_ENV_KEYS = (
|
| 10 |
+
"BTE_SPACE_HARDWARE",
|
| 11 |
+
"SPACE_HARDWARE",
|
| 12 |
+
"HF_SPACE_HARDWARE",
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def is_huggingface_space() -> bool:
|
| 17 |
+
return bool(os.getenv("SPACE_ID") or os.getenv("SPACE_HOST"))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def configured_space_hardware() -> str | None:
|
| 21 |
+
for key in _HARDWARE_ENV_KEYS:
|
| 22 |
+
value = os.getenv(key, "").strip().lower()
|
| 23 |
+
if value:
|
| 24 |
+
return value
|
| 25 |
+
return _hub_space_hardware()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def is_cpu_basic_space() -> bool:
|
| 29 |
+
return configured_space_hardware() == "cpu-basic"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@lru_cache(maxsize=8)
|
| 33 |
+
def _hub_space_hardware() -> str | None:
|
| 34 |
+
repo_id = os.getenv("SPACE_ID", "").strip()
|
| 35 |
+
if not repo_id:
|
| 36 |
+
return None
|
| 37 |
+
try:
|
| 38 |
+
from huggingface_hub import HfApi
|
| 39 |
+
|
| 40 |
+
info = HfApi().space_info(repo_id=repo_id)
|
| 41 |
+
runtime = getattr(info, "runtime", None)
|
| 42 |
+
hardware = getattr(runtime, "hardware", None)
|
| 43 |
+
if hardware:
|
| 44 |
+
return str(hardware).strip().lower()
|
| 45 |
+
except Exception:
|
| 46 |
+
return None
|
| 47 |
+
return None
|
tests/test_extraction_factory.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from unittest.mock import patch
|
| 5 |
+
|
| 6 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
+
|
| 8 |
+
from src.extraction.factory import build_extractor
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_auto_uses_llamacpp_on_cpu_basic_space(monkeypatch):
|
| 12 |
+
monkeypatch.delenv("EXTRACTOR_BACKEND", raising=False)
|
| 13 |
+
monkeypatch.delenv("LLAMACPP_VISION", raising=False)
|
| 14 |
+
monkeypatch.setenv("BTE_SPACE_HARDWARE", "cpu-basic")
|
| 15 |
+
|
| 16 |
+
with patch("src.extraction.factory.LlamaCppGPUExtractor") as llama_cls:
|
| 17 |
+
extractor = build_extractor()
|
| 18 |
+
|
| 19 |
+
llama_cls.assert_called_once_with()
|
| 20 |
+
assert extractor is llama_cls.return_value
|
| 21 |
+
assert os.environ["LLAMACPP_VISION"] == "1"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_auto_uses_transformers_when_hardware_is_not_cpu_basic(monkeypatch):
|
| 25 |
+
monkeypatch.delenv("EXTRACTOR_BACKEND", raising=False)
|
| 26 |
+
monkeypatch.setenv("BTE_SPACE_HARDWARE", "zero-a10g")
|
| 27 |
+
|
| 28 |
+
with patch("src.extraction.factory.AutoExtractor") as auto_cls:
|
| 29 |
+
extractor = build_extractor(model="demo/model")
|
| 30 |
+
|
| 31 |
+
auto_cls.assert_called_once_with(model_id="demo/model")
|
| 32 |
+
assert extractor is auto_cls.return_value
|
tests/test_llamacpp_gpu.py
CHANGED
|
@@ -37,6 +37,7 @@ def test_compose_prompt_keeps_text_files():
|
|
| 37 |
|
| 38 |
def test_extract_uses_vision_generation_when_enabled(monkeypatch):
|
| 39 |
monkeypatch.setenv("LLAMACPP_VISION", "1")
|
|
|
|
| 40 |
image_parts = [{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}]
|
| 41 |
|
| 42 |
with patch(
|
|
@@ -55,10 +56,34 @@ def test_extract_uses_vision_generation_when_enabled(monkeypatch):
|
|
| 55 |
assert result.request_summary["backend"] == "llamacpp-gpu-vision"
|
| 56 |
assert result.request_summary["vision_enabled"] is True
|
| 57 |
assert result.request_summary["mmproj"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
def test_extract_uses_text_generation_when_vision_disabled(monkeypatch):
|
| 61 |
monkeypatch.delenv("LLAMACPP_VISION", raising=False)
|
|
|
|
| 62 |
text_parts = [{"type": "text", "text": "WBC 6.5"}]
|
| 63 |
|
| 64 |
with patch(
|
|
|
|
| 37 |
|
| 38 |
def test_extract_uses_vision_generation_when_enabled(monkeypatch):
|
| 39 |
monkeypatch.setenv("LLAMACPP_VISION", "1")
|
| 40 |
+
monkeypatch.delenv("BTE_SPACE_HARDWARE", raising=False)
|
| 41 |
image_parts = [{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}]
|
| 42 |
|
| 43 |
with patch(
|
|
|
|
| 56 |
assert result.request_summary["backend"] == "llamacpp-gpu-vision"
|
| 57 |
assert result.request_summary["vision_enabled"] is True
|
| 58 |
assert result.request_summary["mmproj"]
|
| 59 |
+
assert result.request_summary["spaces_gpu"] is True
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def test_cpu_basic_uses_plain_cpu_vision_runner(monkeypatch):
|
| 63 |
+
monkeypatch.setenv("BTE_SPACE_HARDWARE", "cpu-basic")
|
| 64 |
+
monkeypatch.setenv("LLAMACPP_VISION", "1")
|
| 65 |
+
image_parts = [{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}]
|
| 66 |
+
|
| 67 |
+
with patch(
|
| 68 |
+
"src.extraction.llamacpp_gpu.document_to_payload_parts",
|
| 69 |
+
return_value=image_parts,
|
| 70 |
+
), patch(
|
| 71 |
+
"src.extraction.llamacpp_gpu._run_llamacpp_vision_generation_cpu",
|
| 72 |
+
return_value='{"patient":{},"tests":[],"notes":[]}',
|
| 73 |
+
) as cpu_run, patch(
|
| 74 |
+
"src.extraction.llamacpp_gpu._run_llamacpp_vision_generation",
|
| 75 |
+
) as gpu_run:
|
| 76 |
+
result = LlamaCppGPUExtractor().extract("/tmp/report.pdf")
|
| 77 |
+
|
| 78 |
+
cpu_run.assert_called_once()
|
| 79 |
+
gpu_run.assert_not_called()
|
| 80 |
+
assert result.request_summary["backend"] == "llamacpp-cpu-vision"
|
| 81 |
+
assert result.request_summary["spaces_gpu"] is False
|
| 82 |
|
| 83 |
|
| 84 |
def test_extract_uses_text_generation_when_vision_disabled(monkeypatch):
|
| 85 |
monkeypatch.delenv("LLAMACPP_VISION", raising=False)
|
| 86 |
+
monkeypatch.delenv("BTE_SPACE_HARDWARE", raising=False)
|
| 87 |
text_parts = [{"type": "text", "text": "WBC 6.5"}]
|
| 88 |
|
| 89 |
with patch(
|