MSG commited on
Commit
1f5cc33
·
2 Parent(s): 543077a3e6d002

Merge pull request #2 from MSghais/feat/poc_gradio

Browse files
.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 backend (requires inference[transformers] extra)
 
11
  # INFERENCE_BACKEND=transformers
12
- # MODEL_ID=Qwen/Qwen2.5-3B-Instruct
 
 
 
 
 
 
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
- title: Small Model Hackathon
 
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
- See [`.env.example`](.env.example) for a full template.
 
 
 
 
 
 
 
 
 
 
 
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
- - [ ] Choose a track (Backyard AI or Thousand Token Wood)
85
- - [ ] Space live under build-small-hackathon
86
- - [ ] Demo video recorded
87
- - [ ] Social post published
88
- - [ ] Submission locked in by **June 15, 2026**
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
- | Variable | Default | Description |
65
- |----------|---------|-------------|
66
- | `INFERENCE_BACKEND` | `llama_cpp` | `llama_cpp` or `transformers` |
67
- | `MODEL_REPO` | `Qwen/Qwen2.5-3B-Instruct-GGUF` | Hub repo for GGUF |
68
- | `MODEL_FILE` | `qwen2.5-3b-instruct-q4_k_m.gguf` | GGUF filename |
69
- | `MODEL_PATH` | — | Local GGUF path (skips Hub download) |
70
- | `N_CTX` | `4096` | Context window |
71
- | `N_GPU_LAYERS` | `0` | GPU layers for llama.cpp (`0` = CPU only) |
72
- | `PORT` | `7860` | Gradio listen port |
73
- | `MODEL_ID` | `Qwen/Qwen2.5-3B-Instruct` | Used when `INFERENCE_BACKEND=transformers` |
 
 
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
- | Setting | Recommendation |
146
- |---------|----------------|
147
- | Hardware | **CPU basic** to start (llama.cpp with `N_GPU_LAYERS=0`) |
148
- | Upgrade | GPU Space if you set `N_GPU_LAYERS > 0` for faster inference |
 
 
149
 
150
  ### 4. Set Space environment variables
151
 
152
  In the Space **Settings → Variables and secrets**:
153
 
154
- | Variable | Value |
155
- |----------|-------|
156
- | `INFERENCE_BACKEND` | `llama_cpp` |
157
- | `MODEL_REPO` | `Qwen/Qwen2.5-3B-Instruct-GGUF` |
158
- | `MODEL_FILE` | `qwen2.5-3b-instruct-q4_k_m.gguf` |
159
- | `N_CTX` | `4096` |
160
- | `N_GPU_LAYERS` | `0` (or higher on GPU hardware) |
 
 
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
- | Symptom | Likely cause | Fix |
181
- |---------|--------------|-----|
182
- | First chat hangs / slow | GGUF downloading from Hub | Pre-download locally; on Space, wait or use Storage Bucket |
183
- | `Failed to load model` in chat | Wrong `MODEL_REPO` / `MODEL_FILE` | Check env vars match a valid GGUF on Hub |
184
- | Docker build fails on `llama-cpp-python` | Missing build tools | Dockerfile already installs `build-essential` and `cmake` |
185
- | Space build fails | Missing `uv.lock` or README YAML | Ensure `sdk: docker` is in root `README.md` frontmatter |
186
- | `transformers` backend error | Optional deps not installed | Run `uv sync --package inference --extra transformers` |
187
- | Port already in use locally | Another process on 7860 | `PORT=7861 uv run --package gradio-space python -m gradio_space.app` |
 
 
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
- | Environment | How to run |
200
- |-------------|------------|
201
- | Local dev | `uv run --package gradio-space python -m gradio_space.app` |
202
- | Docker | `docker run -p 7860:7860 hackathon-space` |
203
- | HF Space | Built and started automatically from `Dockerfile` `CMD` |
 
 
 
 
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.factory import get_backend
 
6
 
7
- _backend = get_backend()
8
- _model_ready = False
9
- _load_error: str | None = None
 
10
 
11
 
12
- def _ensure_model_loaded() -> str | None:
13
- global _model_ready, _load_error
14
 
15
- if _model_ready:
 
 
 
 
16
  return None
17
 
18
- if _load_error:
19
- return _load_error
20
 
21
  try:
22
- _backend.load()
23
- _model_ready = True
24
  return None
25
  except Exception as exc: # noqa: BLE001 — surface model load failures in the UI
26
- _load_error = f"Failed to load model: {exc}"
27
- return _load_error
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
- def warmup() -> str:
50
- if _model_ready:
51
- return "Model ready."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- if _load_error:
54
- return _load_error
55
 
 
56
  return (
57
- "Model not loaded yet. It will download from Hugging Face Hub on the "
58
- "first chat message — this can take a few minutes on CPU."
59
  )
60
 
61
 
 
 
 
 
 
62
  def build_demo() -> gr.Blocks:
63
- model_repo = os.environ.get("MODEL_REPO", "Qwen/Qwen2.5-3B-Instruct-GGUF")
64
- model_file = os.environ.get("MODEL_FILE", "qwen2.5-3b-instruct-q4_k_m.gguf")
65
- backend_name = os.environ.get("INFERENCE_BACKEND", "llama_cpp")
 
 
 
66
 
67
  with gr.Blocks(title="Small Model Hackathon") as demo:
68
  gr.Markdown(
69
  f"""
70
  # Small Model Chat
71
 
72
- Local inference via **{backend_name}**.
73
 
74
- - **Repo:** `{model_repo}`
75
- - **File:** `{model_file}`
 
76
 
77
  Part of the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
78
  """
79
  )
80
- status = gr.Markdown(warmup())
81
- gr.ChatInterface(
82
- fn=chat,
83
- examples=["Hello! What can you help me with?", "Explain llama.cpp in one sentence."],
84
- )
85
- demo.load(warmup, outputs=status)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "transformers>=4.47.0",
 
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.factory import get_backend
 
2
 
3
- __all__ = ["get_backend"]
 
 
 
 
 
 
 
 
 
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
- if backend_name == "llama_cpp":
13
- return LlamaCppBackend()
 
14
 
15
- if backend_name == "transformers":
16
  from inference.transformers import TransformersBackend
17
 
18
- return TransformersBackend()
19
 
20
  raise ValueError(
21
- f"Unknown INFERENCE_BACKEND={backend_name!r}. "
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
- model_path = os.environ.get("MODEL_PATH")
19
- if model_path:
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
- model_repo = os.environ.get("MODEL_REPO", DEFAULT_MODEL_REPO)
26
- model_file = os.environ.get("MODEL_FILE", DEFAULT_MODEL_FILE)
 
 
 
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 = 512,
55
- temperature: float = 0.7,
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 = 512,
73
- temperature: float = 0.7,
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 os
2
 
3
- from inference.base import InferenceBackend
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 AutoModelForCausalLM, AutoTokenizer
 
 
 
 
 
18
  except ImportError as exc:
19
  raise ImportError(
20
- "transformers backend requires optional deps. "
21
- "Install with: uv sync --package inference --extra transformers"
22
  ) from exc
23
 
24
- model_id = os.environ.get("MODEL_ID", "Qwen/Qwen2.5-3B-Instruct")
25
- device = "cuda" if torch.cuda.is_available() else "cpu"
26
-
27
- self._tokenizer = AutoTokenizer.from_pretrained(model_id)
28
- self._model = AutoModelForCausalLM.from_pretrained(
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
- def generate(
37
- self,
38
- prompt: str,
39
- *,
40
- max_tokens: int = 512,
41
- temperature: float = 0.7,
42
- ) -> str:
43
  self.load()
44
- assert self._model is not None
45
- assert self._tokenizer is not None
46
 
47
- import torch
 
 
 
 
48
 
49
- inputs = self._tokenizer(prompt, return_tensors="pt").to(self._model.device)
50
- output = self._model.generate(
51
- **inputs,
52
- max_new_tokens=max_tokens,
53
- temperature=temperature,
54
- do_sample=temperature > 0,
55
- )
56
- generated = output[0][inputs["input_ids"].shape[-1] :]
57
- return self._tokenizer.decode(generated, skip_special_tokens=True).strip()
58
 
59
- def chat(
60
  self,
61
- messages: list[dict[str, str]],
62
  *,
63
- max_tokens: int = 512,
64
- temperature: float = 0.7,
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
- return self.generate(prompt, max_tokens=max_tokens, temperature=temperature)
 
 
 
 
 
 
 
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- # Satisfy static type checkers that expect InferenceBackend.
89
- _: InferenceBackend = TransformersBackend()
 
 
 
 
 
 
 
 
 
 
 
 
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 the configured GGUF model from Hugging Face Hub for offline dev."""
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
- "--repo",
17
- default=os.environ.get("MODEL_REPO", "Qwen/Qwen2.5-3B-Instruct-GGUF"),
18
- help="Hugging Face repo containing the GGUF file",
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 copy/symlink the downloaded model into",
30
  )
31
  args = parser.parse_args()
32
 
33
- args.output_dir.mkdir(parents=True, exist_ok=True)
 
 
34
 
35
- path = hf_hub_download(
36
- repo_id=args.repo,
37
- filename=args.file,
38
- local_dir=args.output_dir,
39
- local_dir_use_symlinks=False,
40
- )
41
- print(f"Model ready at: {path}")
42
- print(f"Set MODEL_PATH={path} to use this file directly.")
 
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' or sys_platform == 'win32'" },
233
  ]
234
  cufft = [
235
- { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
236
  ]
237
  cufile = [
238
  { name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
239
  ]
240
  cupti = [
241
- { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
242
  ]
243
  curand = [
244
- { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
245
  ]
246
  cusolver = [
247
- { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
248
  ]
249
  cusparse = [
250
- { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
251
  ]
252
  nvjitlink = [
253
- { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
254
  ]
255
  nvrtc = [
256
- { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
257
  ]
258
  nvtx = [
259
- { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
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", marker = "extra == 'transformers'", specifier = ">=1.2.0" },
515
  { name = "huggingface-hub", specifier = ">=0.27.0" },
516
  { name = "llama-cpp-python", specifier = ">=0.3.0" },
517
- { name = "torch", marker = "extra == 'transformers'", specifier = ">=2.5.0" },
518
- { name = "transformers", marker = "extra == 'transformers'", specifier = ">=4.47.0" },
 
 
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"