Spaces:
Sleeping
Sleeping
Merge pull request #2 from MSghais/feat/poc_gradio
Browse files- .env.example +16 -3
- Dockerfile +1 -1
- README.md +21 -18
- USAGE.md +47 -36
- apps/gradio-space/src/gradio_space/app.py +110 -39
- libs/inference/pyproject.toml +4 -6
- libs/inference/src/inference/__init__.py +11 -2
- libs/inference/src/inference/config.py +284 -0
- libs/inference/src/inference/factory.py +28 -11
- libs/inference/src/inference/llama_cpp.py +23 -24
- libs/inference/src/inference/transformers.py +138 -43
- libs/inference/tests/test_config.py +76 -0
- models.yaml +54 -0
- notebook/gemma-finetune.ipynb +499 -0
- scripts/download_model.py +80 -21
- uv.lock +57 -26
.env.example
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
INFERENCE_BACKEND=llama_cpp
|
| 2 |
MODEL_REPO=Qwen/Qwen2.5-3B-Instruct-GGUF
|
| 3 |
MODEL_FILE=qwen2.5-3b-instruct-q4_k_m.gguf
|
| 4 |
N_CTX=4096
|
| 5 |
N_GPU_LAYERS=0
|
| 6 |
|
| 7 |
-
# Optional: local GGUF path instead of Hub download
|
| 8 |
# MODEL_PATH=./models/qwen2.5-3b-instruct-q4_k_m.gguf
|
| 9 |
|
| 10 |
-
# Optional: transformers
|
|
|
|
| 11 |
# INFERENCE_BACKEND=transformers
|
| 12 |
-
# MODEL_ID=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- Preset selection (models.yaml is the source of truth) ---
|
| 2 |
+
ACTIVE_MODEL=qwen3b-gguf
|
| 3 |
+
# Dev: enable dropdown in Gradio. Space: leave false to pin one model for visitors.
|
| 4 |
+
ALLOW_MODEL_SWITCH=true
|
| 5 |
+
# MODEL_PRESETS_PATH=./models.yaml
|
| 6 |
+
|
| 7 |
+
# --- Legacy single-model overrides (optional; applied to ACTIVE_MODEL only) ---
|
| 8 |
INFERENCE_BACKEND=llama_cpp
|
| 9 |
MODEL_REPO=Qwen/Qwen2.5-3B-Instruct-GGUF
|
| 10 |
MODEL_FILE=qwen2.5-3b-instruct-q4_k_m.gguf
|
| 11 |
N_CTX=4096
|
| 12 |
N_GPU_LAYERS=0
|
| 13 |
|
| 14 |
+
# Optional: local GGUF path instead of Hub download (set in models.yaml model_path too)
|
| 15 |
# MODEL_PATH=./models/qwen2.5-3b-instruct-q4_k_m.gguf
|
| 16 |
|
| 17 |
+
# Optional: transformers presets (requires inference[transformers] extra)
|
| 18 |
+
# ACTIVE_MODEL=minicpm5-1b
|
| 19 |
# INFERENCE_BACKEND=transformers
|
| 20 |
+
# MODEL_ID=openbmb/MiniCPM5-1B
|
| 21 |
+
# TRUST_REMOTE_CODE=true
|
| 22 |
+
|
| 23 |
+
# Optional: local fine-tuned merged weights
|
| 24 |
+
# ACTIVE_MODEL=gemma-merged-local
|
| 25 |
+
# MODEL_ID=./gemma_merged_model
|
Dockerfile
CHANGED
|
@@ -13,7 +13,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
| 13 |
|
| 14 |
WORKDIR /app
|
| 15 |
|
| 16 |
-
COPY pyproject.toml uv.lock .python-version README.md ./
|
| 17 |
COPY apps/gradio-space/pyproject.toml apps/gradio-space/README.md apps/gradio-space/
|
| 18 |
COPY libs/inference/pyproject.toml libs/inference/README.md libs/inference/
|
| 19 |
COPY apps/gradio-space/src apps/gradio-space/src
|
|
|
|
| 13 |
|
| 14 |
WORKDIR /app
|
| 15 |
|
| 16 |
+
COPY pyproject.toml uv.lock .python-version README.md models.yaml ./
|
| 17 |
COPY apps/gradio-space/pyproject.toml apps/gradio-space/README.md apps/gradio-space/
|
| 18 |
COPY libs/inference/pyproject.toml libs/inference/README.md libs/inference/
|
| 19 |
COPY apps/gradio-space/src apps/gradio-space/src
|
README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
|
|
|
|
| 3 |
emoji: 🦙
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: green
|
|
@@ -7,7 +8,6 @@ sdk: docker
|
|
| 7 |
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
license: apache-2.0
|
| 10 |
-
---
|
| 11 |
|
| 12 |
# Small Model Hackathon
|
| 13 |
|
|
@@ -33,21 +33,23 @@ uv run python scripts/download_model.py
|
|
| 33 |
uv run --package gradio-space python -m gradio_space.app
|
| 34 |
```
|
| 35 |
|
| 36 |
-
Open http://localhost:7860. The model downloads from Hugging Face Hub on the first chat message (or set `MODEL_PATH` to a local GGUF).
|
| 37 |
|
| 38 |
## Environment variables
|
| 39 |
|
| 40 |
-
| Variable | Default | Description |
|
| 41 |
-
|----------|---------|-------------|
|
| 42 |
-
| `INFERENCE_BACKEND` | `llama_cpp` | `llama_cpp` or `transformers` |
|
| 43 |
-
| `MODEL_REPO` | `Qwen/Qwen2.5-3B-Instruct-GGUF` | Hub repo for GGUF |
|
| 44 |
-
| `MODEL_FILE` | `qwen2.5-3b-instruct-q4_k_m.gguf` | GGUF filename |
|
| 45 |
-
| `MODEL_PATH` | — | Local GGUF path (skips Hub download) |
|
| 46 |
-
| `N_CTX` | `4096` | Context window |
|
| 47 |
-
| `N_GPU_LAYERS` | `0` | GPU layers for llama.cpp (0 = CPU) |
|
| 48 |
-
| `MODEL_ID` | `Qwen/Qwen2.5-3B-Instruct` | Used when `INFERENCE_BACKEND=transformers` |
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
## Monorepo layout
|
| 53 |
|
|
@@ -81,11 +83,11 @@ docker run --rm -p 7860:7860 -e MODEL_REPO=Qwen/Qwen2.5-3B-Instruct-GGUF hackath
|
|
| 81 |
|
| 82 |
## Hackathon checklist
|
| 83 |
|
| 84 |
-
-
|
| 85 |
-
-
|
| 86 |
-
-
|
| 87 |
-
-
|
| 88 |
-
-
|
| 89 |
|
| 90 |
### Badge targets
|
| 91 |
|
|
@@ -101,3 +103,4 @@ uv sync --package inference --extra transformers
|
|
| 101 |
INFERENCE_BACKEND=transformers MODEL_ID=Qwen/Qwen2.5-3B-Instruct \
|
| 102 |
uv run --package gradio-space python -m gradio_space.app
|
| 103 |
```
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
|
| 3 |
+
## title: Small Model Hackathon
|
| 4 |
emoji: 🦙
|
| 5 |
colorFrom: blue
|
| 6 |
colorTo: green
|
|
|
|
| 8 |
app_port: 7860
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
|
|
|
| 11 |
|
| 12 |
# Small Model Hackathon
|
| 13 |
|
|
|
|
| 33 |
uv run --package gradio-space python -m gradio_space.app
|
| 34 |
```
|
| 35 |
|
| 36 |
+
Open [http://localhost:7860](http://localhost:7860). The model downloads from Hugging Face Hub on the first chat message (or set `MODEL_PATH` to a local GGUF).
|
| 37 |
|
| 38 |
## Environment variables
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
| Variable | Default | Description |
|
| 42 |
+
| ------------------- | --------------------------------- | ------------------------------------------ |
|
| 43 |
+
| `INFERENCE_BACKEND` | `llama_cpp` | `llama_cpp` or `transformers` |
|
| 44 |
+
| `MODEL_REPO` | `Qwen/Qwen2.5-3B-Instruct-GGUF` | Hub repo for GGUF |
|
| 45 |
+
| `MODEL_FILE` | `qwen2.5-3b-instruct-q4_k_m.gguf` | GGUF filename |
|
| 46 |
+
| `MODEL_PATH` | — | Local GGUF path (skips Hub download) |
|
| 47 |
+
| `N_CTX` | `4096` | Context window |
|
| 48 |
+
| `N_GPU_LAYERS` | `0` | GPU layers for llama.cpp (0 = CPU) |
|
| 49 |
+
| `MODEL_ID` | `Qwen/Qwen2.5-3B-Instruct` | Used when `INFERENCE_BACKEND=transformers` |
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
See `[.env.example](.env.example)` for a full template.
|
| 53 |
|
| 54 |
## Monorepo layout
|
| 55 |
|
|
|
|
| 83 |
|
| 84 |
## Hackathon checklist
|
| 85 |
|
| 86 |
+
- Choose a track (Backyard AI or Thousand Token Wood)
|
| 87 |
+
- Space live under build-small-hackathon
|
| 88 |
+
- Demo video recorded
|
| 89 |
+
- Social post published
|
| 90 |
+
- Submission locked in by **June 15, 2026**
|
| 91 |
|
| 92 |
### Badge targets
|
| 93 |
|
|
|
|
| 103 |
INFERENCE_BACKEND=transformers MODEL_ID=Qwen/Qwen2.5-3B-Instruct \
|
| 104 |
uv run --package gradio-space python -m gradio_space.app
|
| 105 |
```
|
| 106 |
+
|
USAGE.md
CHANGED
|
@@ -45,7 +45,7 @@ MODEL_PATH=./models/qwen2.5-3b-instruct-q4_k_m.gguf
|
|
| 45 |
uv run --package gradio-space python -m gradio_space.app
|
| 46 |
```
|
| 47 |
|
| 48 |
-
Open http://localhost:7860.
|
| 49 |
|
| 50 |
The model loads on the **first chat message** unless you set `MODEL_PATH`. After code changes, restart the process to pick up updates.
|
| 51 |
|
|
@@ -61,16 +61,18 @@ uv run --package gradio-space python -c "from gradio_space.app import build_demo
|
|
| 61 |
|
| 62 |
### Local env reference
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
|
| 66 |
-
|
|
| 67 |
-
| `
|
| 68 |
-
| `
|
| 69 |
-
| `
|
| 70 |
-
| `
|
| 71 |
-
| `
|
| 72 |
-
| `
|
| 73 |
-
| `
|
|
|
|
|
|
|
| 74 |
|
| 75 |
### Optional: transformers backend
|
| 76 |
|
|
@@ -98,7 +100,7 @@ docker run --rm -p 7860:7860 \
|
|
| 98 |
hackathon-space
|
| 99 |
```
|
| 100 |
|
| 101 |
-
Open http://localhost:7860. Stop with `Ctrl+C`.
|
| 102 |
|
| 103 |
To use a pre-downloaded local model inside Docker, mount it and set `MODEL_PATH`:
|
| 104 |
|
|
@@ -142,22 +144,26 @@ hf repo create build-small-hackathon/<your-space-name> \
|
|
| 142 |
|
| 143 |
### 3. Configure hardware
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
|
| 147 |
-
|
|
| 148 |
-
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
### 4. Set Space environment variables
|
| 151 |
|
| 152 |
In the Space **Settings → Variables and secrets**:
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
|
| 156 |
-
|
|
| 157 |
-
| `
|
| 158 |
-
| `
|
| 159 |
-
| `
|
| 160 |
-
| `
|
|
|
|
|
|
|
| 161 |
|
| 162 |
### 5. Build and verify
|
| 163 |
|
|
@@ -177,14 +183,16 @@ If cold starts are too slow, attach a **Storage Bucket** in Space settings so do
|
|
| 177 |
|
| 178 |
## Troubleshooting
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
|
| 182 |
-
|
|
| 183 |
-
|
|
| 184 |
-
|
|
| 185 |
-
|
|
| 186 |
-
|
|
| 187 |
-
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
---
|
| 190 |
|
|
@@ -196,8 +204,11 @@ All three environments use the same command:
|
|
| 196 |
uv run --package gradio-space python -m gradio_space.app
|
| 197 |
```
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
|
| 201 |
-
|
|
| 202 |
-
|
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
uv run --package gradio-space python -m gradio_space.app
|
| 46 |
```
|
| 47 |
|
| 48 |
+
Open [http://localhost:7860](http://localhost:7860).
|
| 49 |
|
| 50 |
The model loads on the **first chat message** unless you set `MODEL_PATH`. After code changes, restart the process to pick up updates.
|
| 51 |
|
|
|
|
| 61 |
|
| 62 |
### Local env reference
|
| 63 |
|
| 64 |
+
|
| 65 |
+
| Variable | Default | Description |
|
| 66 |
+
| ------------------- | --------------------------------- | ------------------------------------------ |
|
| 67 |
+
| `INFERENCE_BACKEND` | `llama_cpp` | `llama_cpp` or `transformers` |
|
| 68 |
+
| `MODEL_REPO` | `Qwen/Qwen2.5-3B-Instruct-GGUF` | Hub repo for GGUF |
|
| 69 |
+
| `MODEL_FILE` | `qwen2.5-3b-instruct-q4_k_m.gguf` | GGUF filename |
|
| 70 |
+
| `MODEL_PATH` | — | Local GGUF path (skips Hub download) |
|
| 71 |
+
| `N_CTX` | `4096` | Context window |
|
| 72 |
+
| `N_GPU_LAYERS` | `0` | GPU layers for llama.cpp (`0` = CPU only) |
|
| 73 |
+
| `PORT` | `7860` | Gradio listen port |
|
| 74 |
+
| `MODEL_ID` | `Qwen/Qwen2.5-3B-Instruct` | Used when `INFERENCE_BACKEND=transformers` |
|
| 75 |
+
|
| 76 |
|
| 77 |
### Optional: transformers backend
|
| 78 |
|
|
|
|
| 100 |
hackathon-space
|
| 101 |
```
|
| 102 |
|
| 103 |
+
Open [http://localhost:7860](http://localhost:7860). Stop with `Ctrl+C`.
|
| 104 |
|
| 105 |
To use a pre-downloaded local model inside Docker, mount it and set `MODEL_PATH`:
|
| 106 |
|
|
|
|
| 144 |
|
| 145 |
### 3. Configure hardware
|
| 146 |
|
| 147 |
+
|
| 148 |
+
| Setting | Recommendation |
|
| 149 |
+
| -------- | ------------------------------------------------------------ |
|
| 150 |
+
| Hardware | **CPU basic** to start (llama.cpp with `N_GPU_LAYERS=0`) |
|
| 151 |
+
| Upgrade | GPU Space if you set `N_GPU_LAYERS > 0` for faster inference |
|
| 152 |
+
|
| 153 |
|
| 154 |
### 4. Set Space environment variables
|
| 155 |
|
| 156 |
In the Space **Settings → Variables and secrets**:
|
| 157 |
|
| 158 |
+
|
| 159 |
+
| Variable | Value |
|
| 160 |
+
| ------------------- | --------------------------------- |
|
| 161 |
+
| `INFERENCE_BACKEND` | `llama_cpp` |
|
| 162 |
+
| `MODEL_REPO` | `Qwen/Qwen2.5-3B-Instruct-GGUF` |
|
| 163 |
+
| `MODEL_FILE` | `qwen2.5-3b-instruct-q4_k_m.gguf` |
|
| 164 |
+
| `N_CTX` | `4096` |
|
| 165 |
+
| `N_GPU_LAYERS` | `0` (or higher on GPU hardware) |
|
| 166 |
+
|
| 167 |
|
| 168 |
### 5. Build and verify
|
| 169 |
|
|
|
|
| 183 |
|
| 184 |
## Troubleshooting
|
| 185 |
|
| 186 |
+
|
| 187 |
+
| Symptom | Likely cause | Fix |
|
| 188 |
+
| ---------------------------------------- | --------------------------------- | -------------------------------------------------------------------- |
|
| 189 |
+
| First chat hangs / slow | GGUF downloading from Hub | Pre-download locally; on Space, wait or use Storage Bucket |
|
| 190 |
+
| `Failed to load model` in chat | Wrong `MODEL_REPO` / `MODEL_FILE` | Check env vars match a valid GGUF on Hub |
|
| 191 |
+
| Docker build fails on `llama-cpp-python` | Missing build tools | Dockerfile already installs `build-essential` and `cmake` |
|
| 192 |
+
| Space build fails | Missing `uv.lock` or README YAML | Ensure `sdk: docker` is in root `README.md` frontmatter |
|
| 193 |
+
| `transformers` backend error | Optional deps not installed | Run `uv sync --package inference --extra transformers` |
|
| 194 |
+
| Port already in use locally | Another process on 7860 | `PORT=7861 uv run --package gradio-space python -m gradio_space.app` |
|
| 195 |
+
|
| 196 |
|
| 197 |
---
|
| 198 |
|
|
|
|
| 204 |
uv run --package gradio-space python -m gradio_space.app
|
| 205 |
```
|
| 206 |
|
| 207 |
+
|
| 208 |
+
| Environment | How to run |
|
| 209 |
+
| ----------- | ---------------------------------------------------------- |
|
| 210 |
+
| Local dev | `uv run --package gradio-space python -m gradio_space.app` |
|
| 211 |
+
| Docker | `docker run -p 7860:7860 hackathon-space` |
|
| 212 |
+
| HF Space | Built and started automatically from `Dockerfile` `CMD` |
|
| 213 |
+
|
| 214 |
+
|
apps/gradio-space/src/gradio_space/app.py
CHANGED
|
@@ -2,36 +2,39 @@ import os
|
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
-
from inference.
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
-
def _ensure_model_loaded() -> str | None:
|
| 13 |
-
global
|
| 14 |
|
| 15 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
return None
|
| 17 |
|
| 18 |
-
if
|
| 19 |
-
return
|
| 20 |
|
| 21 |
try:
|
| 22 |
-
|
| 23 |
-
|
| 24 |
return None
|
| 25 |
except Exception as exc: # noqa: BLE001 — surface model load failures in the UI
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
|
| 30 |
-
def chat(message: str, history: list) -> str:
|
| 31 |
-
load_error = _ensure_model_loaded()
|
| 32 |
-
if load_error:
|
| 33 |
-
return load_error
|
| 34 |
|
|
|
|
| 35 |
messages: list[dict[str, str]] = []
|
| 36 |
for item in history:
|
| 37 |
if isinstance(item, dict):
|
|
@@ -41,48 +44,116 @@ def chat(message: str, history: list) -> str:
|
|
| 41 |
messages.append({"role": "user", "content": user_msg})
|
| 42 |
if assistant_msg:
|
| 43 |
messages.append({"role": "assistant", "content": assistant_msg})
|
|
|
|
| 44 |
|
| 45 |
-
messages.append({"role": "user", "content": message})
|
| 46 |
-
return _backend.chat(messages)
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
if
|
| 54 |
-
return
|
| 55 |
|
|
|
|
| 56 |
return (
|
| 57 |
-
"
|
| 58 |
-
"
|
| 59 |
)
|
| 60 |
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
def build_demo() -> gr.Blocks:
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
with gr.Blocks(title="Small Model Hackathon") as demo:
|
| 68 |
gr.Markdown(
|
| 69 |
f"""
|
| 70 |
# Small Model Chat
|
| 71 |
|
| 72 |
-
Local inference
|
| 73 |
|
| 74 |
-
- **
|
| 75 |
-
- **
|
|
|
|
| 76 |
|
| 77 |
Part of the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
| 78 |
"""
|
| 79 |
)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
return demo
|
| 88 |
|
|
|
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
+
from inference.config import get_app_config, get_model_config
|
| 6 |
+
from inference.factory import get_backend, reset_backend
|
| 7 |
|
| 8 |
+
_app_config = get_app_config()
|
| 9 |
+
_current_model_key: str | None = None
|
| 10 |
+
_load_state: dict[str, bool] = {}
|
| 11 |
+
_load_errors: dict[str, str] = {}
|
| 12 |
|
| 13 |
|
| 14 |
+
def _ensure_model_loaded(model_key: str) -> str | None:
|
| 15 |
+
global _current_model_key
|
| 16 |
|
| 17 |
+
if model_key != _current_model_key:
|
| 18 |
+
reset_backend()
|
| 19 |
+
_current_model_key = model_key
|
| 20 |
+
|
| 21 |
+
if _load_state.get(model_key):
|
| 22 |
return None
|
| 23 |
|
| 24 |
+
if model_key in _load_errors:
|
| 25 |
+
return _load_errors[model_key]
|
| 26 |
|
| 27 |
try:
|
| 28 |
+
get_backend(model_key).load()
|
| 29 |
+
_load_state[model_key] = True
|
| 30 |
return None
|
| 31 |
except Exception as exc: # noqa: BLE001 — surface model load failures in the UI
|
| 32 |
+
message = f"Failed to load model: {exc}"
|
| 33 |
+
_load_errors[model_key] = message
|
| 34 |
+
return message
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
def _history_to_messages(history: list) -> list[dict[str, str]]:
|
| 38 |
messages: list[dict[str, str]] = []
|
| 39 |
for item in history:
|
| 40 |
if isinstance(item, dict):
|
|
|
|
| 44 |
messages.append({"role": "user", "content": user_msg})
|
| 45 |
if assistant_msg:
|
| 46 |
messages.append({"role": "assistant", "content": assistant_msg})
|
| 47 |
+
return messages
|
| 48 |
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
def chat(message: str, history: list, model_key: str) -> str:
|
| 51 |
+
load_error = _ensure_model_loaded(model_key)
|
| 52 |
+
if load_error:
|
| 53 |
+
return load_error
|
| 54 |
|
| 55 |
+
messages = _history_to_messages(history)
|
| 56 |
+
messages.append({"role": "user", "content": message})
|
| 57 |
+
return get_backend(model_key).chat(messages)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _runtime_device_hint(model_key: str) -> str:
|
| 61 |
+
model = get_model_config(model_key)
|
| 62 |
+
if model.backend == "transformers":
|
| 63 |
+
try:
|
| 64 |
+
import torch
|
| 65 |
+
|
| 66 |
+
if torch.cuda.is_available():
|
| 67 |
+
return f"GPU ({torch.cuda.get_device_name(0)})"
|
| 68 |
+
except ImportError:
|
| 69 |
+
pass
|
| 70 |
+
return "CPU"
|
| 71 |
+
if model.n_gpu_layers > 0:
|
| 72 |
+
return f"llama.cpp GPU offload ({model.n_gpu_layers} layers)"
|
| 73 |
+
return "CPU"
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def warmup(model_key: str | None = None) -> str:
|
| 77 |
+
key = model_key or _app_config.active_model
|
| 78 |
+
model = get_model_config(key)
|
| 79 |
+
|
| 80 |
+
if _load_state.get(key):
|
| 81 |
+
backend = get_backend(key)
|
| 82 |
+
device = (
|
| 83 |
+
backend.device_label
|
| 84 |
+
if hasattr(backend, "device_label")
|
| 85 |
+
else _runtime_device_hint(key)
|
| 86 |
+
)
|
| 87 |
+
return f"Model ready: {model.label} on {device}"
|
| 88 |
|
| 89 |
+
if key in _load_errors:
|
| 90 |
+
return _load_errors[key]
|
| 91 |
|
| 92 |
+
device_hint = _runtime_device_hint(key)
|
| 93 |
return (
|
| 94 |
+
f"Preset `{key}` selected ({model.backend}, {device_hint}). "
|
| 95 |
+
"Weights load on the first chat message."
|
| 96 |
)
|
| 97 |
|
| 98 |
|
| 99 |
+
def model_status(model_key: str) -> str:
|
| 100 |
+
model = get_model_config(model_key)
|
| 101 |
+
return f"**{model.label}**\n\n- Backend: `{model.backend}`\n- {warmup(model_key)}"
|
| 102 |
+
|
| 103 |
+
|
| 104 |
def build_demo() -> gr.Blocks:
|
| 105 |
+
active = _app_config.active
|
| 106 |
+
presets_note = (
|
| 107 |
+
f"Presets file: `{_app_config.presets_path}`"
|
| 108 |
+
if _app_config.presets_path
|
| 109 |
+
else "Using built-in presets (models.yaml not found)."
|
| 110 |
+
)
|
| 111 |
|
| 112 |
with gr.Blocks(title="Small Model Hackathon") as demo:
|
| 113 |
gr.Markdown(
|
| 114 |
f"""
|
| 115 |
# Small Model Chat
|
| 116 |
|
| 117 |
+
Local inference with preset-based configuration.
|
| 118 |
|
| 119 |
+
- **Default preset:** `{active.key}` — {active.label}
|
| 120 |
+
- **Backend:** `{active.backend}`
|
| 121 |
+
- {presets_note}
|
| 122 |
|
| 123 |
Part of the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
| 124 |
"""
|
| 125 |
)
|
| 126 |
+
|
| 127 |
+
if _app_config.allow_model_switch and len(_app_config.models) > 1:
|
| 128 |
+
model_dropdown = gr.Dropdown(
|
| 129 |
+
choices=_app_config.model_choices(),
|
| 130 |
+
value=_app_config.active_model,
|
| 131 |
+
label="Model preset",
|
| 132 |
+
info="Switch presets for local testing. Each preset loads on first use.",
|
| 133 |
+
)
|
| 134 |
+
status = gr.Markdown(model_status(_app_config.active_model))
|
| 135 |
+
|
| 136 |
+
model_dropdown.change(
|
| 137 |
+
fn=model_status,
|
| 138 |
+
inputs=model_dropdown,
|
| 139 |
+
outputs=status,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
gr.ChatInterface(
|
| 143 |
+
fn=chat,
|
| 144 |
+
additional_inputs=[model_dropdown],
|
| 145 |
+
examples=[
|
| 146 |
+
["Hello! What can you help me with?", _app_config.active_model],
|
| 147 |
+
["Explain llama.cpp in one sentence.", _app_config.active_model],
|
| 148 |
+
],
|
| 149 |
+
)
|
| 150 |
+
else:
|
| 151 |
+
status = gr.Markdown(model_status(_app_config.active_model))
|
| 152 |
+
gr.ChatInterface(
|
| 153 |
+
fn=lambda message, history: chat(message, history, _app_config.active_model),
|
| 154 |
+
examples=["Hello! What can you help me with?", "Explain llama.cpp in one sentence."],
|
| 155 |
+
)
|
| 156 |
+
demo.load(lambda: warmup(_app_config.active_model), outputs=status)
|
| 157 |
|
| 158 |
return demo
|
| 159 |
|
libs/inference/pyproject.toml
CHANGED
|
@@ -8,15 +8,13 @@ authors = [
|
|
| 8 |
]
|
| 9 |
requires-python = ">=3.12"
|
| 10 |
dependencies = [
|
|
|
|
| 11 |
"huggingface-hub>=0.27.0",
|
| 12 |
"llama-cpp-python>=0.3.0",
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
[project.optional-dependencies]
|
| 16 |
-
transformers = [
|
| 17 |
-
"accelerate>=1.2.0",
|
| 18 |
"torch>=2.5.0",
|
| 19 |
-
"
|
|
|
|
| 20 |
]
|
| 21 |
|
| 22 |
[build-system]
|
|
|
|
| 8 |
]
|
| 9 |
requires-python = ">=3.12"
|
| 10 |
dependencies = [
|
| 11 |
+
"accelerate>=1.2.0",
|
| 12 |
"huggingface-hub>=0.27.0",
|
| 13 |
"llama-cpp-python>=0.3.0",
|
| 14 |
+
"pyyaml>=6.0.2",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"torch>=2.5.0",
|
| 16 |
+
"torchvision>=0.20.0",
|
| 17 |
+
"transformers>=5.7.0",
|
| 18 |
]
|
| 19 |
|
| 20 |
[build-system]
|
libs/inference/src/inference/__init__.py
CHANGED
|
@@ -1,3 +1,12 @@
|
|
| 1 |
-
from inference.
|
|
|
|
| 2 |
|
| 3 |
-
__all__ = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from inference.config import AppConfig, ModelConfig, get_app_config, get_model_config, load_app_config
|
| 2 |
+
from inference.factory import get_backend, reset_backend
|
| 3 |
|
| 4 |
+
__all__ = [
|
| 5 |
+
"AppConfig",
|
| 6 |
+
"ModelConfig",
|
| 7 |
+
"get_app_config",
|
| 8 |
+
"get_backend",
|
| 9 |
+
"get_model_config",
|
| 10 |
+
"load_app_config",
|
| 11 |
+
"reset_backend",
|
| 12 |
+
]
|
libs/inference/src/inference/config.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Central model preset and app configuration."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from dataclasses import dataclass, replace
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Literal
|
| 9 |
+
|
| 10 |
+
BackendName = Literal["llama_cpp", "transformers"]
|
| 11 |
+
|
| 12 |
+
DEFAULT_PRESET_KEY = "qwen3b-gguf"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass(frozen=True)
|
| 16 |
+
class ModelConfig:
|
| 17 |
+
"""Single model preset used by inference backends and the Gradio UI."""
|
| 18 |
+
|
| 19 |
+
key: str
|
| 20 |
+
label: str
|
| 21 |
+
backend: BackendName
|
| 22 |
+
model_repo: str | None = None
|
| 23 |
+
model_file: str | None = None
|
| 24 |
+
model_path: str | None = None
|
| 25 |
+
model_id: str | None = None
|
| 26 |
+
trust_remote_code: bool = False
|
| 27 |
+
multimodal: bool = False
|
| 28 |
+
n_ctx: int = 4096
|
| 29 |
+
n_gpu_layers: int = 0
|
| 30 |
+
max_tokens: int = 512
|
| 31 |
+
temperature: float = 0.7
|
| 32 |
+
|
| 33 |
+
def cache_key(self) -> tuple[Any, ...]:
|
| 34 |
+
return (
|
| 35 |
+
self.backend,
|
| 36 |
+
self.model_repo,
|
| 37 |
+
self.model_file,
|
| 38 |
+
self.model_path,
|
| 39 |
+
self.model_id,
|
| 40 |
+
self.trust_remote_code,
|
| 41 |
+
self.multimodal,
|
| 42 |
+
self.n_ctx,
|
| 43 |
+
self.n_gpu_layers,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
def summary(self) -> str:
|
| 47 |
+
if self.backend == "llama_cpp":
|
| 48 |
+
source = self.model_path or f"{self.model_repo}/{self.model_file}"
|
| 49 |
+
return f"{self.label} · llama.cpp · {source}"
|
| 50 |
+
return f"{self.label} · transformers · {self.model_id}"
|
| 51 |
+
|
| 52 |
+
def resolve_paths(self, base_dir: Path) -> ModelConfig:
|
| 53 |
+
updates: dict[str, Any] = {}
|
| 54 |
+
|
| 55 |
+
if self.model_path:
|
| 56 |
+
path = Path(self.model_path)
|
| 57 |
+
if not path.is_absolute():
|
| 58 |
+
updates["model_path"] = str((base_dir / path).resolve())
|
| 59 |
+
|
| 60 |
+
if self.model_id and self.model_id.startswith(("./", "../")):
|
| 61 |
+
updates["model_id"] = str((base_dir / self.model_id).resolve())
|
| 62 |
+
|
| 63 |
+
return replace(self, **updates) if updates else self
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@dataclass(frozen=True)
|
| 67 |
+
class AppConfig:
|
| 68 |
+
"""Runtime app configuration for dev and Hugging Face Space."""
|
| 69 |
+
|
| 70 |
+
active_model: str
|
| 71 |
+
models: dict[str, ModelConfig]
|
| 72 |
+
allow_model_switch: bool = False
|
| 73 |
+
model_cache_dir: str | None = None
|
| 74 |
+
presets_path: Path | None = None
|
| 75 |
+
|
| 76 |
+
def get_model(self, key: str | None = None) -> ModelConfig:
|
| 77 |
+
model_key = key or self.active_model
|
| 78 |
+
if model_key not in self.models:
|
| 79 |
+
known = ", ".join(sorted(self.models))
|
| 80 |
+
raise KeyError(f"Unknown model preset {model_key!r}. Known presets: {known}")
|
| 81 |
+
return self.models[model_key]
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
def active(self) -> ModelConfig:
|
| 85 |
+
return self.get_model(self.active_model)
|
| 86 |
+
|
| 87 |
+
def model_choices(self) -> list[tuple[str, str]]:
|
| 88 |
+
return [(model.label, model.key) for model in self.models.values()]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _builtin_presets() -> dict[str, ModelConfig]:
|
| 92 |
+
return {
|
| 93 |
+
DEFAULT_PRESET_KEY: ModelConfig(
|
| 94 |
+
key=DEFAULT_PRESET_KEY,
|
| 95 |
+
label="Qwen 2.5 3B Instruct (GGUF)",
|
| 96 |
+
backend="llama_cpp",
|
| 97 |
+
model_repo="Qwen/Qwen2.5-3B-Instruct-GGUF",
|
| 98 |
+
model_file="qwen2.5-3b-instruct-q4_k_m.gguf",
|
| 99 |
+
),
|
| 100 |
+
"minicpm5-1b": ModelConfig(
|
| 101 |
+
key="minicpm5-1b",
|
| 102 |
+
label="MiniCPM5 1B (Transformers)",
|
| 103 |
+
backend="transformers",
|
| 104 |
+
model_id="openbmb/MiniCPM5-1B",
|
| 105 |
+
trust_remote_code=True,
|
| 106 |
+
),
|
| 107 |
+
"gemma-merged-local": ModelConfig(
|
| 108 |
+
key="gemma-merged-local",
|
| 109 |
+
label="Fine-tuned merged model (local)",
|
| 110 |
+
backend="transformers",
|
| 111 |
+
model_id="./gemma_merged_model",
|
| 112 |
+
),
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _find_presets_path() -> Path | None:
|
| 117 |
+
env_path = os.environ.get("MODEL_PRESETS_PATH")
|
| 118 |
+
if env_path:
|
| 119 |
+
path = Path(env_path)
|
| 120 |
+
if path.is_file():
|
| 121 |
+
return path.resolve()
|
| 122 |
+
|
| 123 |
+
for base in (Path.cwd(), *Path.cwd().parents):
|
| 124 |
+
candidate = base / "models.yaml"
|
| 125 |
+
if candidate.is_file():
|
| 126 |
+
return candidate.resolve()
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _repo_root_for(presets_path: Path | None) -> Path:
|
| 131 |
+
if presets_path is not None:
|
| 132 |
+
return presets_path.parent
|
| 133 |
+
app_root = os.environ.get("APP_ROOT")
|
| 134 |
+
if app_root:
|
| 135 |
+
return Path(app_root).resolve()
|
| 136 |
+
return Path.cwd().resolve()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _parse_model_entry(key: str, raw: dict[str, Any]) -> ModelConfig:
|
| 140 |
+
backend = str(raw.get("backend", "llama_cpp")).lower()
|
| 141 |
+
if backend not in ("llama_cpp", "transformers"):
|
| 142 |
+
raise ValueError(f"Preset {key!r}: backend must be llama_cpp or transformers")
|
| 143 |
+
|
| 144 |
+
return ModelConfig(
|
| 145 |
+
key=key,
|
| 146 |
+
label=str(raw.get("label", key)),
|
| 147 |
+
backend=backend, # type: ignore[arg-type]
|
| 148 |
+
model_repo=raw.get("model_repo"),
|
| 149 |
+
model_file=raw.get("model_file"),
|
| 150 |
+
model_path=raw.get("model_path"),
|
| 151 |
+
model_id=raw.get("model_id"),
|
| 152 |
+
trust_remote_code=bool(raw.get("trust_remote_code", False)),
|
| 153 |
+
multimodal=bool(raw.get("multimodal", False)),
|
| 154 |
+
n_ctx=int(raw.get("n_ctx", 4096)),
|
| 155 |
+
n_gpu_layers=int(raw.get("n_gpu_layers", 0)),
|
| 156 |
+
max_tokens=int(raw.get("max_tokens", 512)),
|
| 157 |
+
temperature=float(raw.get("temperature", 0.7)),
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _load_presets_from_yaml(path: Path) -> tuple[dict[str, Any], dict[str, ModelConfig]]:
|
| 162 |
+
try:
|
| 163 |
+
import yaml
|
| 164 |
+
except ImportError as exc:
|
| 165 |
+
raise ImportError(
|
| 166 |
+
"Loading models.yaml requires PyYAML. Install with: uv add --package inference pyyaml"
|
| 167 |
+
) from exc
|
| 168 |
+
|
| 169 |
+
data = yaml.safe_load(path.read_text()) or {}
|
| 170 |
+
defaults = data.get("defaults", {})
|
| 171 |
+
raw_models = data.get("models", {})
|
| 172 |
+
if not isinstance(raw_models, dict) or not raw_models:
|
| 173 |
+
raise ValueError(f"{path}: expected non-empty top-level 'models' mapping")
|
| 174 |
+
|
| 175 |
+
models = {key: _parse_model_entry(key, value) for key, value in raw_models.items()}
|
| 176 |
+
return defaults, models
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def _apply_legacy_env_overrides(model: ModelConfig) -> ModelConfig:
|
| 180 |
+
"""Keep single-model .env workflow working alongside preset keys."""
|
| 181 |
+
|
| 182 |
+
updates: dict[str, Any] = {}
|
| 183 |
+
|
| 184 |
+
backend = os.environ.get("INFERENCE_BACKEND")
|
| 185 |
+
if backend:
|
| 186 |
+
updates["backend"] = backend.lower()
|
| 187 |
+
|
| 188 |
+
for field, env_name in (
|
| 189 |
+
("model_repo", "MODEL_REPO"),
|
| 190 |
+
("model_file", "MODEL_FILE"),
|
| 191 |
+
("model_path", "MODEL_PATH"),
|
| 192 |
+
("model_id", "MODEL_ID"),
|
| 193 |
+
):
|
| 194 |
+
value = os.environ.get(env_name)
|
| 195 |
+
if value:
|
| 196 |
+
updates[field] = value
|
| 197 |
+
|
| 198 |
+
if os.environ.get("TRUST_REMOTE_CODE", "").lower() in {"1", "true", "yes"}:
|
| 199 |
+
updates["trust_remote_code"] = True
|
| 200 |
+
|
| 201 |
+
for field, env_name in (
|
| 202 |
+
("n_ctx", "N_CTX"),
|
| 203 |
+
("n_gpu_layers", "N_GPU_LAYERS"),
|
| 204 |
+
("max_tokens", "MAX_TOKENS"),
|
| 205 |
+
):
|
| 206 |
+
value = os.environ.get(env_name)
|
| 207 |
+
if value is not None and value != "":
|
| 208 |
+
updates[field] = int(value)
|
| 209 |
+
|
| 210 |
+
temperature = os.environ.get("TEMPERATURE")
|
| 211 |
+
if temperature is not None and temperature != "":
|
| 212 |
+
updates["temperature"] = float(temperature)
|
| 213 |
+
|
| 214 |
+
return replace(model, **updates) if updates else model
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def load_app_config() -> AppConfig:
|
| 218 |
+
presets_path = _find_presets_path()
|
| 219 |
+
repo_root = _repo_root_for(presets_path)
|
| 220 |
+
|
| 221 |
+
if presets_path is None:
|
| 222 |
+
defaults: dict[str, Any] = {}
|
| 223 |
+
models = _builtin_presets()
|
| 224 |
+
else:
|
| 225 |
+
defaults, models = _load_presets_from_yaml(presets_path)
|
| 226 |
+
|
| 227 |
+
active_model = os.environ.get("ACTIVE_MODEL") or defaults.get(
|
| 228 |
+
"active_model", DEFAULT_PRESET_KEY
|
| 229 |
+
)
|
| 230 |
+
if active_model not in models:
|
| 231 |
+
active_model = next(iter(models))
|
| 232 |
+
|
| 233 |
+
allow_model_switch = os.environ.get("ALLOW_MODEL_SWITCH")
|
| 234 |
+
if allow_model_switch is None:
|
| 235 |
+
allow_switch = bool(defaults.get("allow_model_switch", False))
|
| 236 |
+
else:
|
| 237 |
+
allow_switch = allow_model_switch.lower() in {"1", "true", "yes"}
|
| 238 |
+
|
| 239 |
+
cache_dir = os.environ.get("MODEL_CACHE_DIR") or defaults.get("model_cache_dir")
|
| 240 |
+
|
| 241 |
+
resolved_models = {
|
| 242 |
+
key: model.resolve_paths(repo_root) for key, model in models.items()
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
has_legacy_override = any(
|
| 246 |
+
os.environ.get(name)
|
| 247 |
+
for name in (
|
| 248 |
+
"INFERENCE_BACKEND",
|
| 249 |
+
"MODEL_REPO",
|
| 250 |
+
"MODEL_FILE",
|
| 251 |
+
"MODEL_PATH",
|
| 252 |
+
"MODEL_ID",
|
| 253 |
+
"N_CTX",
|
| 254 |
+
"N_GPU_LAYERS",
|
| 255 |
+
)
|
| 256 |
+
)
|
| 257 |
+
if has_legacy_override:
|
| 258 |
+
resolved_models[active_model] = _apply_legacy_env_overrides(
|
| 259 |
+
resolved_models[active_model]
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
return AppConfig(
|
| 263 |
+
active_model=active_model,
|
| 264 |
+
models=resolved_models,
|
| 265 |
+
allow_model_switch=allow_switch,
|
| 266 |
+
model_cache_dir=cache_dir,
|
| 267 |
+
presets_path=presets_path,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
_app_config: AppConfig | None = None
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def get_app_config(reload: bool = False) -> AppConfig:
|
| 275 |
+
global _app_config
|
| 276 |
+
if _app_config is None or reload:
|
| 277 |
+
_app_config = load_app_config()
|
| 278 |
+
return _app_config
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def get_model_config(key: str | None = None) -> ModelConfig:
|
| 282 |
+
config = get_app_config()
|
| 283 |
+
model_key = key or config.active_model
|
| 284 |
+
return config.get_model(model_key)
|
libs/inference/src/inference/factory.py
CHANGED
|
@@ -1,23 +1,40 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from functools import lru_cache
|
| 3 |
-
|
| 4 |
from inference.base import InferenceBackend
|
|
|
|
| 5 |
from inference.llama_cpp import LlamaCppBackend
|
| 6 |
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
@lru_cache(maxsize=1)
|
| 9 |
-
def get_backend() -> InferenceBackend:
|
| 10 |
-
backend_name = os.environ.get("INFERENCE_BACKEND", "llama_cpp").lower()
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
|
|
|
| 14 |
|
| 15 |
-
if
|
| 16 |
from inference.transformers import TransformersBackend
|
| 17 |
|
| 18 |
-
return TransformersBackend()
|
| 19 |
|
| 20 |
raise ValueError(
|
| 21 |
-
f"Unknown
|
| 22 |
"Expected 'llama_cpp' or 'transformers'."
|
| 23 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from inference.base import InferenceBackend
|
| 2 |
+
from inference.config import ModelConfig, get_model_config
|
| 3 |
from inference.llama_cpp import LlamaCppBackend
|
| 4 |
|
| 5 |
+
_backend: InferenceBackend | None = None
|
| 6 |
+
_backend_key: tuple | None = None
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
def _create_backend(config: ModelConfig) -> InferenceBackend:
|
| 10 |
+
if config.backend == "llama_cpp":
|
| 11 |
+
return LlamaCppBackend(config)
|
| 12 |
|
| 13 |
+
if config.backend == "transformers":
|
| 14 |
from inference.transformers import TransformersBackend
|
| 15 |
|
| 16 |
+
return TransformersBackend(config)
|
| 17 |
|
| 18 |
raise ValueError(
|
| 19 |
+
f"Unknown backend {config.backend!r} for preset {config.key!r}. "
|
| 20 |
"Expected 'llama_cpp' or 'transformers'."
|
| 21 |
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def get_backend(model_key: str | None = None) -> InferenceBackend:
|
| 25 |
+
global _backend, _backend_key
|
| 26 |
+
|
| 27 |
+
config = get_model_config(model_key)
|
| 28 |
+
cache_key = config.cache_key()
|
| 29 |
+
|
| 30 |
+
if _backend is None or _backend_key != cache_key:
|
| 31 |
+
_backend = _create_backend(config)
|
| 32 |
+
_backend_key = cache_key
|
| 33 |
+
|
| 34 |
+
return _backend
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def reset_backend() -> None:
|
| 38 |
+
global _backend, _backend_key
|
| 39 |
+
_backend = None
|
| 40 |
+
_backend_key = None
|
libs/inference/src/inference/llama_cpp.py
CHANGED
|
@@ -4,31 +4,32 @@ from pathlib import Path
|
|
| 4 |
from huggingface_hub import hf_hub_download
|
| 5 |
from llama_cpp import Llama
|
| 6 |
|
| 7 |
-
|
| 8 |
-
DEFAULT_MODEL_REPO = "Qwen/Qwen2.5-3B-Instruct-GGUF"
|
| 9 |
-
DEFAULT_MODEL_FILE = "qwen2.5-3b-instruct-q4_k_m.gguf"
|
| 10 |
|
| 11 |
|
| 12 |
class LlamaCppBackend:
|
| 13 |
-
def __init__(self) -> None:
|
|
|
|
| 14 |
self._model: Llama | None = None
|
| 15 |
self._model_path: str | None = None
|
| 16 |
|
| 17 |
def _resolve_model_path(self) -> str:
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
path = Path(model_path)
|
| 21 |
if not path.exists():
|
| 22 |
-
raise FileNotFoundError(f"MODEL_PATH does not exist: {model_path}")
|
| 23 |
return str(path)
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
| 27 |
cache_dir = os.environ.get("MODEL_CACHE_DIR")
|
| 28 |
|
| 29 |
return hf_hub_download(
|
| 30 |
-
repo_id=model_repo,
|
| 31 |
-
filename=model_file,
|
| 32 |
cache_dir=cache_dir,
|
| 33 |
)
|
| 34 |
|
|
@@ -37,13 +38,11 @@ class LlamaCppBackend:
|
|
| 37 |
return
|
| 38 |
|
| 39 |
self._model_path = self._resolve_model_path()
|
| 40 |
-
n_ctx = int(os.environ.get("N_CTX", "4096"))
|
| 41 |
-
n_gpu_layers = int(os.environ.get("N_GPU_LAYERS", "0"))
|
| 42 |
|
| 43 |
self._model = Llama(
|
| 44 |
model_path=self._model_path,
|
| 45 |
-
n_ctx=n_ctx,
|
| 46 |
-
n_gpu_layers=n_gpu_layers,
|
| 47 |
verbose=False,
|
| 48 |
)
|
| 49 |
|
|
@@ -51,16 +50,16 @@ class LlamaCppBackend:
|
|
| 51 |
self,
|
| 52 |
prompt: str,
|
| 53 |
*,
|
| 54 |
-
max_tokens: int =
|
| 55 |
-
temperature: float =
|
| 56 |
) -> str:
|
| 57 |
self.load()
|
| 58 |
assert self._model is not None
|
| 59 |
|
| 60 |
result = self._model(
|
| 61 |
prompt,
|
| 62 |
-
max_tokens=max_tokens,
|
| 63 |
-
temperature=temperature,
|
| 64 |
echo=False,
|
| 65 |
)
|
| 66 |
return result["choices"][0]["text"].strip()
|
|
@@ -69,15 +68,15 @@ class LlamaCppBackend:
|
|
| 69 |
self,
|
| 70 |
messages: list[dict[str, str]],
|
| 71 |
*,
|
| 72 |
-
max_tokens: int =
|
| 73 |
-
temperature: float =
|
| 74 |
) -> str:
|
| 75 |
self.load()
|
| 76 |
assert self._model is not None
|
| 77 |
|
| 78 |
result = self._model.create_chat_completion(
|
| 79 |
messages=messages,
|
| 80 |
-
max_tokens=max_tokens,
|
| 81 |
-
temperature=temperature,
|
| 82 |
)
|
| 83 |
return result["choices"][0]["message"]["content"].strip()
|
|
|
|
| 4 |
from huggingface_hub import hf_hub_download
|
| 5 |
from llama_cpp import Llama
|
| 6 |
|
| 7 |
+
from inference.config import ModelConfig
|
|
|
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
class LlamaCppBackend:
|
| 11 |
+
def __init__(self, config: ModelConfig) -> None:
|
| 12 |
+
self._config = config
|
| 13 |
self._model: Llama | None = None
|
| 14 |
self._model_path: str | None = None
|
| 15 |
|
| 16 |
def _resolve_model_path(self) -> str:
|
| 17 |
+
if self._config.model_path:
|
| 18 |
+
path = Path(self._config.model_path)
|
|
|
|
| 19 |
if not path.exists():
|
| 20 |
+
raise FileNotFoundError(f"MODEL_PATH does not exist: {self._config.model_path}")
|
| 21 |
return str(path)
|
| 22 |
|
| 23 |
+
if not self._config.model_repo or not self._config.model_file:
|
| 24 |
+
raise ValueError(
|
| 25 |
+
f"Preset {self._config.key!r} requires model_repo and model_file for llama_cpp"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
cache_dir = os.environ.get("MODEL_CACHE_DIR")
|
| 29 |
|
| 30 |
return hf_hub_download(
|
| 31 |
+
repo_id=self._config.model_repo,
|
| 32 |
+
filename=self._config.model_file,
|
| 33 |
cache_dir=cache_dir,
|
| 34 |
)
|
| 35 |
|
|
|
|
| 38 |
return
|
| 39 |
|
| 40 |
self._model_path = self._resolve_model_path()
|
|
|
|
|
|
|
| 41 |
|
| 42 |
self._model = Llama(
|
| 43 |
model_path=self._model_path,
|
| 44 |
+
n_ctx=self._config.n_ctx,
|
| 45 |
+
n_gpu_layers=self._config.n_gpu_layers,
|
| 46 |
verbose=False,
|
| 47 |
)
|
| 48 |
|
|
|
|
| 50 |
self,
|
| 51 |
prompt: str,
|
| 52 |
*,
|
| 53 |
+
max_tokens: int | None = None,
|
| 54 |
+
temperature: float | None = None,
|
| 55 |
) -> str:
|
| 56 |
self.load()
|
| 57 |
assert self._model is not None
|
| 58 |
|
| 59 |
result = self._model(
|
| 60 |
prompt,
|
| 61 |
+
max_tokens=max_tokens or self._config.max_tokens,
|
| 62 |
+
temperature=temperature if temperature is not None else self._config.temperature,
|
| 63 |
echo=False,
|
| 64 |
)
|
| 65 |
return result["choices"][0]["text"].strip()
|
|
|
|
| 68 |
self,
|
| 69 |
messages: list[dict[str, str]],
|
| 70 |
*,
|
| 71 |
+
max_tokens: int | None = None,
|
| 72 |
+
temperature: float | None = None,
|
| 73 |
) -> str:
|
| 74 |
self.load()
|
| 75 |
assert self._model is not None
|
| 76 |
|
| 77 |
result = self._model.create_chat_completion(
|
| 78 |
messages=messages,
|
| 79 |
+
max_tokens=max_tokens or self._config.max_tokens,
|
| 80 |
+
temperature=temperature if temperature is not None else self._config.temperature,
|
| 81 |
)
|
| 82 |
return result["choices"][0]["message"]["content"].strip()
|
libs/inference/src/inference/transformers.py
CHANGED
|
@@ -1,72 +1,136 @@
|
|
| 1 |
-
import
|
| 2 |
|
| 3 |
-
from inference.
|
| 4 |
|
| 5 |
|
| 6 |
class TransformersBackend:
|
| 7 |
-
def __init__(self) -> None:
|
|
|
|
| 8 |
self._model = None
|
| 9 |
self._tokenizer = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
def load(self) -> None:
|
| 12 |
if self._model is not None:
|
| 13 |
return
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
try:
|
| 16 |
import torch
|
| 17 |
-
from transformers import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
except ImportError as exc:
|
| 19 |
raise ImportError(
|
| 20 |
-
"transformers backend requires
|
| 21 |
-
"Install with: uv sync --
|
| 22 |
) from exc
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
model_id,
|
| 30 |
-
torch_dtype=torch.float16 if device == "cuda" else torch.float32,
|
| 31 |
-
device_map="auto" if device == "cuda" else None,
|
| 32 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
if device == "cpu":
|
| 34 |
self._model.to(device)
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
prompt: str,
|
| 39 |
-
*,
|
| 40 |
-
max_tokens: int = 512,
|
| 41 |
-
temperature: float = 0.7,
|
| 42 |
-
) -> str:
|
| 43 |
self.load()
|
| 44 |
-
|
| 45 |
-
assert self._tokenizer is not None
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
generated = output[0][inputs["input_ids"].shape[-1] :]
|
| 57 |
-
return self._tokenizer.decode(generated, skip_special_tokens=True).strip()
|
| 58 |
|
| 59 |
-
def
|
| 60 |
self,
|
| 61 |
-
messages: list[dict[str,
|
| 62 |
*,
|
| 63 |
-
max_tokens: int =
|
| 64 |
-
temperature: float =
|
| 65 |
) -> str:
|
| 66 |
self.load()
|
| 67 |
assert self._model is not None
|
| 68 |
-
assert self._tokenizer is not None
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
if hasattr(self._tokenizer, "apply_chat_template"):
|
| 71 |
prompt = self._tokenizer.apply_chat_template(
|
| 72 |
messages,
|
|
@@ -76,14 +140,45 @@ class TransformersBackend:
|
|
| 76 |
else:
|
| 77 |
parts = []
|
| 78 |
for message in messages:
|
| 79 |
-
role = message["role"]
|
| 80 |
-
content = message["content"]
|
| 81 |
parts.append(f"{role}: {content}")
|
| 82 |
parts.append("assistant:")
|
| 83 |
prompt = "\n".join(parts)
|
| 84 |
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
|
| 3 |
+
from inference.config import ModelConfig
|
| 4 |
|
| 5 |
|
| 6 |
class TransformersBackend:
|
| 7 |
+
def __init__(self, config: ModelConfig) -> None:
|
| 8 |
+
self._config = config
|
| 9 |
self._model = None
|
| 10 |
self._tokenizer = None
|
| 11 |
+
self._processor = None
|
| 12 |
+
self._device_label: str | None = None
|
| 13 |
+
|
| 14 |
+
def _resolve_device(self):
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
if torch.cuda.is_available():
|
| 18 |
+
return "cuda", torch.float16, "auto"
|
| 19 |
+
return "cpu", torch.float32, None
|
| 20 |
|
| 21 |
def load(self) -> None:
|
| 22 |
if self._model is not None:
|
| 23 |
return
|
| 24 |
|
| 25 |
+
if not self._config.model_id:
|
| 26 |
+
raise ValueError(
|
| 27 |
+
f"Preset {self._config.key!r} requires model_id for transformers backend"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
try:
|
| 31 |
import torch
|
| 32 |
+
from transformers import (
|
| 33 |
+
AutoModelForCausalLM,
|
| 34 |
+
AutoModelForImageTextToText,
|
| 35 |
+
AutoProcessor,
|
| 36 |
+
AutoTokenizer,
|
| 37 |
+
)
|
| 38 |
except ImportError as exc:
|
| 39 |
raise ImportError(
|
| 40 |
+
"transformers backend requires torch and transformers. "
|
| 41 |
+
"Install with: uv sync --all-packages"
|
| 42 |
) from exc
|
| 43 |
|
| 44 |
+
device, torch_dtype, device_map = self._resolve_device()
|
| 45 |
+
self._device_label = (
|
| 46 |
+
f"cuda ({torch.cuda.get_device_name(0)})"
|
| 47 |
+
if device == "cuda"
|
| 48 |
+
else "cpu"
|
|
|
|
|
|
|
|
|
|
| 49 |
)
|
| 50 |
+
|
| 51 |
+
common_kwargs = {
|
| 52 |
+
"trust_remote_code": self._config.trust_remote_code,
|
| 53 |
+
}
|
| 54 |
+
model_kwargs = {
|
| 55 |
+
**common_kwargs,
|
| 56 |
+
"dtype": torch_dtype,
|
| 57 |
+
"device_map": device_map,
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if self._config.multimodal:
|
| 61 |
+
self._processor = AutoProcessor.from_pretrained(
|
| 62 |
+
self._config.model_id,
|
| 63 |
+
**common_kwargs,
|
| 64 |
+
)
|
| 65 |
+
self._model = AutoModelForImageTextToText.from_pretrained(
|
| 66 |
+
self._config.model_id,
|
| 67 |
+
**model_kwargs,
|
| 68 |
+
)
|
| 69 |
+
else:
|
| 70 |
+
self._tokenizer = AutoTokenizer.from_pretrained(
|
| 71 |
+
self._config.model_id,
|
| 72 |
+
**common_kwargs,
|
| 73 |
+
)
|
| 74 |
+
self._model = AutoModelForCausalLM.from_pretrained(
|
| 75 |
+
self._config.model_id,
|
| 76 |
+
**model_kwargs,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
if device == "cpu":
|
| 80 |
self._model.to(device)
|
| 81 |
|
| 82 |
+
@property
|
| 83 |
+
def device_label(self) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
self.load()
|
| 85 |
+
return self._device_label or "unknown"
|
|
|
|
| 86 |
|
| 87 |
+
def _normalize_messages(
|
| 88 |
+
self, messages: list[dict[str, str]]
|
| 89 |
+
) -> list[dict[str, object]]:
|
| 90 |
+
if not self._config.multimodal:
|
| 91 |
+
return messages
|
| 92 |
|
| 93 |
+
normalized: list[dict[str, object]] = []
|
| 94 |
+
for message in messages:
|
| 95 |
+
content = message["content"]
|
| 96 |
+
if isinstance(content, str):
|
| 97 |
+
content = [{"type": "text", "text": content}]
|
| 98 |
+
normalized.append({"role": message["role"], "content": content})
|
| 99 |
+
return normalized
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
def _generate_from_messages(
|
| 102 |
self,
|
| 103 |
+
messages: list[dict[str, object]],
|
| 104 |
*,
|
| 105 |
+
max_tokens: int | None = None,
|
| 106 |
+
temperature: float | None = None,
|
| 107 |
) -> str:
|
| 108 |
self.load()
|
| 109 |
assert self._model is not None
|
|
|
|
| 110 |
|
| 111 |
+
max_new_tokens = max_tokens or self._config.max_tokens
|
| 112 |
+
temp = self._config.temperature if temperature is None else temperature
|
| 113 |
+
|
| 114 |
+
if self._config.multimodal:
|
| 115 |
+
assert self._processor is not None
|
| 116 |
+
inputs = self._processor.apply_chat_template(
|
| 117 |
+
messages,
|
| 118 |
+
tokenize=True,
|
| 119 |
+
add_generation_prompt=True,
|
| 120 |
+
return_dict=True,
|
| 121 |
+
return_tensors="pt",
|
| 122 |
+
)
|
| 123 |
+
inputs = inputs.to(self._model.device)
|
| 124 |
+
output = self._model.generate(
|
| 125 |
+
**inputs,
|
| 126 |
+
max_new_tokens=max_new_tokens,
|
| 127 |
+
temperature=temp,
|
| 128 |
+
do_sample=temp > 0,
|
| 129 |
+
)
|
| 130 |
+
generated = output[0][inputs["input_ids"].shape[-1] :]
|
| 131 |
+
return self._processor.decode(generated, skip_special_tokens=True).strip()
|
| 132 |
+
|
| 133 |
+
assert self._tokenizer is not None
|
| 134 |
if hasattr(self._tokenizer, "apply_chat_template"):
|
| 135 |
prompt = self._tokenizer.apply_chat_template(
|
| 136 |
messages,
|
|
|
|
| 140 |
else:
|
| 141 |
parts = []
|
| 142 |
for message in messages:
|
| 143 |
+
role = str(message["role"])
|
| 144 |
+
content = str(message["content"])
|
| 145 |
parts.append(f"{role}: {content}")
|
| 146 |
parts.append("assistant:")
|
| 147 |
prompt = "\n".join(parts)
|
| 148 |
|
| 149 |
+
inputs = self._tokenizer(prompt, return_tensors="pt").to(self._model.device)
|
| 150 |
+
output = self._model.generate(
|
| 151 |
+
**inputs,
|
| 152 |
+
max_new_tokens=max_new_tokens,
|
| 153 |
+
temperature=temp,
|
| 154 |
+
do_sample=temp > 0,
|
| 155 |
+
)
|
| 156 |
+
generated = output[0][inputs["input_ids"].shape[-1] :]
|
| 157 |
+
return self._tokenizer.decode(generated, skip_special_tokens=True).strip()
|
| 158 |
|
| 159 |
+
def generate(
|
| 160 |
+
self,
|
| 161 |
+
prompt: str,
|
| 162 |
+
*,
|
| 163 |
+
max_tokens: int | None = None,
|
| 164 |
+
temperature: float | None = None,
|
| 165 |
+
) -> str:
|
| 166 |
+
return self.chat(
|
| 167 |
+
[{"role": "user", "content": prompt}],
|
| 168 |
+
max_tokens=max_tokens,
|
| 169 |
+
temperature=temperature,
|
| 170 |
+
)
|
| 171 |
|
| 172 |
+
def chat(
|
| 173 |
+
self,
|
| 174 |
+
messages: list[dict[str, str]],
|
| 175 |
+
*,
|
| 176 |
+
max_tokens: int | None = None,
|
| 177 |
+
temperature: float | None = None,
|
| 178 |
+
) -> str:
|
| 179 |
+
normalized = self._normalize_messages(messages)
|
| 180 |
+
return self._generate_from_messages(
|
| 181 |
+
normalized,
|
| 182 |
+
max_tokens=max_tokens,
|
| 183 |
+
temperature=temperature,
|
| 184 |
+
)
|
libs/inference/tests/test_config.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from inference.config import load_app_config
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_load_app_config_from_models_yaml(tmp_path, monkeypatch):
|
| 9 |
+
presets = tmp_path / "models.yaml"
|
| 10 |
+
presets.write_text(
|
| 11 |
+
"""
|
| 12 |
+
defaults:
|
| 13 |
+
active_model: demo
|
| 14 |
+
allow_model_switch: true
|
| 15 |
+
models:
|
| 16 |
+
demo:
|
| 17 |
+
label: Demo preset
|
| 18 |
+
backend: llama_cpp
|
| 19 |
+
model_repo: org/model-GGUF
|
| 20 |
+
model_file: demo.gguf
|
| 21 |
+
"""
|
| 22 |
+
)
|
| 23 |
+
monkeypatch.chdir(tmp_path)
|
| 24 |
+
monkeypatch.delenv("ACTIVE_MODEL", raising=False)
|
| 25 |
+
|
| 26 |
+
config = load_app_config()
|
| 27 |
+
|
| 28 |
+
assert config.active_model == "demo"
|
| 29 |
+
assert config.allow_model_switch is True
|
| 30 |
+
assert config.get_model("demo").model_repo == "org/model-GGUF"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_legacy_env_overrides_active_preset(tmp_path, monkeypatch):
|
| 34 |
+
presets = tmp_path / "models.yaml"
|
| 35 |
+
presets.write_text(
|
| 36 |
+
"""
|
| 37 |
+
defaults:
|
| 38 |
+
active_model: demo
|
| 39 |
+
models:
|
| 40 |
+
demo:
|
| 41 |
+
label: Demo
|
| 42 |
+
backend: llama_cpp
|
| 43 |
+
model_repo: org/original
|
| 44 |
+
model_file: original.gguf
|
| 45 |
+
"""
|
| 46 |
+
)
|
| 47 |
+
monkeypatch.chdir(tmp_path)
|
| 48 |
+
monkeypatch.setenv("MODEL_REPO", "org/override")
|
| 49 |
+
monkeypatch.setenv("MODEL_FILE", "override.gguf")
|
| 50 |
+
|
| 51 |
+
model = load_app_config().get_model("demo")
|
| 52 |
+
|
| 53 |
+
assert model.model_repo == "org/override"
|
| 54 |
+
assert model.model_file == "override.gguf"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_resolve_relative_model_path(tmp_path, monkeypatch):
|
| 58 |
+
local_dir = tmp_path / "gemma_merged_model"
|
| 59 |
+
local_dir.mkdir()
|
| 60 |
+
presets = tmp_path / "models.yaml"
|
| 61 |
+
presets.write_text(
|
| 62 |
+
f"""
|
| 63 |
+
defaults:
|
| 64 |
+
active_model: local
|
| 65 |
+
models:
|
| 66 |
+
local:
|
| 67 |
+
label: Local merged
|
| 68 |
+
backend: transformers
|
| 69 |
+
model_id: ./{local_dir.name}
|
| 70 |
+
"""
|
| 71 |
+
)
|
| 72 |
+
monkeypatch.chdir(tmp_path)
|
| 73 |
+
|
| 74 |
+
model = load_app_config().get_model("local")
|
| 75 |
+
|
| 76 |
+
assert model.model_id == str(local_dir.resolve())
|
models.yaml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Model preset registry for dev and Hugging Face Space.
|
| 2 |
+
# Select active preset with ACTIVE_MODEL; override any field via .env (see .env.example).
|
| 3 |
+
|
| 4 |
+
defaults:
|
| 5 |
+
active_model: minicpm-v-4.6
|
| 6 |
+
# Dev: set ALLOW_MODEL_SWITCH=true in .env to expose a dropdown in Gradio.
|
| 7 |
+
# Space: keep false so visitors use one pinned model.
|
| 8 |
+
allow_model_switch: false
|
| 9 |
+
|
| 10 |
+
models:
|
| 11 |
+
minicpm-v-4.6:
|
| 12 |
+
label: MiniCPM-V 4.6 (Transformers, ~0.8B, default)
|
| 13 |
+
backend: transformers
|
| 14 |
+
model_id: openbmb/MiniCPM-V-4.6
|
| 15 |
+
trust_remote_code: true
|
| 16 |
+
multimodal: true
|
| 17 |
+
|
| 18 |
+
qwen3b-gguf:
|
| 19 |
+
label: Qwen 2.5 3B Instruct (GGUF)
|
| 20 |
+
backend: llama_cpp
|
| 21 |
+
model_repo: Qwen/Qwen2.5-3B-Instruct-GGUF
|
| 22 |
+
model_file: qwen2.5-3b-instruct-q4_k_m.gguf
|
| 23 |
+
n_ctx: 4096
|
| 24 |
+
n_gpu_layers: 0
|
| 25 |
+
|
| 26 |
+
llama32-3b-gguf:
|
| 27 |
+
label: Llama 3.2 3B Instruct (GGUF)
|
| 28 |
+
backend: llama_cpp
|
| 29 |
+
model_repo: bartowski/Llama-3.2-3B-Instruct-GGUF
|
| 30 |
+
model_file: Llama-3.2-3B-Instruct-Q4_K_M.gguf
|
| 31 |
+
n_ctx: 4096
|
| 32 |
+
n_gpu_layers: 0
|
| 33 |
+
|
| 34 |
+
minicpm5-1b:
|
| 35 |
+
label: MiniCPM5 1B (Transformers)
|
| 36 |
+
backend: transformers
|
| 37 |
+
model_id: openbmb/MiniCPM5-1B
|
| 38 |
+
trust_remote_code: true
|
| 39 |
+
|
| 40 |
+
gemma4-e2b-mobile:
|
| 41 |
+
label: Gemma 4 E2B IT QAT Mobile (Transformers)
|
| 42 |
+
backend: transformers
|
| 43 |
+
model_id: google/gemma-4-E2B-it-qat-mobile-transformers
|
| 44 |
+
trust_remote_code: true
|
| 45 |
+
|
| 46 |
+
gemma-merged-local:
|
| 47 |
+
label: Fine-tuned merged model (local path)
|
| 48 |
+
backend: transformers
|
| 49 |
+
model_id: ./gemma_merged_model
|
| 50 |
+
|
| 51 |
+
gemma-lora-local:
|
| 52 |
+
label: Fine-tuned LoRA adapter (local path)
|
| 53 |
+
backend: transformers
|
| 54 |
+
model_id: ./gemma_finetuned_model
|
notebook/gemma-finetune.ipynb
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"metadata": {
|
| 6 |
+
"colab_type": "code",
|
| 7 |
+
"execution": {
|
| 8 |
+
"iopub.execute_input": "2026-06-07T13:53:33.681800Z",
|
| 9 |
+
"iopub.status.busy": "2026-06-07T13:53:33.681410Z",
|
| 10 |
+
"iopub.status.idle": "2026-06-07T13:53:47.949186Z",
|
| 11 |
+
"shell.execute_reply": "2026-06-07T13:53:47.948138Z",
|
| 12 |
+
"shell.execute_reply.started": "2026-06-07T13:53:33.681762Z"
|
| 13 |
+
},
|
| 14 |
+
"trusted": true
|
| 15 |
+
},
|
| 16 |
+
"source": [
|
| 17 |
+
"!pip install -U transformers"
|
| 18 |
+
],
|
| 19 |
+
"execution_count": null,
|
| 20 |
+
"outputs": []
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"cell_type": "markdown",
|
| 24 |
+
"metadata": {
|
| 25 |
+
"colab_type": "text"
|
| 26 |
+
},
|
| 27 |
+
"source": [
|
| 28 |
+
"## Local Inference on GPU \n",
|
| 29 |
+
"Model page: https://huggingface.co/google/gemma-4-E2B-it-qat-mobile-transformers\n",
|
| 30 |
+
"\n",
|
| 31 |
+
"⚠️ If the generated code snippets do not work, please open an issue on either the [model repo](https://huggingface.co/google/gemma-4-E2B-it-qat-mobile-transformers)\n",
|
| 32 |
+
"\t\t\tand/or on [huggingface.js](https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/src/model-libraries-snippets.ts) 🙏"
|
| 33 |
+
]
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"cell_type": "code",
|
| 37 |
+
"metadata": {},
|
| 38 |
+
"source": [],
|
| 39 |
+
"execution_count": null,
|
| 40 |
+
"outputs": []
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"cell_type": "code",
|
| 44 |
+
"metadata": {
|
| 45 |
+
"execution": {
|
| 46 |
+
"iopub.execute_input": "2026-06-07T14:00:05.743785Z",
|
| 47 |
+
"iopub.status.busy": "2026-06-07T14:00:05.742974Z",
|
| 48 |
+
"iopub.status.idle": "2026-06-07T14:00:05.748408Z",
|
| 49 |
+
"shell.execute_reply": "2026-06-07T14:00:05.747778Z",
|
| 50 |
+
"shell.execute_reply.started": "2026-06-07T14:00:05.743756Z"
|
| 51 |
+
},
|
| 52 |
+
"trusted": true
|
| 53 |
+
},
|
| 54 |
+
"source": [
|
| 55 |
+
"# Load model directly\n",
|
| 56 |
+
"from transformers import AutoModel\n",
|
| 57 |
+
"import torch\n",
|
| 58 |
+
"\n",
|
| 59 |
+
"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
|
| 60 |
+
"\n",
|
| 61 |
+
"model_name_gemma=\"google/gemma-4-E2B-it-qat-mobile-transformers\"\n",
|
| 62 |
+
"model_name=\"openbmb/MiniCPM5-1B\"\n",
|
| 63 |
+
"model_openbmb=\"openbmb/MiniCPM5-1B\""
|
| 64 |
+
],
|
| 65 |
+
"execution_count": null,
|
| 66 |
+
"outputs": []
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"cell_type": "code",
|
| 70 |
+
"metadata": {
|
| 71 |
+
"colab_type": "code",
|
| 72 |
+
"execution": {
|
| 73 |
+
"iopub.execute_input": "2026-06-07T13:58:21.771701Z",
|
| 74 |
+
"iopub.status.busy": "2026-06-07T13:58:21.770950Z",
|
| 75 |
+
"iopub.status.idle": "2026-06-07T13:58:42.868203Z",
|
| 76 |
+
"shell.execute_reply": "2026-06-07T13:58:42.867564Z",
|
| 77 |
+
"shell.execute_reply.started": "2026-06-07T13:58:21.771669Z"
|
| 78 |
+
},
|
| 79 |
+
"trusted": true
|
| 80 |
+
},
|
| 81 |
+
"source": [
|
| 82 |
+
"\n",
|
| 83 |
+
"model = AutoModel.from_pretrained(model_name, dtype=\"auto\").to(device)"
|
| 84 |
+
],
|
| 85 |
+
"execution_count": null,
|
| 86 |
+
"outputs": []
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"cell_type": "code",
|
| 90 |
+
"metadata": {
|
| 91 |
+
"execution": {
|
| 92 |
+
"iopub.execute_input": "2026-06-07T14:00:17.480009Z",
|
| 93 |
+
"iopub.status.busy": "2026-06-07T14:00:17.479002Z",
|
| 94 |
+
"iopub.status.idle": "2026-06-07T14:00:28.838734Z",
|
| 95 |
+
"shell.execute_reply": "2026-06-07T14:00:28.837707Z",
|
| 96 |
+
"shell.execute_reply.started": "2026-06-07T14:00:17.479974Z"
|
| 97 |
+
},
|
| 98 |
+
"trusted": true
|
| 99 |
+
},
|
| 100 |
+
"source": [
|
| 101 |
+
"\n",
|
| 102 |
+
"from transformers import AutoTokenizer, AutoModelForCausalLM\n",
|
| 103 |
+
"\n",
|
| 104 |
+
"tokenizer = AutoTokenizer.from_pretrained(\"openbmb/MiniCPM5-1B\")\n",
|
| 105 |
+
"model = AutoModelForCausalLM.from_pretrained(\"openbmb/MiniCPM5-1B\")\n"
|
| 106 |
+
],
|
| 107 |
+
"execution_count": null,
|
| 108 |
+
"outputs": []
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"cell_type": "code",
|
| 112 |
+
"metadata": {
|
| 113 |
+
"execution": {
|
| 114 |
+
"iopub.execute_input": "2026-06-07T14:00:51.808940Z",
|
| 115 |
+
"iopub.status.busy": "2026-06-07T14:00:51.808046Z",
|
| 116 |
+
"iopub.status.idle": "2026-06-07T14:00:59.481653Z",
|
| 117 |
+
"shell.execute_reply": "2026-06-07T14:00:59.480980Z",
|
| 118 |
+
"shell.execute_reply.started": "2026-06-07T14:00:51.808892Z"
|
| 119 |
+
},
|
| 120 |
+
"trusted": true
|
| 121 |
+
},
|
| 122 |
+
"source": [
|
| 123 |
+
"messages = [\n",
|
| 124 |
+
" {\"role\": \"user\", \"content\": \"Who are you?\"},\n",
|
| 125 |
+
"]\n",
|
| 126 |
+
"inputs = tokenizer.apply_chat_template(\n",
|
| 127 |
+
"\tmessages,\n",
|
| 128 |
+
"\tadd_generation_prompt=True,\n",
|
| 129 |
+
"\ttokenize=True,\n",
|
| 130 |
+
"\treturn_dict=True,\n",
|
| 131 |
+
"\treturn_tensors=\"pt\",\n",
|
| 132 |
+
").to(model.device)\n",
|
| 133 |
+
"\n",
|
| 134 |
+
"outputs = model.generate(**inputs, max_new_tokens=120)\n",
|
| 135 |
+
"print(tokenizer.decode(outputs[0][inputs[\"input_ids\"].shape[-1]:]))"
|
| 136 |
+
],
|
| 137 |
+
"execution_count": null,
|
| 138 |
+
"outputs": []
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"cell_type": "code",
|
| 142 |
+
"metadata": {
|
| 143 |
+
"execution": {
|
| 144 |
+
"iopub.execute_input": "2026-06-07T13:59:02.169453Z",
|
| 145 |
+
"iopub.status.busy": "2026-06-07T13:59:02.168479Z",
|
| 146 |
+
"iopub.status.idle": "2026-06-07T13:59:04.394628Z",
|
| 147 |
+
"shell.execute_reply": "2026-06-07T13:59:04.393301Z",
|
| 148 |
+
"shell.execute_reply.started": "2026-06-07T13:59:02.169418Z"
|
| 149 |
+
},
|
| 150 |
+
"trusted": true
|
| 151 |
+
},
|
| 152 |
+
"source": [
|
| 153 |
+
"# from transformers import AutoTokenizer, pipeline\n",
|
| 154 |
+
"\n",
|
| 155 |
+
"# tokenizer = AutoTokenizer.from_pretrained(model_name)\n",
|
| 156 |
+
"\n",
|
| 157 |
+
"# pipeline = pipeline(\n",
|
| 158 |
+
"# \"text-generation\",\n",
|
| 159 |
+
"# model=model,\n",
|
| 160 |
+
"# tokenizer=tokenizer,\n",
|
| 161 |
+
"# model_kwargs={\"torch_dtype\": \"auto\"},\n",
|
| 162 |
+
"# )\n",
|
| 163 |
+
"\n",
|
| 164 |
+
"# # Example of generating chat/text\n",
|
| 165 |
+
"# messages = [\n",
|
| 166 |
+
"# {\"role\": \"user\", \"content\": \"What is your favorite color?\"},\n",
|
| 167 |
+
"# ]\n",
|
| 168 |
+
"\n",
|
| 169 |
+
"# prompt = pipeline.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n",
|
| 170 |
+
"# outputs = pipeline(prompt, max_new_tokens=256, do_sample=True, temperature=0.7, top_k=50, top_p=0.95)\n",
|
| 171 |
+
"# print(outputs[0][\"generated_text\"])"
|
| 172 |
+
],
|
| 173 |
+
"execution_count": null,
|
| 174 |
+
"outputs": []
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"cell_type": "markdown",
|
| 178 |
+
"metadata": {},
|
| 179 |
+
"source": [
|
| 180 |
+
"# Finetuning"
|
| 181 |
+
]
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"cell_type": "code",
|
| 185 |
+
"metadata": {
|
| 186 |
+
"execution": {
|
| 187 |
+
"iopub.execute_input": "2026-06-07T14:01:09.304174Z",
|
| 188 |
+
"iopub.status.busy": "2026-06-07T14:01:09.303286Z",
|
| 189 |
+
"iopub.status.idle": "2026-06-07T14:01:32.898893Z",
|
| 190 |
+
"shell.execute_reply": "2026-06-07T14:01:32.897942Z",
|
| 191 |
+
"shell.execute_reply.started": "2026-06-07T14:01:09.304140Z"
|
| 192 |
+
},
|
| 193 |
+
"trusted": true
|
| 194 |
+
},
|
| 195 |
+
"source": [
|
| 196 |
+
"# Install necessary libraries for finetuning (if not already installed)\n",
|
| 197 |
+
"!pip install -q -U accelerate peft bitsandbytes transformers trl datasets\n",
|
| 198 |
+
"\n",
|
| 199 |
+
"import torch\n",
|
| 200 |
+
"from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig\n",
|
| 201 |
+
"from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training\n",
|
| 202 |
+
"from trl import SFTTrainer\n",
|
| 203 |
+
"from datasets import Dataset\n",
|
| 204 |
+
"\n",
|
| 205 |
+
"# --- 1. Load Model and Tokenizer for Finetuning with QLoRA ---\n",
|
| 206 |
+
"# The `model` variable from a previous cell is AutoModel, for finetuning\n",
|
| 207 |
+
"# we typically need AutoModelForCausalLM with BitsAndBytesConfig.\n",
|
| 208 |
+
"# Let's reload it for clarity in this finetuning example.\n",
|
| 209 |
+
"\n",
|
| 210 |
+
"# model_id = \"google/gemma-4-E2B-it-qat-mobile-transformers\"\n",
|
| 211 |
+
"model_id = model_openbmb\n",
|
| 212 |
+
"\n",
|
| 213 |
+
"# Configure 4-bit quantization\n",
|
| 214 |
+
"bnb_config = BitsAndBytesConfig(\n",
|
| 215 |
+
" load_in_4bit=True,\n",
|
| 216 |
+
" bnb_4bit_quant_type=\"nf4\",\n",
|
| 217 |
+
" bnb_4bit_compute_dtype=torch.bfloat16,\n",
|
| 218 |
+
" bnb_4bit_use_double_quant=False,\n",
|
| 219 |
+
")\n",
|
| 220 |
+
"\n",
|
| 221 |
+
"# Load the model with 4-bit quantization\n",
|
| 222 |
+
"# Assuming 'model' from earlier cell is a suitable base for this if not reloaded\n",
|
| 223 |
+
"# If you've already loaded it as `AutoModel`, you might need to ensure it's `AutoModelForCausalLM`\n",
|
| 224 |
+
"# and can be quantizied. For a robust finetuning example, it's safer to load it here again.\n",
|
| 225 |
+
"\n",
|
| 226 |
+
"tuned_model = AutoModelForCausalLM.from_pretrained(\n",
|
| 227 |
+
" model_id,\n",
|
| 228 |
+
" quantization_config=bnb_config,\n",
|
| 229 |
+
" device_map=\"auto\",\n",
|
| 230 |
+
")\n",
|
| 231 |
+
"\n",
|
| 232 |
+
"tuned_model.config.use_cache = False\n",
|
| 233 |
+
"tuned_model.config.pretraining_tp = 1\n",
|
| 234 |
+
"\n",
|
| 235 |
+
"tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)\n",
|
| 236 |
+
"tokenizer.pad_token = tokenizer.eos_token\n"
|
| 237 |
+
],
|
| 238 |
+
"execution_count": null,
|
| 239 |
+
"outputs": []
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"cell_type": "code",
|
| 243 |
+
"metadata": {},
|
| 244 |
+
"source": [
|
| 245 |
+
"\n",
|
| 246 |
+
"# Prepare model for k-bit training\n",
|
| 247 |
+
"tuned_model = prepare_model_for_kbit_training(tuned_model)\n",
|
| 248 |
+
"\n",
|
| 249 |
+
"# --- 2. Configure LoRA ---\n",
|
| 250 |
+
"lora_config = LoraConfig(\n",
|
| 251 |
+
" r=16, # LoRA attention dimension\n",
|
| 252 |
+
" lora_alpha=16, # Alpha parameter for LoRA scaling\n",
|
| 253 |
+
" target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\"], # Target all linear layers\n",
|
| 254 |
+
" lora_dropout=0.05, # Dropout probability for LoRA layers\n",
|
| 255 |
+
" bias=\"none\", # Only add bias to the LoRA layers\n",
|
| 256 |
+
" task_type=\"CAUSAL_LM\", # Task type for causal language modeling\n",
|
| 257 |
+
")\n",
|
| 258 |
+
"\n",
|
| 259 |
+
"# Do NOT call get_peft_model() here — SFTTrainer wraps the model when peft_config is passed.\n",
|
| 260 |
+
"# tuned_model = get_peft_model(tuned_model, lora_config)\n",
|
| 261 |
+
"\n",
|
| 262 |
+
"# --- 3. Prepare a Sample Dataset ---\n",
|
| 263 |
+
"# For a real-world scenario, you would load your own dataset using `load_dataset`\n",
|
| 264 |
+
"# from the `datasets` library and format it appropriately.\n",
|
| 265 |
+
"# This is a simple dummy dataset for demonstration.\n",
|
| 266 |
+
"\n",
|
| 267 |
+
"# Example instruction tuning dataset format\n",
|
| 268 |
+
"data = {\n",
|
| 269 |
+
" \"text\": [\n",
|
| 270 |
+
" \"<start_of_turn>user\\nWhat is the capital of France?<end_of_turn>\\n<start_of_turn>model\\nParis is the capital of France.<end_of_turn>\",\n",
|
| 271 |
+
" \"<start_of_turn>user\\nSuggest a healthy snack.\\n<end_of_turn>\\n<start_of_turn>model\\nAlmonds or a piece of fruit like an apple are great healthy snack options.<end_of_turn>\",\n",
|
| 272 |
+
" \"<start_of_turn>user\\nExplain the concept of photosynthesis.\\n<end_of_turn>\\n<start_of_turn>model\\nPhotosynthesis is the process by which green plants and some other organisms convert light energy into chemical energy.<end_of_turn>\"\n",
|
| 273 |
+
" ]\n",
|
| 274 |
+
"}\n",
|
| 275 |
+
"\n",
|
| 276 |
+
"dataset = Dataset.from_dict(data)\n",
|
| 277 |
+
"\n",
|
| 278 |
+
"# --- 4. Define Training Arguments ---\n",
|
| 279 |
+
"from transformers import TrainingArguments\n",
|
| 280 |
+
"\n",
|
| 281 |
+
"training_args = TrainingArguments(\n",
|
| 282 |
+
" output_dir=\"./gemma_finetuned\", # Output directory for checkpoints and logs\n",
|
| 283 |
+
" num_train_epochs=1, # Number of training epochs\n",
|
| 284 |
+
" per_device_train_batch_size=2, # Batch size per GPU/CPU for training\n",
|
| 285 |
+
" gradient_accumulation_steps=2, # Number of updates steps to accumulate before performing a backward/update pass\n",
|
| 286 |
+
" optim=\"paged_adamw_8bit\", # Optimizer to use\n",
|
| 287 |
+
" save_steps=100, # Save checkpoint every X updates steps\n",
|
| 288 |
+
" logging_steps=10, # Log every X updates steps\n",
|
| 289 |
+
" learning_rate=2e-4, # Initial learning rate for AdamW optimizer\n",
|
| 290 |
+
" weight_decay=0.001, # Weight decay for AdamW\n",
|
| 291 |
+
" fp16=False, # Must match bnb_4bit_compute_dtype (bf16 below)\n",
|
| 292 |
+
" bf16=True, # Use bf16 when bnb_4bit_compute_dtype=torch.bfloat16\n",
|
| 293 |
+
" max_grad_norm=0.3, # Max gradient norm\n",
|
| 294 |
+
" max_steps=-1, # Don't limit training by steps, use epochs\n",
|
| 295 |
+
" warmup_ratio=0.03, # Ratio of total steps for a linear warmup from 0 to learning_rate\n",
|
| 296 |
+
" # group_by_length=True, # Group sequences of roughly the same length together to speed up training\n",
|
| 297 |
+
" lr_scheduler_type=\"constant\", # Learning rate scheduler type\n",
|
| 298 |
+
" report_to=\"none\" # Disable reporting to any tracking service\n",
|
| 299 |
+
")\n",
|
| 300 |
+
"\n",
|
| 301 |
+
"# --- 5. Initialize and Run SFTTrainer ---\n",
|
| 302 |
+
"\n",
|
| 303 |
+
"trainer = SFTTrainer(\n",
|
| 304 |
+
" model=tuned_model, # plain (non-PEFT) base model\n",
|
| 305 |
+
" train_dataset=dataset,\n",
|
| 306 |
+
" peft_config=lora_config, # SFTTrainer applies LoRA internally\n",
|
| 307 |
+
" # dataset_text_field=\"text\", # Name of the column containing the text data\n",
|
| 308 |
+
" # tokenizer=tokenizer,\n",
|
| 309 |
+
" args=training_args,\n",
|
| 310 |
+
" # packing=False, # Whether to pack multiple short examples into one longer sequence to improve efficiency\n",
|
| 311 |
+
" # max_seq_length=512, # Max sequence length to use for training\n",
|
| 312 |
+
")\n",
|
| 313 |
+
"\n",
|
| 314 |
+
"print(\"Starting finetuning...\")\n",
|
| 315 |
+
"trainer.train()\n",
|
| 316 |
+
"print(\"Finetuning complete!\")\n",
|
| 317 |
+
"\n",
|
| 318 |
+
"# --- 6. Save the LoRA adapter ---\n",
|
| 319 |
+
"trainer.model.save_pretrained(\"./gemma_finetuned_model\")\n",
|
| 320 |
+
"tokenizer.save_pretrained(\"./gemma_finetuned_model\")\n",
|
| 321 |
+
"\n",
|
| 322 |
+
"# --- 7. (Optional) Merge LoRA adapters for inference ---\n",
|
| 323 |
+
"# Merge in-memory from the trained model (avoids AutoPeft reload + torchao version issues).\n",
|
| 324 |
+
"merged_model = trainer.model.merge_and_unload()\n",
|
| 325 |
+
"merged_model.save_pretrained(\"gemma_merged_model\", safe_serialization=True)\n",
|
| 326 |
+
"tokenizer.save_pretrained(\"gemma_merged_model\")\n",
|
| 327 |
+
"\n",
|
| 328 |
+
"# If you need to reload the adapter from disk later instead, upgrade torchao first:\n",
|
| 329 |
+
"# !pip install -U \"torchao>=0.16.0\"\n",
|
| 330 |
+
"# from peft import PeftModel\n",
|
| 331 |
+
"# base_model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config, device_map=\"auto\")\n",
|
| 332 |
+
"# peft_model = PeftModel.from_pretrained(base_model, \"./gemma_finetuned_model\")\n",
|
| 333 |
+
""
|
| 334 |
+
],
|
| 335 |
+
"execution_count": null,
|
| 336 |
+
"outputs": []
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"cell_type": "code",
|
| 340 |
+
"metadata": {
|
| 341 |
+
"execution": {
|
| 342 |
+
"iopub.execute_input": "2026-06-07T14:02:33.440153Z",
|
| 343 |
+
"iopub.status.busy": "2026-06-07T14:02:33.439372Z",
|
| 344 |
+
"iopub.status.idle": "2026-06-07T14:02:36.463743Z",
|
| 345 |
+
"shell.execute_reply": "2026-06-07T14:02:36.462765Z",
|
| 346 |
+
"shell.execute_reply.started": "2026-06-07T14:02:33.440118Z"
|
| 347 |
+
},
|
| 348 |
+
"trusted": true
|
| 349 |
+
},
|
| 350 |
+
"source": [
|
| 351 |
+
"\n",
|
| 352 |
+
"# Prepare model for k-bit training\n",
|
| 353 |
+
"tuned_model = prepare_model_for_kbit_training(tuned_model)\n",
|
| 354 |
+
"\n",
|
| 355 |
+
"# --- 2. Configure LoRA ---\n",
|
| 356 |
+
"lora_config = LoraConfig(\n",
|
| 357 |
+
" r=16, # LoRA attention dimension\n",
|
| 358 |
+
" lora_alpha=16, # Alpha parameter for LoRA scaling\n",
|
| 359 |
+
" target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\"], # Target all linear layers\n",
|
| 360 |
+
" lora_dropout=0.05, # Dropout probability for LoRA layers\n",
|
| 361 |
+
" bias=\"none\", # Only add bias to the LoRA layers\n",
|
| 362 |
+
" task_type=\"CAUSAL_LM\", # Task type for causal language modeling\n",
|
| 363 |
+
")\n",
|
| 364 |
+
"\n",
|
| 365 |
+
"# Do NOT call get_peft_model() here — SFTTrainer wraps the model when peft_config is passed.\n",
|
| 366 |
+
"# tuned_model = get_peft_model(tuned_model, lora_config)\n",
|
| 367 |
+
"\n",
|
| 368 |
+
"# --- 3. Prepare a Sample Dataset ---\n",
|
| 369 |
+
"# For a real-world scenario, you would load your own dataset using `load_dataset`\n",
|
| 370 |
+
"# from the `datasets` library and format it appropriately.\n",
|
| 371 |
+
"# This is a simple dummy dataset for demonstration.\n",
|
| 372 |
+
"\n",
|
| 373 |
+
"# Example instruction tuning dataset format\n",
|
| 374 |
+
"data = {\n",
|
| 375 |
+
" \"text\": [\n",
|
| 376 |
+
" \"<start_of_turn>user\\nWhat is the capital of France?<end_of_turn>\\n<start_of_turn>model\\nParis is the capital of France.<end_of_turn>\",\n",
|
| 377 |
+
" \"<start_of_turn>user\\nSuggest a healthy snack.\\n<end_of_turn>\\n<start_of_turn>model\\nAlmonds or a piece of fruit like an apple are great healthy snack options.<end_of_turn>\",\n",
|
| 378 |
+
" \"<start_of_turn>user\\nExplain the concept of photosynthesis.\\n<end_of_turn>\\n<start_of_turn>model\\nPhotosynthesis is the process by which green plants and some other organisms convert light energy into chemical energy.<end_of_turn>\"\n",
|
| 379 |
+
" ]\n",
|
| 380 |
+
"}\n",
|
| 381 |
+
"\n",
|
| 382 |
+
"dataset = Dataset.from_dict(data)\n",
|
| 383 |
+
"\n",
|
| 384 |
+
"# --- 4. Define Training Arguments ---\n",
|
| 385 |
+
"from transformers import TrainingArguments\n",
|
| 386 |
+
"\n",
|
| 387 |
+
"training_args = TrainingArguments(\n",
|
| 388 |
+
" output_dir=\"./gemma_finetuned\", # Output directory for checkpoints and logs\n",
|
| 389 |
+
" num_train_epochs=1, # Number of training epochs\n",
|
| 390 |
+
" per_device_train_batch_size=2, # Batch size per GPU/CPU for training\n",
|
| 391 |
+
" gradient_accumulation_steps=2, # Number of updates steps to accumulate before performing a backward/update pass\n",
|
| 392 |
+
" optim=\"paged_adamw_8bit\", # Optimizer to use\n",
|
| 393 |
+
" save_steps=100, # Save checkpoint every X updates steps\n",
|
| 394 |
+
" logging_steps=10, # Log every X updates steps\n",
|
| 395 |
+
" learning_rate=2e-4, # Initial learning rate for AdamW optimizer\n",
|
| 396 |
+
" weight_decay=0.001, # Weight decay for AdamW\n",
|
| 397 |
+
" fp16=True, # Enable mixed precision training\n",
|
| 398 |
+
" bf16=False, # Disable BF16 if using FP16\n",
|
| 399 |
+
" max_grad_norm=0.3, # Max gradient norm\n",
|
| 400 |
+
" max_steps=-1, # Don't limit training by steps, use epochs\n",
|
| 401 |
+
" warmup_ratio=0.03, # Ratio of total steps for a linear warmup from 0 to learning_rate\n",
|
| 402 |
+
" # group_by_length=True, # Group sequences of roughly the same length together to speed up training\n",
|
| 403 |
+
" lr_scheduler_type=\"constant\", # Learning rate scheduler type\n",
|
| 404 |
+
" report_to=\"none\" # Disable reporting to any tracking service\n",
|
| 405 |
+
")\n",
|
| 406 |
+
"\n",
|
| 407 |
+
"# --- 5. Initialize and Run SFTTrainer ---\n",
|
| 408 |
+
"\n",
|
| 409 |
+
"trainer = SFTTrainer(\n",
|
| 410 |
+
" model=tuned_model, # plain (non-PEFT) base model\n",
|
| 411 |
+
" train_dataset=dataset,\n",
|
| 412 |
+
" peft_config=lora_config, # SFTTrainer applies LoRA internally\n",
|
| 413 |
+
" # dataset_text_field=\"text\", # Name of the column containing the text data\n",
|
| 414 |
+
" # tokenizer=tokenizer,\n",
|
| 415 |
+
" args=training_args,\n",
|
| 416 |
+
" # packing=False, # Whether to pack multiple short examples into one longer sequence to improve efficiency\n",
|
| 417 |
+
" # max_seq_length=512, # Max sequence length to use for training\n",
|
| 418 |
+
")\n",
|
| 419 |
+
"\n",
|
| 420 |
+
"print(\"Starting finetuning...\")\n",
|
| 421 |
+
"trainer.train()\n",
|
| 422 |
+
"print(\"Finetuning complete!\")\n",
|
| 423 |
+
"\n",
|
| 424 |
+
"# --- 6. (Optional) Save the finetuned model ---\n",
|
| 425 |
+
"# trainer.save_model(\"./gemma_finetuned_model\")\n",
|
| 426 |
+
"\n",
|
| 427 |
+
"# --- 7. (Optional) Merge LoRA adapters with the base model for inference ---\n",
|
| 428 |
+
"# from peft import AutoPeftModelForCausalLM\n",
|
| 429 |
+
"# merged_model = AutoPeftModelForCausalLM.from_pretrained(\n",
|
| 430 |
+
"# \"./gemma_finetuned_model\",\n",
|
| 431 |
+
"# device_map=\"auto\",\n",
|
| 432 |
+
"# torch_dtype=torch.bfloat16 # or torch.float16 depending on your hardware\n",
|
| 433 |
+
"# )\n",
|
| 434 |
+
"# merged_model.save_pretrained(\"gemma_merged_model\", safe_serialization=True)\n",
|
| 435 |
+
"# tokenizer.save_pretrained(\"gemma_merged_model\")\n"
|
| 436 |
+
],
|
| 437 |
+
"execution_count": null,
|
| 438 |
+
"outputs": []
|
| 439 |
+
},
|
| 440 |
+
{
|
| 441 |
+
"cell_type": "code",
|
| 442 |
+
"metadata": {
|
| 443 |
+
"trusted": true
|
| 444 |
+
},
|
| 445 |
+
"source": [
|
| 446 |
+
"# Duplicate cell removed — run the finetuning cell above.\n"
|
| 447 |
+
],
|
| 448 |
+
"execution_count": null,
|
| 449 |
+
"outputs": []
|
| 450 |
+
},
|
| 451 |
+
{
|
| 452 |
+
"cell_type": "code",
|
| 453 |
+
"metadata": {},
|
| 454 |
+
"source": [
|
| 455 |
+
"# Alternative (pick ONE approach, not both):\n",
|
| 456 |
+
"#\n",
|
| 457 |
+
"# Option A — let SFTTrainer apply LoRA (used in the cell above):\n",
|
| 458 |
+
"# tuned_model = prepare_model_for_kbit_training(tuned_model)\n",
|
| 459 |
+
"# trainer = SFTTrainer(model=tuned_model, peft_config=lora_config, ...)\n",
|
| 460 |
+
"#\n",
|
| 461 |
+
"# Option B — wrap manually, omit peft_config from SFTTrainer:\n",
|
| 462 |
+
"# tuned_model = get_peft_model(tuned_model, lora_config)\n",
|
| 463 |
+
"# trainer = SFTTrainer(model=tuned_model, ...) # no peft_config\n",
|
| 464 |
+
"\n"
|
| 465 |
+
],
|
| 466 |
+
"execution_count": null,
|
| 467 |
+
"outputs": []
|
| 468 |
+
}
|
| 469 |
+
],
|
| 470 |
+
"metadata": {
|
| 471 |
+
"accelerator": "GPU",
|
| 472 |
+
"colab": {
|
| 473 |
+
"gpuType": "T4",
|
| 474 |
+
"machine_shape": "hm"
|
| 475 |
+
},
|
| 476 |
+
"kaggle": {
|
| 477 |
+
"accelerator": "gpu"
|
| 478 |
+
},
|
| 479 |
+
"kernelspec": {
|
| 480 |
+
"display_name": "Python 3",
|
| 481 |
+
"language": "python",
|
| 482 |
+
"name": "python3"
|
| 483 |
+
},
|
| 484 |
+
"language_info": {
|
| 485 |
+
"codemirror_mode": {
|
| 486 |
+
"name": "ipython",
|
| 487 |
+
"version": 3
|
| 488 |
+
},
|
| 489 |
+
"file_extension": ".py",
|
| 490 |
+
"mimetype": "text/x-python",
|
| 491 |
+
"name": "python",
|
| 492 |
+
"nbconvert_exporter": "python",
|
| 493 |
+
"pygments_lexer": "ipython3",
|
| 494 |
+
"version": "3.12.13"
|
| 495 |
+
}
|
| 496 |
+
},
|
| 497 |
+
"nbformat": 4,
|
| 498 |
+
"nbformat_minor": 4
|
| 499 |
+
}
|
scripts/download_model.py
CHANGED
|
@@ -1,45 +1,104 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""Download
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
import argparse
|
| 7 |
-
import os
|
| 8 |
from pathlib import Path
|
| 9 |
|
| 10 |
-
from huggingface_hub import hf_hub_download
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
def main() -> None:
|
| 14 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 15 |
parser.add_argument(
|
| 16 |
-
"--
|
| 17 |
-
default=
|
| 18 |
-
help="
|
| 19 |
-
)
|
| 20 |
-
parser.add_argument(
|
| 21 |
-
"--file",
|
| 22 |
-
default=os.environ.get("MODEL_FILE", "qwen2.5-3b-instruct-q4_k_m.gguf"),
|
| 23 |
-
help="GGUF filename inside the repo",
|
| 24 |
)
|
| 25 |
parser.add_argument(
|
| 26 |
"--output-dir",
|
| 27 |
type=Path,
|
| 28 |
default=Path("models"),
|
| 29 |
-
help="Directory to
|
| 30 |
)
|
| 31 |
args = parser.parse_args()
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
if __name__ == "__main__":
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""Download a configured model preset from Hugging Face Hub for offline dev."""
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
import argparse
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
+
from huggingface_hub import hf_hub_download, snapshot_download
|
| 10 |
+
|
| 11 |
+
from inference.config import get_app_config, get_model_config
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _is_local_path(value: str) -> bool:
|
| 15 |
+
return value.startswith(("./", "../")) or Path(value).is_absolute()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _download_llama_cpp(model, preset_key: str, output_dir: Path) -> Path:
|
| 19 |
+
if model.model_path:
|
| 20 |
+
path = Path(model.model_path)
|
| 21 |
+
if not path.exists():
|
| 22 |
+
raise SystemExit(f"Local MODEL_PATH does not exist: {model.model_path}")
|
| 23 |
+
print(f"Preset {preset_key!r} already points to local file: {path}")
|
| 24 |
+
print(f"Set MODEL_PATH={path} or update models.yaml model_path to use it directly.")
|
| 25 |
+
return path.resolve()
|
| 26 |
+
|
| 27 |
+
if not model.model_repo or not model.model_file:
|
| 28 |
+
raise SystemExit(f"Preset {preset_key!r} is missing model_repo/model_file.")
|
| 29 |
+
|
| 30 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
path = Path(
|
| 32 |
+
hf_hub_download(
|
| 33 |
+
repo_id=model.model_repo,
|
| 34 |
+
filename=model.model_file,
|
| 35 |
+
local_dir=output_dir,
|
| 36 |
+
local_dir_use_symlinks=False,
|
| 37 |
+
)
|
| 38 |
+
)
|
| 39 |
+
print(f"Preset: {preset_key} ({model.label})")
|
| 40 |
+
print(f"Model ready at: {path}")
|
| 41 |
+
print("Add to models.yaml under that preset:")
|
| 42 |
+
print(f" model_path: {path.resolve()}")
|
| 43 |
+
return path.resolve()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _download_transformers(model, preset_key: str, output_dir: Path) -> Path:
|
| 47 |
+
if not model.model_id:
|
| 48 |
+
raise SystemExit(f"Preset {preset_key!r} is missing model_id.")
|
| 49 |
+
|
| 50 |
+
if _is_local_path(model.model_id):
|
| 51 |
+
path = Path(model.model_id).resolve()
|
| 52 |
+
if not path.exists():
|
| 53 |
+
raise SystemExit(f"Local model_id does not exist: {model.model_id}")
|
| 54 |
+
print(f"Preset {preset_key!r} already points to local path: {path}")
|
| 55 |
+
print(f"Set MODEL_ID={path} or update models.yaml model_id to use it directly.")
|
| 56 |
+
return path
|
| 57 |
+
|
| 58 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 59 |
+
dest = output_dir / model.model_id
|
| 60 |
+
path = Path(
|
| 61 |
+
snapshot_download(
|
| 62 |
+
repo_id=model.model_id,
|
| 63 |
+
local_dir=dest,
|
| 64 |
+
local_dir_use_symlinks=False,
|
| 65 |
+
)
|
| 66 |
+
)
|
| 67 |
+
print(f"Preset: {preset_key} ({model.label})")
|
| 68 |
+
print(f"Model ready at: {path}")
|
| 69 |
+
print("Add to models.yaml under that preset:")
|
| 70 |
+
print(f" model_id: ./{path.relative_to(Path.cwd())}")
|
| 71 |
+
return path.resolve()
|
| 72 |
|
| 73 |
|
| 74 |
def main() -> None:
|
| 75 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 76 |
parser.add_argument(
|
| 77 |
+
"--preset",
|
| 78 |
+
default=None,
|
| 79 |
+
help="Preset key from models.yaml (default: ACTIVE_MODEL or app default)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
)
|
| 81 |
parser.add_argument(
|
| 82 |
"--output-dir",
|
| 83 |
type=Path,
|
| 84 |
default=Path("models"),
|
| 85 |
+
help="Directory to download the model into",
|
| 86 |
)
|
| 87 |
args = parser.parse_args()
|
| 88 |
|
| 89 |
+
app_config = get_app_config()
|
| 90 |
+
preset_key = args.preset or app_config.active_model
|
| 91 |
+
model = get_model_config(preset_key)
|
| 92 |
|
| 93 |
+
if model.backend == "llama_cpp":
|
| 94 |
+
_download_llama_cpp(model, preset_key, args.output_dir)
|
| 95 |
+
return
|
| 96 |
+
|
| 97 |
+
if model.backend == "transformers":
|
| 98 |
+
_download_transformers(model, preset_key, args.output_dir)
|
| 99 |
+
return
|
| 100 |
+
|
| 101 |
+
raise SystemExit(f"Preset {preset_key!r} uses unsupported backend {model.backend!r}.")
|
| 102 |
|
| 103 |
|
| 104 |
if __name__ == "__main__":
|
uv.lock
CHANGED
|
@@ -198,7 +198,7 @@ name = "cuda-bindings"
|
|
| 198 |
version = "13.3.1"
|
| 199 |
source = { registry = "https://pypi.org/simple" }
|
| 200 |
dependencies = [
|
| 201 |
-
{ name = "cuda-pathfinder" },
|
| 202 |
]
|
| 203 |
wheels = [
|
| 204 |
{ url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" },
|
|
@@ -229,34 +229,34 @@ wheels = [
|
|
| 229 |
|
| 230 |
[package.optional-dependencies]
|
| 231 |
cudart = [
|
| 232 |
-
{ name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'
|
| 233 |
]
|
| 234 |
cufft = [
|
| 235 |
-
{ name = "nvidia-cufft", marker = "sys_platform == 'linux'
|
| 236 |
]
|
| 237 |
cufile = [
|
| 238 |
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
|
| 239 |
]
|
| 240 |
cupti = [
|
| 241 |
-
{ name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'
|
| 242 |
]
|
| 243 |
curand = [
|
| 244 |
-
{ name = "nvidia-curand", marker = "sys_platform == 'linux'
|
| 245 |
]
|
| 246 |
cusolver = [
|
| 247 |
-
{ name = "nvidia-cusolver", marker = "sys_platform == 'linux'
|
| 248 |
]
|
| 249 |
cusparse = [
|
| 250 |
-
{ name = "nvidia-cusparse", marker = "sys_platform == 'linux'
|
| 251 |
]
|
| 252 |
nvjitlink = [
|
| 253 |
-
{ name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'
|
| 254 |
]
|
| 255 |
nvrtc = [
|
| 256 |
-
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'
|
| 257 |
]
|
| 258 |
nvtx = [
|
| 259 |
-
{ name = "nvidia-nvtx", marker = "sys_platform == 'linux'
|
| 260 |
]
|
| 261 |
|
| 262 |
[[package]]
|
|
@@ -498,26 +498,25 @@ name = "inference"
|
|
| 498 |
version = "0.1.0"
|
| 499 |
source = { editable = "libs/inference" }
|
| 500 |
dependencies = [
|
|
|
|
| 501 |
{ name = "huggingface-hub" },
|
| 502 |
{ name = "llama-cpp-python" },
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
[package.optional-dependencies]
|
| 506 |
-
transformers = [
|
| 507 |
-
{ name = "accelerate" },
|
| 508 |
{ name = "torch" },
|
|
|
|
| 509 |
{ name = "transformers" },
|
| 510 |
]
|
| 511 |
|
| 512 |
[package.metadata]
|
| 513 |
requires-dist = [
|
| 514 |
-
{ name = "accelerate",
|
| 515 |
{ name = "huggingface-hub", specifier = ">=0.27.0" },
|
| 516 |
{ name = "llama-cpp-python", specifier = ">=0.3.0" },
|
| 517 |
-
{ name = "
|
| 518 |
-
{ name = "
|
|
|
|
|
|
|
| 519 |
]
|
| 520 |
-
provides-extras = ["transformers"]
|
| 521 |
|
| 522 |
[[package]]
|
| 523 |
name = "iniconfig"
|
|
@@ -720,7 +719,7 @@ name = "nvidia-cublas"
|
|
| 720 |
version = "13.1.1.3"
|
| 721 |
source = { registry = "https://pypi.org/simple" }
|
| 722 |
dependencies = [
|
| 723 |
-
{ name = "nvidia-cuda-nvrtc" },
|
| 724 |
]
|
| 725 |
wheels = [
|
| 726 |
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
|
|
@@ -759,7 +758,7 @@ name = "nvidia-cudnn-cu13"
|
|
| 759 |
version = "9.20.0.48"
|
| 760 |
source = { registry = "https://pypi.org/simple" }
|
| 761 |
dependencies = [
|
| 762 |
-
{ name = "nvidia-cublas" },
|
| 763 |
]
|
| 764 |
wheels = [
|
| 765 |
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
|
|
@@ -771,7 +770,7 @@ name = "nvidia-cufft"
|
|
| 771 |
version = "12.0.0.61"
|
| 772 |
source = { registry = "https://pypi.org/simple" }
|
| 773 |
dependencies = [
|
| 774 |
-
{ name = "nvidia-nvjitlink" },
|
| 775 |
]
|
| 776 |
wheels = [
|
| 777 |
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
|
|
@@ -801,9 +800,9 @@ name = "nvidia-cusolver"
|
|
| 801 |
version = "12.0.4.66"
|
| 802 |
source = { registry = "https://pypi.org/simple" }
|
| 803 |
dependencies = [
|
| 804 |
-
{ name = "nvidia-cublas" },
|
| 805 |
-
{ name = "nvidia-cusparse" },
|
| 806 |
-
{ name = "nvidia-nvjitlink" },
|
| 807 |
]
|
| 808 |
wheels = [
|
| 809 |
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
|
|
@@ -815,7 +814,7 @@ name = "nvidia-cusparse"
|
|
| 815 |
version = "12.6.3.3"
|
| 816 |
source = { registry = "https://pypi.org/simple" }
|
| 817 |
dependencies = [
|
| 818 |
-
{ name = "nvidia-nvjitlink" },
|
| 819 |
]
|
| 820 |
wheels = [
|
| 821 |
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
|
|
@@ -1614,6 +1613,38 @@ wheels = [
|
|
| 1614 |
{ url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" },
|
| 1615 |
]
|
| 1616 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1617 |
[[package]]
|
| 1618 |
name = "tqdm"
|
| 1619 |
version = "4.68.1"
|
|
|
|
| 198 |
version = "13.3.1"
|
| 199 |
source = { registry = "https://pypi.org/simple" }
|
| 200 |
dependencies = [
|
| 201 |
+
{ name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
| 202 |
]
|
| 203 |
wheels = [
|
| 204 |
{ url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" },
|
|
|
|
| 229 |
|
| 230 |
[package.optional-dependencies]
|
| 231 |
cudart = [
|
| 232 |
+
{ name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" },
|
| 233 |
]
|
| 234 |
cufft = [
|
| 235 |
+
{ name = "nvidia-cufft", marker = "sys_platform == 'linux'" },
|
| 236 |
]
|
| 237 |
cufile = [
|
| 238 |
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
|
| 239 |
]
|
| 240 |
cupti = [
|
| 241 |
+
{ name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" },
|
| 242 |
]
|
| 243 |
curand = [
|
| 244 |
+
{ name = "nvidia-curand", marker = "sys_platform == 'linux'" },
|
| 245 |
]
|
| 246 |
cusolver = [
|
| 247 |
+
{ name = "nvidia-cusolver", marker = "sys_platform == 'linux'" },
|
| 248 |
]
|
| 249 |
cusparse = [
|
| 250 |
+
{ name = "nvidia-cusparse", marker = "sys_platform == 'linux'" },
|
| 251 |
]
|
| 252 |
nvjitlink = [
|
| 253 |
+
{ name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" },
|
| 254 |
]
|
| 255 |
nvrtc = [
|
| 256 |
+
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" },
|
| 257 |
]
|
| 258 |
nvtx = [
|
| 259 |
+
{ name = "nvidia-nvtx", marker = "sys_platform == 'linux'" },
|
| 260 |
]
|
| 261 |
|
| 262 |
[[package]]
|
|
|
|
| 498 |
version = "0.1.0"
|
| 499 |
source = { editable = "libs/inference" }
|
| 500 |
dependencies = [
|
| 501 |
+
{ name = "accelerate" },
|
| 502 |
{ name = "huggingface-hub" },
|
| 503 |
{ name = "llama-cpp-python" },
|
| 504 |
+
{ name = "pyyaml" },
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
{ name = "torch" },
|
| 506 |
+
{ name = "torchvision" },
|
| 507 |
{ name = "transformers" },
|
| 508 |
]
|
| 509 |
|
| 510 |
[package.metadata]
|
| 511 |
requires-dist = [
|
| 512 |
+
{ name = "accelerate", specifier = ">=1.2.0" },
|
| 513 |
{ name = "huggingface-hub", specifier = ">=0.27.0" },
|
| 514 |
{ name = "llama-cpp-python", specifier = ">=0.3.0" },
|
| 515 |
+
{ name = "pyyaml", specifier = ">=6.0.2" },
|
| 516 |
+
{ name = "torch", specifier = ">=2.5.0" },
|
| 517 |
+
{ name = "torchvision", specifier = ">=0.20.0" },
|
| 518 |
+
{ name = "transformers", specifier = ">=5.7.0" },
|
| 519 |
]
|
|
|
|
| 520 |
|
| 521 |
[[package]]
|
| 522 |
name = "iniconfig"
|
|
|
|
| 719 |
version = "13.1.1.3"
|
| 720 |
source = { registry = "https://pypi.org/simple" }
|
| 721 |
dependencies = [
|
| 722 |
+
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
| 723 |
]
|
| 724 |
wheels = [
|
| 725 |
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
|
|
|
|
| 758 |
version = "9.20.0.48"
|
| 759 |
source = { registry = "https://pypi.org/simple" }
|
| 760 |
dependencies = [
|
| 761 |
+
{ name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
| 762 |
]
|
| 763 |
wheels = [
|
| 764 |
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
|
|
|
|
| 770 |
version = "12.0.0.61"
|
| 771 |
source = { registry = "https://pypi.org/simple" }
|
| 772 |
dependencies = [
|
| 773 |
+
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
| 774 |
]
|
| 775 |
wheels = [
|
| 776 |
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
|
|
|
|
| 800 |
version = "12.0.4.66"
|
| 801 |
source = { registry = "https://pypi.org/simple" }
|
| 802 |
dependencies = [
|
| 803 |
+
{ name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
| 804 |
+
{ name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
| 805 |
+
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
| 806 |
]
|
| 807 |
wheels = [
|
| 808 |
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
|
|
|
|
| 814 |
version = "12.6.3.3"
|
| 815 |
source = { registry = "https://pypi.org/simple" }
|
| 816 |
dependencies = [
|
| 817 |
+
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
| 818 |
]
|
| 819 |
wheels = [
|
| 820 |
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
|
|
|
|
| 1613 |
{ url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" },
|
| 1614 |
]
|
| 1615 |
|
| 1616 |
+
[[package]]
|
| 1617 |
+
name = "torchvision"
|
| 1618 |
+
version = "0.27.0"
|
| 1619 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1620 |
+
dependencies = [
|
| 1621 |
+
{ name = "numpy" },
|
| 1622 |
+
{ name = "pillow" },
|
| 1623 |
+
{ name = "torch" },
|
| 1624 |
+
]
|
| 1625 |
+
wheels = [
|
| 1626 |
+
{ url = "https://files.pythonhosted.org/packages/9e/c8/5cd91932f7f3671b0743dc4ae1a4c16b1d0b45bf4087976277d325bda718/torchvision-0.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1a6dd742a150645126df9e0b2e449874c1d635897c773b322c2e067e98382dfe", size = 1758824, upload-time = "2026-05-13T14:57:15.227Z" },
|
| 1627 |
+
{ url = "https://files.pythonhosted.org/packages/d9/36/7fb7d19477b3d93283b52fea11fa8ee30ab9064a08c97b4a6b91445e26cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65772ff3ec4f4f5d680e30019835555dd239e7fefee4b0a846375fe1cb1592ef", size = 7831034, upload-time = "2026-05-13T14:57:06.483Z" },
|
| 1628 |
+
{ url = "https://files.pythonhosted.org/packages/62/43/dfd894c3f8b01b5b33fde990f0159c1926ebc7b6e2c4193e2efb7da3c4cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7a9966a088d06b4cf6c610e03be62de469efa6f2cd2e7c7eed8e925ed6af59ac", size = 7579774, upload-time = "2026-05-13T14:56:59.337Z" },
|
| 1629 |
+
{ url = "https://files.pythonhosted.org/packages/ff/0c/722e989f9cf026e97ef7cb24a9bb1859e099f72d247ae35388fb89729f73/torchvision-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c037709072ca9b19750c0cbe9e8bb6f91c9a1be1befa26df33e281deccbd8c7", size = 4021073, upload-time = "2026-05-13T14:57:00.848Z" },
|
| 1630 |
+
{ url = "https://files.pythonhosted.org/packages/d8/ae/36547812e6e047c1d80bcacd1b17a340612b08a6e876e0aabf3d0b9228b0/torchvision-0.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:41d6dae73e1af09fa82ded597ae57f2a2314285acde54b25890a8f8e51b999d7", size = 1758826, upload-time = "2026-05-13T14:57:05.262Z" },
|
| 1631 |
+
{ url = "https://files.pythonhosted.org/packages/ae/30/32c4ea842738728a14e3df8c576c62dedcf5ae5cb6a5c984c6429ebe7524/torchvision-0.27.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:70f071c6f74b60d5fe8851636d8d4cd5f4fa29d57fd9348a87a6f17b990b95ba", size = 7789501, upload-time = "2026-05-13T14:56:57.786Z" },
|
| 1632 |
+
{ url = "https://files.pythonhosted.org/packages/f6/24/4d0d48684251bd0673f87d633d5d88ab00227983b00591156eed2f86c8d5/torchvision-0.27.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aaafa6962c9d91f42503de1957d6fa349907d028c06f335bd95da7a5bc57147d", size = 7579868, upload-time = "2026-05-13T14:56:41.618Z" },
|
| 1633 |
+
{ url = "https://files.pythonhosted.org/packages/ba/da/e6edd051d2ba25adf23b120fa97f458dff888d098c51e84724f17d2d1470/torchvision-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:aee384a2782c89517c4ab9061d2720ba59fd2ffe5ef89d0a149cc2d43abdf521", size = 4092700, upload-time = "2026-05-13T14:57:09.729Z" },
|
| 1634 |
+
{ url = "https://files.pythonhosted.org/packages/fa/23/95dfa40431360f42ca949bf861434bed51164adfa8fb9801e05bf3194f50/torchvision-0.27.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:c5121f1b9ab09a7f73e837871deb8321551f7eaeb19d87aa00de9191968eae44", size = 1845008, upload-time = "2026-05-13T14:57:03.768Z" },
|
| 1635 |
+
{ url = "https://files.pythonhosted.org/packages/23/b9/9dbdf76b2b49a75ba8088df6f7c755bdb520afb6c6dbac0102b46cde5e99/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1c01f0d1091ae22b9dfc082b0a0fe5faaf053686a29b4fb082ba7691375c73cf", size = 7791430, upload-time = "2026-05-13T14:56:56.206Z" },
|
| 1636 |
+
{ url = "https://files.pythonhosted.org/packages/5c/6a/e4a16cf2f3310c2ea7760dc5d9054496844391e0f4c1fae87fefac2f3d9e/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dadea3c5ecfd05bbb2a3312ab0374f213c58bf6459cb059122e2f4dfe13d10ed", size = 7668441, upload-time = "2026-05-13T14:57:02.127Z" },
|
| 1637 |
+
{ url = "https://files.pythonhosted.org/packages/00/70/01b6461117a6a94b5af3f8ee166bb0f045056f3cf187750c110dabfdfffa/torchvision-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a49e55055a39a8506fe7e59850522cab004efb2c3839f6057658889c1d69c815", size = 4141602, upload-time = "2026-05-13T14:56:53.449Z" },
|
| 1638 |
+
{ url = "https://files.pythonhosted.org/packages/92/22/c0633677b3b3f3e69554a21ac087bf705f829c40cd5e3783507b8c006681/torchvision-0.27.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c1fac0fc2a7adf29481fc1938a0e7845c57ba1147a986784109c4d98f434ea8c", size = 1758818, upload-time = "2026-05-13T14:56:54.988Z" },
|
| 1639 |
+
{ url = "https://files.pythonhosted.org/packages/48/e8/55f9d9667b56dae470e69e31beac9b00d458ea393feec1aae95cc4f3f1c9/torchvision-0.27.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:cbf89764fc76f3f17fbf80c12d5a89c691e91cb9d82c38412aaf0568655ffb19", size = 7789667, upload-time = "2026-05-13T14:56:48.858Z" },
|
| 1640 |
+
{ url = "https://files.pythonhosted.org/packages/00/bc/6f8681daf3bbc4c315bb0005110f99d28e3ecd675bf9c8f2c0d393fbac7a/torchvision-0.27.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:91f61b9865423037c327eb56afa207cc72de874e458c361840db9dcf5ce0c0eb", size = 7579848, upload-time = "2026-05-13T14:56:38.209Z" },
|
| 1641 |
+
{ url = "https://files.pythonhosted.org/packages/19/6c/8d8020e6bd1e46c53e487c9c4e9457a07f2ee28931028fb5d71e2da40adc/torchvision-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:5bb82fc3c55daf1788621e504310b0a286f1069627a8742f692aebb075ef25a7", size = 4119284, upload-time = "2026-05-13T14:56:46.625Z" },
|
| 1642 |
+
{ url = "https://files.pythonhosted.org/packages/8d/7e/e78c48662a8d551606efdbe11c6b9c1d6d2391b92cd0e4591b9e6a2412b8/torchvision-0.27.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2c4099a15150143b9b034730b404a56d572efe0b79489b4c765d929cb4eac7f3", size = 1758828, upload-time = "2026-05-13T14:56:52.293Z" },
|
| 1643 |
+
{ url = "https://files.pythonhosted.org/packages/21/dd/d03ee9f9ee7bf11a8c7c776fb8e7fd6102f59c013791a2a4e5175bd6cba7/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b4c6bb0a670dcba017b3643e21902c9b8a1cc1c127d602f1488fa29ec3c6e865", size = 7790618, upload-time = "2026-05-13T14:56:44.721Z" },
|
| 1644 |
+
{ url = "https://files.pythonhosted.org/packages/39/08/4002336a74742be70728603ec1769feb2b55e0d19c532c9ec9f92008de76/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1c2db4bde82bc48ebff73436a6adf34d4f809448268a70d9a1285f5c8f92313d", size = 7580217, upload-time = "2026-05-13T14:56:43.274Z" },
|
| 1645 |
+
{ url = "https://files.pythonhosted.org/packages/ed/cb/4dd4783eb3565f526ba6e64b6f6ca26c00eacc924cdfe60455db9d91b84b/torchvision-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:72bf547e58ddb948689734eed6f4b6a2031f979dba4fb08e3690688b392e929f", size = 4226392, upload-time = "2026-05-13T14:56:40.235Z" },
|
| 1646 |
+
]
|
| 1647 |
+
|
| 1648 |
[[package]]
|
| 1649 |
name = "tqdm"
|
| 1650 |
version = "4.68.1"
|