Spaces:
Sleeping
Sleeping
MSG msgencrypted-auto Cursor commited on
Commit ·
727cb75
1
Parent(s): 6cea344
Feat/monday sprint 2 (#19)
Browse files* plan sprint
* model config loading
* model config
* llama studio config
* common fix
* lm eval fix
* llama studio config
* llama studio model
* modal fix
* modal fix eval
* fix
* fix config and stuff model switch
* common fix
* server app modal
* server and orgin fix
* Pin torch 2.11 for HF ZeroGPU Space compatibility.
ZeroGPU rejects torch 2.12; supported versions are 2.8–2.11.
Co-authored-by: Cursor <cursoragent@cursor.com>
* finetuning stuff
* fix common and experiment
* readme md
* experiment
* fix experiment
* finetuning stuff
* finetuning
* fix experiment and stuff
* adaptaters + config fix
---------
Co-authored-by: msgencrypted-auto <msgencrypted.auto@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- .cursor/plans/llama_backend_model_switching_77de87de.plan.md +231 -0
- .cursor/plans/todo_last_sprint_tracks_b100b17b.plan.md +187 -0
- .env.example +8 -2
- README.md +40 -3
- TODO.md +2 -2
- USAGE.md +31 -0
- apps/gradio-space/README.md +11 -1
- apps/gradio-space/src/gradio_space/api/studio.py +19 -2
- apps/gradio-space/src/gradio_space/model_loading.py +35 -4
- apps/gradio-space/src/gradio_space/server.py +1 -1
- apps/gradio-space/src/gradio_space/tabs/chat.py +11 -4
- apps/gradio-space/src/gradio_space/ui/settings_panel.py +20 -5
- apps/gradio-space/static/studio/studio.js +34 -3
- apps/gradio-space/tests/test_model_loading.py +48 -0
- libs/inference/src/inference/config.py +2 -2
- libs/inference/src/inference/transformers.py +14 -4
- libs/inference/tests/test_config.py +63 -0
- models.yaml +29 -3
- requirements.txt +3 -2
- research/evals/configs/lm_eval_code.yaml +4 -0
- research/evals/configs/lm_eval_math.yaml +2 -0
- research/evals/src/slm_evals/run_lm_eval.py +26 -0
- research/finetune.py +169 -31
- research/modal/_common.py +84 -23
- research/modal/experiments.yaml +33 -6
- research/modal/server_app.py +5 -0
.cursor/plans/llama_backend_model_switching_77de87de.plan.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: Llama backend model switching
|
| 3 |
+
overview: Add the official MiniCPM-V-4.6 GGUF preset from openbmb/MiniCPM-V-4.6-gguf for the llama.cpp / Llama Champion path, then wire a shared runtime model selector so local dev can switch between transformers and llama.cpp backends (and other presets) from Gradio Settings and Studio — not just the Chat debug tab.
|
| 4 |
+
todos:
|
| 5 |
+
- id: add-gguf-preset
|
| 6 |
+
content: Add minicpm-v-4.6-gguf preset to models.yaml (openbmb/MiniCPM-V-4.6-gguf) and document in .env.example
|
| 7 |
+
status: completed
|
| 8 |
+
- id: runtime-model-state
|
| 9 |
+
content: Add set_runtime_model_key() and make get_active_model_key() runtime-aware in model_loading.py
|
| 10 |
+
status: completed
|
| 11 |
+
- id: classic-ui-sync
|
| 12 |
+
content: Wire Settings + Chat dropdowns to set_runtime_model_key; reload on change
|
| 13 |
+
status: completed
|
| 14 |
+
- id: studio-api-sync
|
| 15 |
+
content: Add api_set_active_model + studio.js settings dropdown handler; sync debug picker
|
| 16 |
+
status: completed
|
| 17 |
+
- id: tests-docs
|
| 18 |
+
content: Test preset parsing + runtime key override; document local switching in USAGE.md
|
| 19 |
+
status: completed
|
| 20 |
+
isProject: false
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
# Llama backend + runtime model switching (local dev)
|
| 24 |
+
|
| 25 |
+
## What already exists
|
| 26 |
+
|
| 27 |
+
Your repo already has **two inference backends** behind one factory — no new backend code is required for **text** inference:
|
| 28 |
+
|
| 29 |
+
```mermaid
|
| 30 |
+
flowchart LR
|
| 31 |
+
GradioUI[Gradio Classic + Studio]
|
| 32 |
+
ModelLoading[model_loading.py]
|
| 33 |
+
Factory[factory.py]
|
| 34 |
+
LlamaCpp[LlamaCppBackend]
|
| 35 |
+
Transformers[TransformersBackend]
|
| 36 |
+
GradioUI --> ModelLoading --> Factory
|
| 37 |
+
Factory -->|preset.backend=llama_cpp| LlamaCpp
|
| 38 |
+
Factory -->|preset.backend=transformers| Transformers
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
- Presets live in [`models.yaml`](models.yaml); backend is chosen **per preset**, not via a separate toggle.
|
| 42 |
+
- Switching transformers → llama.cpp means switching preset, e.g. `minicpm-v-4.6` → `minicpm-v-4.6-gguf` (to be added).
|
| 43 |
+
- [`libs/inference/src/inference/llama_cpp.py`](libs/inference/src/inference/llama_cpp.py) downloads GGUF from Hub and runs `create_chat_completion`.
|
| 44 |
+
- [`ALLOW_MODEL_SWITCH`](libs/inference/src/inference/config.py) gates dropdowns in Settings, Chat, and Studio debug — but **only Chat/Debug actually pass the selected key to inference**.
|
| 45 |
+
|
| 46 |
+
### Current gap (why switching feels broken)
|
| 47 |
+
|
| 48 |
+
[`get_active_model_key()`](apps/gradio-space/src/gradio_space/model_loading.py) always returns the **startup** preset from env/`models.yaml`:
|
| 49 |
+
|
| 50 |
+
```12:13:apps/gradio-space/src/gradio_space/model_loading.py
|
| 51 |
+
def get_active_model_key() -> str:
|
| 52 |
+
return _app_config.active_model
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
Lesson slides, ResearchMind, EchoCoach, TeacherVoice, and Studio Research/Slides all call `get_active_model_key()` — so changing the Settings dropdown only updates the status panel, not the model used by those tabs.
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## Step 1 — Add MiniCPM-V-4.6 GGUF preset (OpenBMB + llama.cpp)
|
| 60 |
+
|
| 61 |
+
Official GGUF is published at [`openbmb/MiniCPM-V-4.6-gguf`](https://huggingface.co/openbmb/MiniCPM-V-4.6-gguf). This is the **quantized llama.cpp build** of the same ~0.8B multimodal model already registered as `minicpm-v-4.6` (transformers). Recommended quant for local dev: **Q4_K_M** (~529 MB).
|
| 62 |
+
|
| 63 |
+
Add to [`models.yaml`](models.yaml):
|
| 64 |
+
|
| 65 |
+
```yaml
|
| 66 |
+
minicpm-v-4.6-gguf:
|
| 67 |
+
label: MiniCPM-V 4.6 (GGUF / llama.cpp)
|
| 68 |
+
backend: llama_cpp
|
| 69 |
+
model_repo: openbmb/MiniCPM-V-4.6-gguf
|
| 70 |
+
model_file: MiniCPM-V-4.6-Q4_K_M.gguf
|
| 71 |
+
multimodal: true
|
| 72 |
+
n_ctx: 8192
|
| 73 |
+
n_gpu_layers: 0
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
Pair with the existing transformers preset for A/B comparison:
|
| 77 |
+
|
| 78 |
+
| Preset key | Backend | Hub source | Use case |
|
| 79 |
+
|------------|---------|------------|----------|
|
| 80 |
+
| `minicpm-v-4.6` | transformers | `openbmb/MiniCPM-V-4.6` | Full multimodal (image/video) via HF processor |
|
| 81 |
+
| `minicpm-v-4.6-gguf` | llama_cpp | `openbmb/MiniCPM-V-4.6-gguf` | Llama Champion / Off-the-Grid; text chat + future image via llama.cpp |
|
| 82 |
+
|
| 83 |
+
Also update [`.env.example`](.env.example) with a commented dev block:
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
ALLOW_MODEL_SWITCH=true
|
| 87 |
+
ACTIVE_MODEL=minicpm-v-4.6 # transformers default (or minicpm5-1b)
|
| 88 |
+
# switch in UI to minicpm-v-4.6-gguf for llama.cpp
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Prefetch locally (optional, speeds first load):
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
uv run python scripts/download_model.py --preset minicpm-v-4.6-gguf
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
Per the [model card](https://huggingface.co/openbmb/MiniCPM-V-4.6-gguf), llama.cpp loads it directly — no custom fork:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
llama-cli -hf openbmb/MiniCPM-V-4.6-gguf:Q4_K_M
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
This satisfies the **Llama Champion** badge (llama.cpp runtime) while keeping the **OpenBMB / Tiny Titan** story (same MiniCPM-V 4.6 model family). LoRA/merged lesson presets on MiniCPM5-1B remain **transformers-only**.
|
| 104 |
+
|
| 105 |
+
### Multimodal caveat (text vs image)
|
| 106 |
+
|
| 107 |
+
- **Text-only tabs** (Lesson slides, ResearchMind, Chat, EchoCoach) work immediately — `LlamaCppBackend.chat()` passes string messages to `create_chat_completion`.
|
| 108 |
+
- **Image input via llama.cpp** requires OpenAI-style message content arrays (`type: image_url`). Current `LlamaCppBackend.chat()` types messages as `list[dict[str, str]]` and does not forward images. Defer image support to a follow-up unless a tab needs it now; keep `minicpm-v-4.6` (transformers) for full VLM demos.
|
| 109 |
+
|
| 110 |
+
---
|
| 111 |
+
|
| 112 |
+
## Step 2 — Shared runtime model state
|
| 113 |
+
|
| 114 |
+
Extend [`model_loading.py`](apps/gradio-space/src/gradio_space/model_loading.py):
|
| 115 |
+
|
| 116 |
+
| Function | Behavior |
|
| 117 |
+
|----------|----------|
|
| 118 |
+
| `set_runtime_model_key(key: str) -> str` | Validate key exists; if changed, call `reset_backend()` and clear load cache for old key; return label for UI |
|
| 119 |
+
| `get_active_model_key()` | Return `_runtime_model_key` if set, else `_app_config.active_model` |
|
| 120 |
+
| `reload_model(key)` | Also call `set_runtime_model_key(key)` so reload pins the selection app-wide |
|
| 121 |
+
|
| 122 |
+
This is a small, centralized change — every tab that already calls `get_active_model_key()` will automatically respect the runtime selection once Settings updates it.
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
## Step 3 — Classic Gradio UI wiring
|
| 127 |
+
|
| 128 |
+
### Settings panel ([`settings_panel.py`](apps/gradio-space/src/gradio_space/ui/settings_panel.py))
|
| 129 |
+
|
| 130 |
+
On dropdown `.change`:
|
| 131 |
+
1. Call `set_runtime_model_key(selected_key)`
|
| 132 |
+
2. Update status markdown (existing `model_status`)
|
| 133 |
+
3. Optionally auto-reload weights (or keep explicit "Reload model" button — recommend **reload on change** for dev UX)
|
| 134 |
+
|
| 135 |
+
Return `model_dropdown` from `build_settings_panel()` (already does) and expose it to [`app.py`](apps/gradio-space/src/gradio_space/app.py) if needed for cross-tab sync.
|
| 136 |
+
|
| 137 |
+
### Chat tab ([`tabs/chat.py`](apps/gradio-space/src/gradio_space/tabs/chat.py))
|
| 138 |
+
|
| 139 |
+
When `allow_model_switch` is on:
|
| 140 |
+
- On Chat model dropdown change → `set_runtime_model_key(mkey)` so Chat and Settings stay in sync
|
| 141 |
+
- Default dropdown value = `get_active_model_key()` (runtime-aware)
|
| 142 |
+
|
| 143 |
+
### App header badge (small UX win)
|
| 144 |
+
|
| 145 |
+
When `allow_model_switch` is false, keep current read-only badge. When true, show active preset + backend in Settings accordion header so devs always know which backend is live.
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## Step 4 — Studio UI wiring
|
| 150 |
+
|
| 151 |
+
In [`api/studio.py`](apps/gradio-space/src/gradio_space/api/studio.py):
|
| 152 |
+
|
| 153 |
+
- Add `api_set_active_model(model_key: str)` → calls `set_runtime_model_key`, returns updated `model_status`
|
| 154 |
+
- Register as `@server.api(name="set_active_model")`
|
| 155 |
+
- `api_model_choices()` should report `active_model=get_active_model_key()` (runtime-aware)
|
| 156 |
+
- `api_reload_model()` already accepts `model_key`; ensure it calls `set_runtime_model_key` too
|
| 157 |
+
|
| 158 |
+
In [`static/studio/studio.js`](apps/gradio-space/static/studio/studio.js) `initSettings()`:
|
| 159 |
+
- On `#settings-model-key` change → `callApi("set_active_model", [key])` then refresh status
|
| 160 |
+
- Keep debug chat dropdown in sync with settings dropdown
|
| 161 |
+
|
| 162 |
+
Studio Research + Slides already delegate to helpers that use `get_active_model_key()` — no per-endpoint `model_key` param needed once runtime state exists.
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
## Step 5 — Dev workflow (how you use it)
|
| 167 |
+
|
| 168 |
+
```bash
|
| 169 |
+
# .env
|
| 170 |
+
ALLOW_MODEL_SWITCH=true
|
| 171 |
+
ACTIVE_MODEL=minicpm-v-4.6
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
uv sync --all-packages
|
| 176 |
+
uv run --package gradio-space python -m gradio_space.server
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
| Goal | Action |
|
| 180 |
+
|------|--------|
|
| 181 |
+
| Transformers MiniCPM-V 4.6 (full VLM) | Select `minicpm-v-4.6` in Settings (or leave startup default) |
|
| 182 |
+
| llama.cpp MiniCPM-V 4.6 (Llama track) | Select `minicpm-v-4.6-gguf` — backend switches automatically |
|
| 183 |
+
| Text-only MiniCPM5 | Select `minicpm5-1b` |
|
| 184 |
+
| Fine-tuned lesson LoRA | Select `minicpm5-1b-lesson-lora` (transformers only) |
|
| 185 |
+
| Compare Qwen GGUF baseline | Select `qwen3b-gguf` |
|
| 186 |
+
|
| 187 |
+
**There is no separate "backend" dropdown** — backend follows the preset. Dropdown labels already include backend hints; optionally prefix choices with `[llama.cpp]` / `[transformers]` in `model_choices()` for clarity.
|
| 188 |
+
|
| 189 |
+
### Compatibility notes to surface in Settings status
|
| 190 |
+
|
| 191 |
+
- `minicpm-v-4.6-gguf` is text-ready on all tabs; image/video input needs transformers `minicpm-v-4.6` until llama.cpp multimodal messages are wired
|
| 192 |
+
- LoRA/merged local presets require transformers
|
| 193 |
+
- First llama.cpp load downloads ~529 MB GGUF from Hub (subsequent loads use cache)
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## Step 6 — Tests and docs
|
| 198 |
+
|
| 199 |
+
- Extend [`libs/inference/tests/test_config.py`](libs/inference/tests/test_config.py) to assert `minicpm-v-4.6-gguf` parses with `backend=llama_cpp` and `multimodal=true`
|
| 200 |
+
- Add a small unit test for `set_runtime_model_key` / `get_active_model_key` override in gradio-space tests (or inference tests if kept in `model_loading.py`)
|
| 201 |
+
- Add a short "Switching models locally" subsection to [`USAGE.md`](USAGE.md) and [`apps/gradio-space/README.md`](apps/gradio-space/README.md)
|
| 202 |
+
- Update [`TODO.md`](TODO.md) Llama Champion checklist to reference `minicpm-v-4.6-gguf` instead of generic MiniCPM5 GGUF
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## Architecture after changes
|
| 207 |
+
|
| 208 |
+
```mermaid
|
| 209 |
+
sequenceDiagram
|
| 210 |
+
participant Dev as Dev_UI_Settings
|
| 211 |
+
participant ML as model_loading
|
| 212 |
+
participant Factory as inference_factory
|
| 213 |
+
participant Tab as Any_Tab_or_Studio_API
|
| 214 |
+
|
| 215 |
+
Dev->>ML: set_runtime_model_key("minicpm-v-4.6-gguf")
|
| 216 |
+
ML->>Factory: reset_backend()
|
| 217 |
+
Tab->>ML: get_active_model_key()
|
| 218 |
+
ML-->>Tab: "minicpm-v-4.6-gguf"
|
| 219 |
+
Tab->>ML: ensure_model_loaded(key)
|
| 220 |
+
ML->>Factory: get_backend(key).load()
|
| 221 |
+
Note over Factory: LlamaCppBackend loads MiniCPM-V-4.6-Q4_K_M.gguf
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
---
|
| 225 |
+
|
| 226 |
+
## Out of scope (per your choices)
|
| 227 |
+
|
| 228 |
+
- Pinning HF Space to Llama GGUF for judges (deployment config only — set `ACTIVE_MODEL=minicpm-v-4.6-gguf` in Space secrets; keep `ALLOW_MODEL_SWITCH=false`)
|
| 229 |
+
- llama.cpp multimodal image message plumbing in `LlamaCppBackend` (defer; transformers preset covers VLM demos)
|
| 230 |
+
- Converting fine-tuned LoRA weights to GGUF
|
| 231 |
+
- Separate backend-only toggle (preset-based switching is simpler and already matches factory design)
|
.cursor/plans/todo_last_sprint_tracks_b100b17b.plan.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: TODO Last Sprint Tracks
|
| 3 |
+
overview: "Close remaining hackathon badge gaps after the completed llama backend sprint: publish a gated Well-Tuned adapter via Modal, finish Llama Champion README/TODO hygiene (preset + runtime switching already shipped as minicpm-v-4.6-gguf), and ship Field Notes as in-repo report plus HF blog draft."
|
| 4 |
+
todos:
|
| 5 |
+
- id: well-tuned-publish
|
| 6 |
+
content: Run Modal teaching-lora pipeline (smoke → full publish) and verify MSGEncrypted/minicpm5-1b-teaching-lora is public on Hub
|
| 7 |
+
status: pending
|
| 8 |
+
- id: gguf-preset
|
| 9 |
+
content: "DONE via llama_backend plan: minicpm-v-4.6-gguf preset in models.yaml + .env.example + tests"
|
| 10 |
+
status: completed
|
| 11 |
+
- id: runtime-switching
|
| 12 |
+
content: "DONE via llama_backend plan: set_runtime_model_key, Classic Settings + Studio api_set_active_model"
|
| 13 |
+
status: completed
|
| 14 |
+
- id: llama-docs
|
| 15 |
+
content: "Add Llama Champion to README.md badge targets; cross-link USAGE.md switching section (already written)"
|
| 16 |
+
status: pending
|
| 17 |
+
- id: llama-space-optional
|
| 18 |
+
content: "Optional: pin ACTIVE_MODEL=minicpm-v-4.6-gguf on a dev Space or document local-only verify for judges"
|
| 19 |
+
status: pending
|
| 20 |
+
- id: field-notes-md
|
| 21 |
+
content: Write research/docs/field-notes.md covering skill-matrix QLoRA → lm-eval → gate → Hub publish (include teaching-lora results)
|
| 22 |
+
status: pending
|
| 23 |
+
- id: hf-blog
|
| 24 |
+
content: Adapt field-notes.md into HF blog post and link from README when published
|
| 25 |
+
status: pending
|
| 26 |
+
- id: readme-scorecard
|
| 27 |
+
content: Update README badge targets + TODO.md checkboxes; confirm social post URL
|
| 28 |
+
status: pending
|
| 29 |
+
isProject: false
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
# TODO.md Last Sprint Tracks Plan (updated)
|
| 33 |
+
|
| 34 |
+
Target: **6 merit badges + Bonus Quest Champion** per [TODO.md](TODO.md).
|
| 35 |
+
|
| 36 |
+
**Prerequisite completed:** [llama_backend_model_switching plan](llama_backend_model_switching_77de87de.plan.md) — all todos marked done.
|
| 37 |
+
|
| 38 |
+
```mermaid
|
| 39 |
+
flowchart LR
|
| 40 |
+
subgraph done [Already done]
|
| 41 |
+
OffGrid[Off the Grid]
|
| 42 |
+
OffBrand[Off Brand]
|
| 43 |
+
Sharing[Sharing is Caring]
|
| 44 |
+
LlamaCore["Llama Champion core: minicpm-v-4.6-gguf + runtime switching"]
|
| 45 |
+
end
|
| 46 |
+
subgraph sprint [Remaining sprint]
|
| 47 |
+
WellTuned[Well-Tuned: Modal publish]
|
| 48 |
+
LlamaDocs["Llama Champion: README + TODO hygiene"]
|
| 49 |
+
FieldNotes[Field Notes: blog]
|
| 50 |
+
Hygiene[README scorecard + social]
|
| 51 |
+
end
|
| 52 |
+
done --> Bonus[Bonus Quest Champion]
|
| 53 |
+
WellTuned --> Bonus
|
| 54 |
+
LlamaDocs --> Bonus
|
| 55 |
+
FieldNotes --> Bonus
|
| 56 |
+
Hygiene --> Bonus
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
**Strategy unchanged:** Main HF Space stays on `ACTIVE_MODEL=minicpm5-1b` (transformers) for the live teacher demo + Well-Tuned LoRA story. Llama Champion is satisfied via **local llama.cpp preset + runtime switching** (already implemented), not a Space switch.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## Completed by llama backend sprint (remove from scope)
|
| 64 |
+
|
| 65 |
+
The original Track 2 assumed `minicpm5-1b-gguf`. The implemented approach uses **`minicpm-v-4.6-gguf`** instead — same OpenBMB / Tiny Titan story, official [`openbmb/MiniCPM-V-4.6-gguf`](https://huggingface.co/openbmb/MiniCPM-V-4.6-gguf), and pairs with the existing transformers VLM preset for A/B comparison.
|
| 66 |
+
|
| 67 |
+
| Deliverable | Status | Where |
|
| 68 |
+
|-------------|--------|-------|
|
| 69 |
+
| GGUF preset | Done | [`models.yaml`](models.yaml) `minicpm-v-4.6-gguf` |
|
| 70 |
+
| `.env.example` dev block | Done | Commented `ALLOW_MODEL_SWITCH` + preset hint |
|
| 71 |
+
| Runtime model switching | Done | [`model_loading.py`](apps/gradio-space/src/gradio_space/model_loading.py) `set_runtime_model_key()` |
|
| 72 |
+
| Classic Settings + Chat sync | Done | [`settings_panel.py`](apps/gradio-space/src/gradio_space/ui/settings_panel.py), [`tabs/chat.py`](apps/gradio-space/src/gradio_space/tabs/chat.py) |
|
| 73 |
+
| Studio API + JS sync | Done | [`api/studio.py`](apps/gradio-space/src/gradio_space/api/studio.py), [`studio.js`](apps/gradio-space/static/studio/studio.js) |
|
| 74 |
+
| Local switching docs | Done | [`USAGE.md`](USAGE.md) "Switching models locally", [`apps/gradio-space/README.md`](apps/gradio-space/README.md) |
|
| 75 |
+
| Tests | Done | [`test_config.py`](libs/inference/tests/test_config.py), [`test_model_loading.py`](apps/gradio-space/tests/test_model_loading.py) |
|
| 76 |
+
| TODO.md preset item | Done | `[x] Add minicpm-v-4.6-gguf preset` |
|
| 77 |
+
|
| 78 |
+
**Do not add `minicpm5-1b-gguf`** unless you explicitly want a second GGUF preset for the 1B lesson model — the llama sprint chose V4.6 GGUF deliberately (multimodal family parity, smaller download ~529 MB).
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## Track 1 — Well-Tuned: publish one public adapter (unchanged)
|
| 83 |
+
|
| 84 |
+
**Status:** Pipeline complete; needs operational GPU run.
|
| 85 |
+
|
| 86 |
+
**Primary job:** `teaching-lora` → `MSGEncrypted/minicpm5-1b-teaching-lora`
|
| 87 |
+
|
| 88 |
+
**Run sequence:**
|
| 89 |
+
|
| 90 |
+
1. Smoke: `modal run research/modal/finetune_app.py --job teaching-lora --max-steps 20 --no-publish`
|
| 91 |
+
2. Full publish: `modal run research/modal/finetune_app.py --job teaching-lora` (or `::publish_only` if artifacts exist)
|
| 92 |
+
3. Verify public Hub repo + model card tags from [`render_model_card`](research/modal/_common.py)
|
| 93 |
+
|
| 94 |
+
**Optional Space tie-in:**
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
modal volume get slm-finetune teaching-lora ./models/finetuned/minicpm5-1b-lora
|
| 98 |
+
# ACTIVE_MODEL=minicpm5-1b-lesson-lora
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## Track 2 — Llama Champion: finish docs + badge closure (reduced scope)
|
| 104 |
+
|
| 105 |
+
**What remains** (README + TODO hygiene only — no new preset or switching code):
|
| 106 |
+
|
| 107 |
+
1. **README.md** — add **Llama Champion** to Badge targets section:
|
| 108 |
+
- Preset: `minicpm-v-4.6-gguf` (llama.cpp backend)
|
| 109 |
+
- Local verify: `ALLOW_MODEL_SWITCH=true`, select preset in Settings — link to [USAGE.md switching section](USAGE.md)
|
| 110 |
+
- Note: LoRA lesson presets remain transformers-only; main Space stays `minicpm5-1b`
|
| 111 |
+
|
| 112 |
+
2. **TODO.md** — check off completed items, narrow open ones:
|
| 113 |
+
- `[x]` Add preset (already done)
|
| 114 |
+
- `[ ]` Document llama.cpp path in README (USAGE already done; README pending)
|
| 115 |
+
- `[ ]` "Run Space on llama.cpp" — **optional**: either pin `ACTIVE_MODEL=minicpm-v-4.6-gguf` on a dev Space, or document that local runtime switching satisfies the badge (see llama plan out-of-scope note)
|
| 116 |
+
|
| 117 |
+
3. **Optional polish:** 30s demo clip showing Settings → `minicpm-v-4.6-gguf` → Chat response on llama.cpp
|
| 118 |
+
|
| 119 |
+
**Acceptance:** README documents Llama Champion; TODO scorecard `[ ]` → `[x]` for Llama Champion.
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## Track 3 — Field Notes: in-repo report + HF blog draft (unchanged)
|
| 124 |
+
|
| 125 |
+
Create [`research/docs/field-notes.md`](research/docs/field-notes.md):
|
| 126 |
+
|
| 127 |
+
| Section | Content source |
|
| 128 |
+
|---------|----------------|
|
| 129 |
+
| Problem & stack | README + lesson agent narrative |
|
| 130 |
+
| Skill-matrix design | [`experiments.yaml`](research/modal/experiments.yaml) |
|
| 131 |
+
| Pipeline | train → lm-eval → gate → Hub publish |
|
| 132 |
+
| Modal ops | [`research/modal/README.md`](research/modal/README.md) |
|
| 133 |
+
| Results | `teaching-lora` gate output (after Track 1) |
|
| 134 |
+
| Local inference story | Brief mention of llama.cpp switching (completed sprint) |
|
| 135 |
+
| Repro | Modal commands + Hub adapter link |
|
| 136 |
+
|
| 137 |
+
HF blog: adapt same content (~800–1200 words), link from README.
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
## Track 4 — README + submission hygiene (merged with Track 2)
|
| 142 |
+
|
| 143 |
+
Update [`README.md`](README.md) Badge targets:
|
| 144 |
+
|
| 145 |
+
- **Llama Champion** — `minicpm-v-4.6-gguf`, link to USAGE switching docs
|
| 146 |
+
- **Field Notes** — link to `research/docs/field-notes.md` (+ HF blog when live)
|
| 147 |
+
- **Bonus Quest Champion** — all 6 merit badges qualify
|
| 148 |
+
|
| 149 |
+
Update [`TODO.md`](TODO.md) checkboxes as tracks complete.
|
| 150 |
+
|
| 151 |
+
**Manual:** confirm social post URL in README; Community Choice share.
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## Revised execution order
|
| 156 |
+
|
| 157 |
+
| Step | Type | Est. | Unblocks |
|
| 158 |
+
|------|------|------|----------|
|
| 159 |
+
| 1. Modal `teaching-lora` publish | GPU ops | 1–3 hr | Well-Tuned + Field Notes results |
|
| 160 |
+
| 2. README Llama Champion + badge table | Docs | ~20 min | Llama Champion closure |
|
| 161 |
+
| 3. `research/docs/field-notes.md` | Docs | ~2 hr | Field Notes badge |
|
| 162 |
+
| 4. README/TODO full scorecard | Docs | ~15 min | Submission completeness |
|
| 163 |
+
| 5. HF blog adaptation | Docs | ~1 hr | Judge visibility |
|
| 164 |
+
| 6. Optional Space GGUF pin or demo clip | Ops | ~30 min | Stronger "Run on llama.cpp" checkbox |
|
| 165 |
+
|
| 166 |
+
**Estimated remaining:** ~3–5 hours (Modal GPU dominates; Llama code work is done).
|
| 167 |
+
|
| 168 |
+
---
|
| 169 |
+
|
| 170 |
+
## Out of scope
|
| 171 |
+
|
| 172 |
+
- `minicpm5-1b-gguf` preset (superseded by `minicpm-v-4.6-gguf` decision)
|
| 173 |
+
- New runtime switching code (done)
|
| 174 |
+
- OpenAI / Nemotron / Thousand Token Wood tracks
|
| 175 |
+
- llama.cpp multimodal image plumbing in `LlamaCppBackend`
|
| 176 |
+
- LoRA → GGUF conversion
|
| 177 |
+
- Model verification pipeline — post-hackathon
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
## Risk mitigations
|
| 182 |
+
|
| 183 |
+
| Risk | Mitigation |
|
| 184 |
+
|------|------------|
|
| 185 |
+
| Gate fails on `teaching-lora` | Try `math-lora`; increase `max_steps`; inspect Volume before re-run |
|
| 186 |
+
| Judges expect Space on llama.cpp | README + optional dev Space with `ACTIVE_MODEL=minicpm-v-4.6-gguf`; local switching demo clip |
|
| 187 |
+
| HF blog time crunch | In-repo `field-notes.md` first |
|
.env.example
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# --- Preset selection (models.yaml is the source of truth) ---
|
| 2 |
ACTIVE_MODEL=minicpm5-1b
|
| 3 |
-
#
|
| 4 |
-
ALLOW_MODEL_SWITCH=false
|
| 5 |
# MODEL_PRESETS_PATH=./models.yaml
|
| 6 |
|
| 7 |
# --- Agent outputs ---
|
|
@@ -24,7 +24,13 @@ ALLOW_MODEL_SWITCH=false
|
|
| 24 |
# MODEL_ID=openbmb/MiniCPM5-1B
|
| 25 |
# TRUST_REMOTE_CODE=true
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# --- llama.cpp presets (optional) ---
|
|
|
|
| 28 |
# ACTIVE_MODEL=qwen3b-gguf
|
| 29 |
# INFERENCE_BACKEND=llama_cpp
|
| 30 |
# MODEL_REPO=Qwen/Qwen2.5-3B-Instruct-GGUF
|
|
|
|
| 1 |
# --- Preset selection (models.yaml is the source of truth) ---
|
| 2 |
ACTIVE_MODEL=minicpm5-1b
|
| 3 |
+
# Defaults to true when unset (models.yaml). Space: set false to pin one model for visitors.
|
| 4 |
+
# ALLOW_MODEL_SWITCH=false
|
| 5 |
# MODEL_PRESETS_PATH=./models.yaml
|
| 6 |
|
| 7 |
# --- Agent outputs ---
|
|
|
|
| 24 |
# MODEL_ID=openbmb/MiniCPM5-1B
|
| 25 |
# TRUST_REMOTE_CODE=true
|
| 26 |
|
| 27 |
+
# --- Local dev: switch backends/models in Gradio Settings (Classic + Studio) ---
|
| 28 |
+
# ALLOW_MODEL_SWITCH=true
|
| 29 |
+
# ACTIVE_MODEL=minicpm-v-4.6 # transformers default (or minicpm5-1b)
|
| 30 |
+
# switch in UI to minicpm-v-4.6-gguf for llama.cpp / Llama Champion track
|
| 31 |
+
|
| 32 |
# --- llama.cpp presets (optional) ---
|
| 33 |
+
# ACTIVE_MODEL=minicpm-v-4.6-gguf
|
| 34 |
# ACTIVE_MODEL=qwen3b-gguf
|
| 35 |
# INFERENCE_BACKEND=llama_cpp
|
| 36 |
# MODEL_REPO=Qwen/Qwen2.5-3B-Instruct-GGUF
|
README.md
CHANGED
|
@@ -31,6 +31,8 @@ See **[USAGE.md](USAGE.md)** for local run, Gradio SDK / ZeroGPU Space deploymen
|
|
| 31 |
|
| 32 |
**Demo video:** [https://www.youtube.com/watch?v=bwtOiZvJ-7k](https://www.youtube.com/watch?v=bwtOiZvJ-7k)
|
| 33 |
|
|
|
|
|
|
|
| 34 |
## Prerequisites
|
| 35 |
|
| 36 |
- [uv](https://docs.astral.sh/uv/)
|
|
@@ -80,6 +82,38 @@ modal run research/modal/server_app.py --pipeline # full sweep
|
|
| 80 |
|
| 81 |
Pull a passing adapter into the Space: `modal volume get slm-finetune math-lora ./models/finetuned/minicpm5-1b-lora`, then set `ACTIVE_MODEL=minicpm5-1b-lesson-lora`.
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
## How it works
|
| 84 |
|
| 85 |
1. **Skill** — `skills/education-pptx/SKILL.md` (Hermes / agentskills.io format)
|
|
@@ -109,7 +143,8 @@ Optional research tooling (not required for the Space): see [research/USAGE.md](
|
|
| 109 |
|
| 110 |
| Variable | Default | Description |
|
| 111 |
| -------- | ------- | ----------- |
|
| 112 |
-
| `ACTIVE_MODEL` | `minicpm5-1b` | Preset key from `models.yaml` |
|
|
|
|
| 113 |
| `AGENT_OUTPUTS_DIR` | `/tmp/agent_outputs` | Generated `.pptx` files |
|
| 114 |
| `AGENT_TRACES_DIR` | `outputs/traces` | Agent trace JSON |
|
| 115 |
| `SKILLS_DIR` | `./skills` | Skill definitions root |
|
|
@@ -124,7 +159,7 @@ See [`.env.example`](.env.example) and [`models.yaml`](models.yaml) for model pr
|
|
| 124 |
1. Create a Space under [build-small-hackathon](https://huggingface.co/build-small-hackathon) with **Gradio** SDK (Blank template).
|
| 125 |
2. Link this repository — HF builds from root `app.py` + `requirements.txt` (README YAML above).
|
| 126 |
3. Hardware: **ZeroGPU** for burst GPU inference, or **GPU basic** for always-on GPU.
|
| 127 |
-
4. Set `ACTIVE_MODEL=minicpm5-1b`, `ALLOW_MODEL_SWITCH=false`, `RESEARCHMIND_DATA_DIR=/tmp/researchmind`.
|
| 128 |
|
| 129 |
A root `Dockerfile` is kept for a later **Docker SDK** deploy (flip README to `sdk: docker`). See [USAGE.md](USAGE.md).
|
| 130 |
|
|
@@ -136,10 +171,11 @@ A root `Dockerfile` is kept for a later **Docker SDK** deploy (flip README to `s
|
|
| 136 |
| **Off Brand** | Custom Studio UI at `/` (Gradio 6 Server mode, not default Gradio chrome) |
|
| 137 |
| **Modal** (partner) | GPU `train → eval → gate → publish` on [Modal](https://modal.com) — [`research/modal/`](research/modal/) |
|
| 138 |
| **Well-Tuned** (finetuning) | Skill-matrix QLoRA adapters on MiniCPM5-1B, lm-eval gates, Hub publish |
|
|
|
|
| 139 |
|
| 140 |
- Space live under build-small-hackathon
|
| 141 |
- Demo video: [YouTube](https://www.youtube.com/watch?v=bwtOiZvJ-7k) — real user enters topic → download `.pptx` → show agent trace
|
| 142 |
-
- Social post published
|
| 143 |
- Submission by **June 15, 2026**
|
| 144 |
|
| 145 |
### Badge targets
|
|
@@ -149,6 +185,7 @@ A root `Dockerfile` is kept for a later **Docker SDK** deploy (flip README to `s
|
|
| 149 |
- **OpenBMB** — `openbmb/MiniCPM5-1B`
|
| 150 |
- **Sharing is Caring** — upload traces with `scripts/upload_trace.py`
|
| 151 |
- **Off-the-Grid** — local inference only (no cloud LLM API)
|
|
|
|
| 152 |
- **Well-Tuned** — per-skill QLoRA adapters trained + gated + published via the [Modal + Fine-tuning track](#modal--fine-tuning-track-well-tuned)
|
| 153 |
- **Modal** — same pipeline; see [`research/modal/README.md`](research/modal/README.md)
|
| 154 |
|
|
|
|
| 31 |
|
| 32 |
**Demo video:** [https://www.youtube.com/watch?v=bwtOiZvJ-7k](https://www.youtube.com/watch?v=bwtOiZvJ-7k)
|
| 33 |
|
| 34 |
+
**X post:** [https://x.com/MSG_Encrypted/status/2066570320861921748](https://x.com/MSG_Encrypted/status/2066570320861921748)
|
| 35 |
+
|
| 36 |
## Prerequisites
|
| 37 |
|
| 38 |
- [uv](https://docs.astral.sh/uv/)
|
|
|
|
| 82 |
|
| 83 |
Pull a passing adapter into the Space: `modal volume get slm-finetune math-lora ./models/finetuned/minicpm5-1b-lora`, then set `ACTIVE_MODEL=minicpm5-1b-lesson-lora`.
|
| 84 |
|
| 85 |
+
### Llama track (Llama Champion + Off-the-Grid)
|
| 86 |
+
|
| 87 |
+
The same OpenBMB **MiniCPM-V 4.6** model runs on **llama.cpp** via the [`minicpm-v-4.6-gguf`](models.yaml) preset — GGUF weights from [`openbmb/MiniCPM-V-4.6-gguf`](https://huggingface.co/openbmb/MiniCPM-V-4.6-gguf) (~529 MB Q4_K_M). No cloud LLM API; inference stays fully local through [`libs/inference/src/inference/llama_cpp.py`](libs/inference/src/inference/llama_cpp.py).
|
| 88 |
+
|
| 89 |
+
| Preset | Backend | Use case |
|
| 90 |
+
| ------ | ------- | -------- |
|
| 91 |
+
| `minicpm-v-4.6` | transformers | Full VLM (image/video) via Hugging Face |
|
| 92 |
+
| `minicpm-v-4.6-gguf` | llama.cpp | **Llama Champion** badge; text on all tabs today |
|
| 93 |
+
|
| 94 |
+
**Space (judges):** pin the GGUF preset — no runtime switching for visitors.
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
ACTIVE_MODEL=minicpm-v-4.6-gguf
|
| 98 |
+
ALLOW_MODEL_SWITCH=false
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
**Local dev:** switch backends at runtime without restarting.
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
ALLOW_MODEL_SWITCH=true
|
| 105 |
+
ACTIVE_MODEL=minicpm-v-4.6 # transformers startup default
|
| 106 |
+
# Settings or Chat → select minicpm-v-4.6-gguf for llama.cpp
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Prefetch weights (optional):
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
uv run python scripts/download_model.py --preset minicpm-v-4.6-gguf
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
See [USAGE.md](USAGE.md) (section *Switching models locally*) for Classic and Studio UI details.
|
| 116 |
+
|
| 117 |
## How it works
|
| 118 |
|
| 119 |
1. **Skill** — `skills/education-pptx/SKILL.md` (Hermes / agentskills.io format)
|
|
|
|
| 143 |
|
| 144 |
| Variable | Default | Description |
|
| 145 |
| -------- | ------- | ----------- |
|
| 146 |
+
| `ACTIVE_MODEL` | `minicpm5-1b` | Preset key from `models.yaml` (use `minicpm-v-4.6-gguf` for Llama track) |
|
| 147 |
+
| `ALLOW_MODEL_SWITCH` | `false` | Set `true` locally to switch presets in Settings / Chat |
|
| 148 |
| `AGENT_OUTPUTS_DIR` | `/tmp/agent_outputs` | Generated `.pptx` files |
|
| 149 |
| `AGENT_TRACES_DIR` | `outputs/traces` | Agent trace JSON |
|
| 150 |
| `SKILLS_DIR` | `./skills` | Skill definitions root |
|
|
|
|
| 159 |
1. Create a Space under [build-small-hackathon](https://huggingface.co/build-small-hackathon) with **Gradio** SDK (Blank template).
|
| 160 |
2. Link this repository — HF builds from root `app.py` + `requirements.txt` (README YAML above).
|
| 161 |
3. Hardware: **ZeroGPU** for burst GPU inference, or **GPU basic** for always-on GPU.
|
| 162 |
+
4. Set `ACTIVE_MODEL=minicpm5-1b` (or `minicpm-v-4.6-gguf` for [Llama track](#llama-track-llama-champion--off-the-grid)), `ALLOW_MODEL_SWITCH=false`, `RESEARCHMIND_DATA_DIR=/tmp/researchmind`.
|
| 163 |
|
| 164 |
A root `Dockerfile` is kept for a later **Docker SDK** deploy (flip README to `sdk: docker`). See [USAGE.md](USAGE.md).
|
| 165 |
|
|
|
|
| 171 |
| **Off Brand** | Custom Studio UI at `/` (Gradio 6 Server mode, not default Gradio chrome) |
|
| 172 |
| **Modal** (partner) | GPU `train → eval → gate → publish` on [Modal](https://modal.com) — [`research/modal/`](research/modal/) |
|
| 173 |
| **Well-Tuned** (finetuning) | Skill-matrix QLoRA adapters on MiniCPM5-1B, lm-eval gates, Hub publish |
|
| 174 |
+
| **Llama Champion** | `minicpm-v-4.6-gguf` on llama.cpp — same OpenBMB VLM family, local GGUF inference |
|
| 175 |
|
| 176 |
- Space live under build-small-hackathon
|
| 177 |
- Demo video: [YouTube](https://www.youtube.com/watch?v=bwtOiZvJ-7k) — real user enters topic → download `.pptx` → show agent trace
|
| 178 |
+
- Social post published: [X](https://x.com/MSG_Encrypted/status/2066570320861921748)
|
| 179 |
- Submission by **June 15, 2026**
|
| 180 |
|
| 181 |
### Badge targets
|
|
|
|
| 185 |
- **OpenBMB** — `openbmb/MiniCPM5-1B`
|
| 186 |
- **Sharing is Caring** — upload traces with `scripts/upload_trace.py`
|
| 187 |
- **Off-the-Grid** — local inference only (no cloud LLM API)
|
| 188 |
+
- **Llama Champion** — llama.cpp backend with [`openbmb/MiniCPM-V-4.6-gguf`](https://huggingface.co/openbmb/MiniCPM-V-4.6-gguf); see [Llama track](#llama-track-llama-champion--off-the-grid)
|
| 189 |
- **Well-Tuned** — per-skill QLoRA adapters trained + gated + published via the [Modal + Fine-tuning track](#modal--fine-tuning-track-well-tuned)
|
| 190 |
- **Modal** — same pipeline; see [`research/modal/README.md`](research/modal/README.md)
|
| 191 |
|
TODO.md
CHANGED
|
@@ -15,8 +15,8 @@ below is parked for follow-up PRs.
|
|
| 15 |
|
| 16 |
## 🦙 Llama Champion badge (cheap, high value)
|
| 17 |
- [ ] Run the Space on the **llama.cpp / GGUF** backend (`libs/inference/src/inference/llama_cpp.py`).
|
| 18 |
-
- [
|
| 19 |
-
- [ ] Document the llama.cpp path in README + Space (
|
| 20 |
|
| 21 |
## 📓 Field Notes badge (cheapest miss — no blog exists yet)
|
| 22 |
- [ ] Write a blog post / report on the fine-tuning + Modal pipeline:
|
|
|
|
| 15 |
|
| 16 |
## 🦙 Llama Champion badge (cheap, high value)
|
| 17 |
- [ ] Run the Space on the **llama.cpp / GGUF** backend (`libs/inference/src/inference/llama_cpp.py`).
|
| 18 |
+
- [x] Add `minicpm-v-4.6-gguf` preset (`openbmb/MiniCPM-V-4.6-gguf`) — OpenBMB multimodal on llama.cpp.
|
| 19 |
+
- [ ] Document the llama.cpp path in README + Space (`ACTIVE_MODEL=minicpm-v-4.6-gguf`).
|
| 20 |
|
| 21 |
## 📓 Field Notes badge (cheapest miss — no blog exists yet)
|
| 22 |
- [ ] Write a blog post / report on the fine-tuning + Modal pipeline:
|
USAGE.md
CHANGED
|
@@ -58,6 +58,37 @@ The header in Classic includes a link back to Studio UI.
|
|
| 58 |
|
| 59 |
The model loads on the **first Generate** (Lesson slides) or chat message. Agent traces are written to `outputs/traces/`. After code changes, restart the process to pick up updates.
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
### Lesson slides — research sources
|
| 62 |
|
| 63 |
The **Lesson slides** tab can ground outlines on external sources before building the deck:
|
|
|
|
| 58 |
|
| 59 |
The model loads on the **first Generate** (Lesson slides) or chat message. Agent traces are written to `outputs/traces/`. After code changes, restart the process to pick up updates.
|
| 60 |
|
| 61 |
+
### Switching models locally (transformers ↔ llama.cpp)
|
| 62 |
+
|
| 63 |
+
For local dev you can switch presets at runtime without restarting:
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# .env
|
| 67 |
+
ALLOW_MODEL_SWITCH=true
|
| 68 |
+
ACTIVE_MODEL=minicpm-v-4.6 # startup default (transformers)
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
| UI | Where to switch |
|
| 72 |
+
|----|-----------------|
|
| 73 |
+
| **Classic** (`/classic`) | **Settings** accordion → Model preset dropdown (reloads on change) |
|
| 74 |
+
| **Classic** Chat tab | Model preset dropdown (syncs app-wide) |
|
| 75 |
+
| **Studio** (`/`) | Settings drawer → Model preset; Debug tab has the same list |
|
| 76 |
+
|
| 77 |
+
| Goal | Preset key |
|
| 78 |
+
|------|------------|
|
| 79 |
+
| MiniCPM-V 4.6 transformers (full VLM) | `minicpm-v-4.6` |
|
| 80 |
+
| MiniCPM-V 4.6 llama.cpp / Llama Champion | `minicpm-v-4.6-gguf` |
|
| 81 |
+
| MiniCPM5 1B text | `minicpm5-1b` |
|
| 82 |
+
| Lesson LoRA (transformers only) | `minicpm5-1b-lesson-lora` |
|
| 83 |
+
|
| 84 |
+
Prefetch the GGUF weights (optional):
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
uv run python scripts/download_model.py --preset minicpm-v-4.6-gguf
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
On Hugging Face Space, keep `ALLOW_MODEL_SWITCH=false` and pin one preset via `ACTIVE_MODEL`.
|
| 91 |
+
|
| 92 |
### Lesson slides — research sources
|
| 93 |
|
| 94 |
The **Lesson slides** tab can ground outlines on external sources before building the deck:
|
apps/gradio-space/README.md
CHANGED
|
@@ -41,10 +41,20 @@ This package uses **Gradio 6 Server mode** (`gradio.Server`):
|
|
| 41 |
|
| 42 |
**Settings & debug**
|
| 43 |
|
| 44 |
-
- `model_status`, `model_choices`, `reload_model`
|
| 45 |
- `debug_chat`
|
| 46 |
- `save_upload`
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
## Demo script (judges) — Language lessons + Cohere stack
|
| 49 |
|
| 50 |
**Badge line:** Cohere Labs — Transcribe + Tiny Aya on one local Language lessons page.
|
|
|
|
| 41 |
|
| 42 |
**Settings & debug**
|
| 43 |
|
| 44 |
+
- `model_status`, `model_choices`, `set_active_model`, `reload_model`
|
| 45 |
- `debug_chat`
|
| 46 |
- `save_upload`
|
| 47 |
|
| 48 |
+
### Switching models locally
|
| 49 |
+
|
| 50 |
+
Set `ALLOW_MODEL_SWITCH=true` in `.env` (see [USAGE.md](../../USAGE.md)). The Settings drawer and Classic **Settings** accordion share one runtime preset — changing it reloads weights and applies to Lesson slides, Research, and voice tabs (not just Chat debug).
|
| 51 |
+
|
| 52 |
+
| Preset | Backend |
|
| 53 |
+
|--------|---------|
|
| 54 |
+
| `minicpm-v-4.6` | transformers (full VLM) |
|
| 55 |
+
| `minicpm-v-4.6-gguf` | llama.cpp (Llama Champion track) |
|
| 56 |
+
| `minicpm5-1b` | transformers |
|
| 57 |
+
|
| 58 |
## Demo script (judges) — Language lessons + Cohere stack
|
| 59 |
|
| 60 |
**Badge line:** Cohere Labs — Transcribe + Tiny Aya on one local Language lessons page.
|
apps/gradio-space/src/gradio_space/api/studio.py
CHANGED
|
@@ -26,6 +26,7 @@ from gradio_space.model_loading import (
|
|
| 26 |
get_active_model_key,
|
| 27 |
model_status,
|
| 28 |
reload_model,
|
|
|
|
| 29 |
)
|
| 30 |
from gradio_space.research_helpers import (
|
| 31 |
list_session_choices,
|
|
@@ -873,7 +874,8 @@ def api_model_status() -> dict[str, Any]:
|
|
| 873 |
|
| 874 |
|
| 875 |
def api_model_choices() -> dict[str, Any]:
|
| 876 |
-
|
|
|
|
| 877 |
allow_switch = bool(
|
| 878 |
_app_config.allow_model_switch and len(_app_config.models) > 1
|
| 879 |
)
|
|
@@ -881,7 +883,7 @@ def api_model_choices() -> dict[str, Any]:
|
|
| 881 |
if allow_switch:
|
| 882 |
choices = [{"key": k, "label": label} for label, k in _app_config.model_choices()]
|
| 883 |
return ok(
|
| 884 |
-
active_model=
|
| 885 |
active_label=active.label,
|
| 886 |
active_backend=active.backend,
|
| 887 |
allow_model_switch=allow_switch,
|
|
@@ -891,6 +893,17 @@ def api_model_choices() -> dict[str, Any]:
|
|
| 891 |
)
|
| 892 |
|
| 893 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
def api_reload_model(model_key: str = "") -> dict[str, Any]:
|
| 895 |
key = (model_key or "").strip() or get_active_model_key()
|
| 896 |
status_md = reload_model(key)
|
|
@@ -1211,6 +1224,10 @@ def register_studio_apis(server: gr.Server) -> None:
|
|
| 1211 |
def _model_choices() -> dict[str, Any]:
|
| 1212 |
return api_model_choices()
|
| 1213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1214 |
@server.api(name="reload_model")
|
| 1215 |
def _reload_model(model_key: str = "") -> dict[str, Any]:
|
| 1216 |
return api_reload_model(model_key)
|
|
|
|
| 26 |
get_active_model_key,
|
| 27 |
model_status,
|
| 28 |
reload_model,
|
| 29 |
+
select_and_reload_model,
|
| 30 |
)
|
| 31 |
from gradio_space.research_helpers import (
|
| 32 |
list_session_choices,
|
|
|
|
| 874 |
|
| 875 |
|
| 876 |
def api_model_choices() -> dict[str, Any]:
|
| 877 |
+
key = get_active_model_key()
|
| 878 |
+
active = _app_config.get_model(key)
|
| 879 |
allow_switch = bool(
|
| 880 |
_app_config.allow_model_switch and len(_app_config.models) > 1
|
| 881 |
)
|
|
|
|
| 883 |
if allow_switch:
|
| 884 |
choices = [{"key": k, "label": label} for label, k in _app_config.model_choices()]
|
| 885 |
return ok(
|
| 886 |
+
active_model=key,
|
| 887 |
active_label=active.label,
|
| 888 |
active_backend=active.backend,
|
| 889 |
allow_model_switch=allow_switch,
|
|
|
|
| 893 |
)
|
| 894 |
|
| 895 |
|
| 896 |
+
def api_set_active_model(model_key: str = "") -> dict[str, Any]:
|
| 897 |
+
key = (model_key or "").strip() or get_active_model_key()
|
| 898 |
+
try:
|
| 899 |
+
status_md = select_and_reload_model(key)
|
| 900 |
+
except KeyError as exc:
|
| 901 |
+
return err(str(exc), model_key=key)
|
| 902 |
+
if status_md.lower().startswith("error") or "failed" in status_md.lower():
|
| 903 |
+
return err(status_md, status_markdown=status_md, model_key=key)
|
| 904 |
+
return ok(status_markdown=status_md, model_key=key)
|
| 905 |
+
|
| 906 |
+
|
| 907 |
def api_reload_model(model_key: str = "") -> dict[str, Any]:
|
| 908 |
key = (model_key or "").strip() or get_active_model_key()
|
| 909 |
status_md = reload_model(key)
|
|
|
|
| 1224 |
def _model_choices() -> dict[str, Any]:
|
| 1225 |
return api_model_choices()
|
| 1226 |
|
| 1227 |
+
@server.api(name="set_active_model")
|
| 1228 |
+
def _set_active_model(model_key: str = "") -> dict[str, Any]:
|
| 1229 |
+
return api_set_active_model(model_key)
|
| 1230 |
+
|
| 1231 |
@server.api(name="reload_model")
|
| 1232 |
def _reload_model(model_key: str = "") -> dict[str, Any]:
|
| 1233 |
return api_reload_model(model_key)
|
apps/gradio-space/src/gradio_space/model_loading.py
CHANGED
|
@@ -4,13 +4,29 @@ from inference.factory import get_backend, reset_backend
|
|
| 4 |
from inference.response_clean import strip_reasoning_output
|
| 5 |
|
| 6 |
_app_config = get_app_config()
|
|
|
|
| 7 |
_current_model_key: str | None = None
|
| 8 |
_load_state: dict[str, bool] = {}
|
| 9 |
_load_errors: dict[str, str] = {}
|
| 10 |
|
| 11 |
|
| 12 |
def get_active_model_key() -> str:
|
| 13 |
-
return _app_config.active_model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
def ensure_model_loaded(model_key: str) -> str | None:
|
|
@@ -53,7 +69,7 @@ def runtime_device_hint(model_key: str) -> str:
|
|
| 53 |
|
| 54 |
|
| 55 |
def warmup(model_key: str | None = None) -> str:
|
| 56 |
-
key = model_key or
|
| 57 |
model = get_model_config(key)
|
| 58 |
|
| 59 |
if _load_state.get(key):
|
|
@@ -80,7 +96,8 @@ def reload_model(model_key: str) -> str:
|
|
| 80 |
"""Clear cached backend and reload weights for settings panel."""
|
| 81 |
global _current_model_key
|
| 82 |
|
| 83 |
-
key = model_key or
|
|
|
|
| 84 |
reset_backend()
|
| 85 |
_current_model_key = None
|
| 86 |
_load_state.pop(key, None)
|
|
@@ -91,6 +108,11 @@ def reload_model(model_key: str) -> str:
|
|
| 91 |
return warmup(key)
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def preload_active_model() -> str:
|
| 95 |
"""Load the active preset at startup so the first request is fast."""
|
| 96 |
key = get_active_model_key()
|
|
@@ -106,7 +128,16 @@ def preload_active_model() -> str:
|
|
| 106 |
|
| 107 |
def model_status(model_key: str) -> str:
|
| 108 |
model = get_model_config(model_key)
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
|
| 112 |
def _history_to_messages(history: list) -> list[dict[str, str]]:
|
|
|
|
| 4 |
from inference.response_clean import strip_reasoning_output
|
| 5 |
|
| 6 |
_app_config = get_app_config()
|
| 7 |
+
_runtime_model_key: str | None = None
|
| 8 |
_current_model_key: str | None = None
|
| 9 |
_load_state: dict[str, bool] = {}
|
| 10 |
_load_errors: dict[str, str] = {}
|
| 11 |
|
| 12 |
|
| 13 |
def get_active_model_key() -> str:
|
| 14 |
+
return _runtime_model_key or _app_config.active_model
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def set_runtime_model_key(key: str) -> str:
|
| 18 |
+
"""Pin the active preset for all tabs until process restart."""
|
| 19 |
+
global _runtime_model_key, _current_model_key
|
| 20 |
+
|
| 21 |
+
model = get_model_config(key)
|
| 22 |
+
previous = get_active_model_key()
|
| 23 |
+
if key != previous:
|
| 24 |
+
reset_backend()
|
| 25 |
+
_current_model_key = None
|
| 26 |
+
_load_state.pop(previous, None)
|
| 27 |
+
_load_errors.pop(previous, None)
|
| 28 |
+
_runtime_model_key = key
|
| 29 |
+
return model.label
|
| 30 |
|
| 31 |
|
| 32 |
def ensure_model_loaded(model_key: str) -> str | None:
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
def warmup(model_key: str | None = None) -> str:
|
| 72 |
+
key = model_key or get_active_model_key()
|
| 73 |
model = get_model_config(key)
|
| 74 |
|
| 75 |
if _load_state.get(key):
|
|
|
|
| 96 |
"""Clear cached backend and reload weights for settings panel."""
|
| 97 |
global _current_model_key
|
| 98 |
|
| 99 |
+
key = model_key or get_active_model_key()
|
| 100 |
+
set_runtime_model_key(key)
|
| 101 |
reset_backend()
|
| 102 |
_current_model_key = None
|
| 103 |
_load_state.pop(key, None)
|
|
|
|
| 108 |
return warmup(key)
|
| 109 |
|
| 110 |
|
| 111 |
+
def select_and_reload_model(model_key: str) -> str:
|
| 112 |
+
"""Switch runtime preset and load weights (Settings dropdown)."""
|
| 113 |
+
return reload_model(model_key)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
def preload_active_model() -> str:
|
| 117 |
"""Load the active preset at startup so the first request is fast."""
|
| 118 |
key = get_active_model_key()
|
|
|
|
| 128 |
|
| 129 |
def model_status(model_key: str) -> str:
|
| 130 |
model = get_model_config(model_key)
|
| 131 |
+
notes = ""
|
| 132 |
+
if model.backend == "llama_cpp" and model.multimodal:
|
| 133 |
+
notes = (
|
| 134 |
+
"\n- Note: text-only on llama.cpp; use transformers preset for image/video input."
|
| 135 |
+
)
|
| 136 |
+
return (
|
| 137 |
+
f"**{model.label}**\n\n"
|
| 138 |
+
f"- Backend: `{model.backend}`\n"
|
| 139 |
+
f"- {warmup(model_key)}{notes}"
|
| 140 |
+
)
|
| 141 |
|
| 142 |
|
| 143 |
def _history_to_messages(history: list) -> list[dict[str, str]]:
|
apps/gradio-space/src/gradio_space/server.py
CHANGED
|
@@ -23,7 +23,7 @@ from gradio_space.ui.theme import get_theme, load_css
|
|
| 23 |
_PKG_ROOT = Path(__file__).resolve().parent
|
| 24 |
_APP_ROOT = _PKG_ROOT.parents[1]
|
| 25 |
_STATIC_DIR = _APP_ROOT / "static" / "studio"
|
| 26 |
-
_STUDIO_ASSET_VERSION = "
|
| 27 |
_STUDIO_INDEX_HTML = _STATIC_DIR / "index.html"
|
| 28 |
|
| 29 |
|
|
|
|
| 23 |
_PKG_ROOT = Path(__file__).resolve().parent
|
| 24 |
_APP_ROOT = _PKG_ROOT.parents[1]
|
| 25 |
_STATIC_DIR = _APP_ROOT / "static" / "studio"
|
| 26 |
+
_STUDIO_ASSET_VERSION = "20260615b"
|
| 27 |
_STUDIO_INDEX_HTML = _STATIC_DIR / "index.html"
|
| 28 |
|
| 29 |
|
apps/gradio-space/src/gradio_space/tabs/chat.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
|
|
|
|
| 3 |
from gradio_space.research_helpers import (
|
| 4 |
list_session_choices,
|
| 5 |
rag_aware_chat,
|
|
@@ -29,7 +30,7 @@ def build_chat_tab(workspace: WorkspaceWidgets) -> None:
|
|
| 29 |
"Plain chat or corpus-grounded answers — traces appear in Advanced when RAG is on."
|
| 30 |
)
|
| 31 |
|
| 32 |
-
model_key =
|
| 33 |
|
| 34 |
with gr.Group():
|
| 35 |
gr.Markdown("#### RAG scope (override workspace defaults)")
|
|
@@ -57,11 +58,17 @@ def build_chat_tab(workspace: WorkspaceWidgets) -> None:
|
|
| 57 |
if _app_config.allow_model_switch and len(_app_config.models) > 1:
|
| 58 |
model_dropdown = gr.Dropdown(
|
| 59 |
choices=_app_config.model_choices(),
|
| 60 |
-
value=
|
| 61 |
label="Model preset (debug override)",
|
| 62 |
)
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
def _chat(message, history, mkey, use_rag_flag, sid, docs, ws_sid, ws_docs):
|
|
|
|
| 65 |
sid = resolve_session(sid, ws_sid)
|
| 66 |
docs = resolve_doc_ids(docs, ws_docs)
|
| 67 |
reply, trace_json, trace_summary = rag_aware_chat(
|
|
@@ -83,7 +90,7 @@ def build_chat_tab(workspace: WorkspaceWidgets) -> None:
|
|
| 83 |
examples=[
|
| 84 |
[
|
| 85 |
"What do my ingested sources say about AI agents?",
|
| 86 |
-
|
| 87 |
True,
|
| 88 |
"",
|
| 89 |
[],
|
|
@@ -92,7 +99,7 @@ def build_chat_tab(workspace: WorkspaceWidgets) -> None:
|
|
| 92 |
],
|
| 93 |
[
|
| 94 |
"Hello! What can you help me with?",
|
| 95 |
-
|
| 96 |
False,
|
| 97 |
"",
|
| 98 |
[],
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
|
| 3 |
+
from gradio_space.model_loading import get_active_model_key, set_runtime_model_key
|
| 4 |
from gradio_space.research_helpers import (
|
| 5 |
list_session_choices,
|
| 6 |
rag_aware_chat,
|
|
|
|
| 30 |
"Plain chat or corpus-grounded answers — traces appear in Advanced when RAG is on."
|
| 31 |
)
|
| 32 |
|
| 33 |
+
model_key = get_active_model_key()
|
| 34 |
|
| 35 |
with gr.Group():
|
| 36 |
gr.Markdown("#### RAG scope (override workspace defaults)")
|
|
|
|
| 58 |
if _app_config.allow_model_switch and len(_app_config.models) > 1:
|
| 59 |
model_dropdown = gr.Dropdown(
|
| 60 |
choices=_app_config.model_choices(),
|
| 61 |
+
value=get_active_model_key(),
|
| 62 |
label="Model preset (debug override)",
|
| 63 |
)
|
| 64 |
|
| 65 |
+
def _on_model_change(mkey: str) -> None:
|
| 66 |
+
set_runtime_model_key(mkey)
|
| 67 |
+
|
| 68 |
+
model_dropdown.change(fn=_on_model_change, inputs=model_dropdown)
|
| 69 |
+
|
| 70 |
def _chat(message, history, mkey, use_rag_flag, sid, docs, ws_sid, ws_docs):
|
| 71 |
+
set_runtime_model_key(mkey)
|
| 72 |
sid = resolve_session(sid, ws_sid)
|
| 73 |
docs = resolve_doc_ids(docs, ws_docs)
|
| 74 |
reply, trace_json, trace_summary = rag_aware_chat(
|
|
|
|
| 90 |
examples=[
|
| 91 |
[
|
| 92 |
"What do my ingested sources say about AI agents?",
|
| 93 |
+
get_active_model_key(),
|
| 94 |
True,
|
| 95 |
"",
|
| 96 |
[],
|
|
|
|
| 99 |
],
|
| 100 |
[
|
| 101 |
"Hello! What can you help me with?",
|
| 102 |
+
get_active_model_key(),
|
| 103 |
False,
|
| 104 |
"",
|
| 105 |
[],
|
apps/gradio-space/src/gradio_space/ui/settings_panel.py
CHANGED
|
@@ -3,7 +3,12 @@ from __future__ import annotations
|
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
from echocoach.config import get_echo_coach_config
|
| 6 |
-
from gradio_space.model_loading import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from inference.config import get_app_config
|
| 8 |
from researchmind.config import get_config as get_research_config
|
| 9 |
|
|
@@ -39,11 +44,17 @@ def _paths_summary() -> str:
|
|
| 39 |
def build_settings_panel() -> tuple[gr.Dropdown | None, gr.Markdown, gr.Button]:
|
| 40 |
"""Build settings accordion contents. Returns (model_dropdown or None, status_md, reload_btn)."""
|
| 41 |
model_dropdown: gr.Dropdown | None = None
|
|
|
|
| 42 |
|
| 43 |
if _app_config.allow_model_switch and len(_app_config.models) > 1:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
model_dropdown = gr.Dropdown(
|
| 45 |
choices=_app_config.model_choices(),
|
| 46 |
-
value=
|
| 47 |
label="Model preset",
|
| 48 |
)
|
| 49 |
else:
|
|
@@ -53,7 +64,7 @@ def build_settings_panel() -> tuple[gr.Dropdown | None, gr.Markdown, gr.Button]:
|
|
| 53 |
f"**Backend:** `{active.backend}`"
|
| 54 |
)
|
| 55 |
|
| 56 |
-
status_md = gr.Markdown(value=model_status(
|
| 57 |
gr.Markdown("#### Voice stack")
|
| 58 |
gr.Markdown(_voice_stack_summary())
|
| 59 |
with gr.Accordion("Paths & files", open=False):
|
|
@@ -62,13 +73,17 @@ def build_settings_panel() -> tuple[gr.Dropdown | None, gr.Markdown, gr.Button]:
|
|
| 62 |
reload_btn = gr.Button("Reload model", variant="secondary", size="sm")
|
| 63 |
|
| 64 |
if model_dropdown is not None:
|
| 65 |
-
model_dropdown.change(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
if model_dropdown is not None:
|
| 68 |
reload_btn.click(fn=reload_model, inputs=[model_dropdown], outputs=status_md)
|
| 69 |
else:
|
| 70 |
reload_btn.click(
|
| 71 |
-
fn=lambda: reload_model(
|
| 72 |
outputs=status_md,
|
| 73 |
)
|
| 74 |
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
from echocoach.config import get_echo_coach_config
|
| 6 |
+
from gradio_space.model_loading import (
|
| 7 |
+
get_active_model_key,
|
| 8 |
+
model_status,
|
| 9 |
+
reload_model,
|
| 10 |
+
select_and_reload_model,
|
| 11 |
+
)
|
| 12 |
from inference.config import get_app_config
|
| 13 |
from researchmind.config import get_config as get_research_config
|
| 14 |
|
|
|
|
| 44 |
def build_settings_panel() -> tuple[gr.Dropdown | None, gr.Markdown, gr.Button]:
|
| 45 |
"""Build settings accordion contents. Returns (model_dropdown or None, status_md, reload_btn)."""
|
| 46 |
model_dropdown: gr.Dropdown | None = None
|
| 47 |
+
active_key = get_active_model_key()
|
| 48 |
|
| 49 |
if _app_config.allow_model_switch and len(_app_config.models) > 1:
|
| 50 |
+
active = _app_config.get_model(active_key)
|
| 51 |
+
gr.Markdown(
|
| 52 |
+
f"**Runtime model:** `{active.key}` — {active.label} \n"
|
| 53 |
+
f"**Backend:** `{active.backend}`"
|
| 54 |
+
)
|
| 55 |
model_dropdown = gr.Dropdown(
|
| 56 |
choices=_app_config.model_choices(),
|
| 57 |
+
value=active_key,
|
| 58 |
label="Model preset",
|
| 59 |
)
|
| 60 |
else:
|
|
|
|
| 64 |
f"**Backend:** `{active.backend}`"
|
| 65 |
)
|
| 66 |
|
| 67 |
+
status_md = gr.Markdown(value=model_status(active_key))
|
| 68 |
gr.Markdown("#### Voice stack")
|
| 69 |
gr.Markdown(_voice_stack_summary())
|
| 70 |
with gr.Accordion("Paths & files", open=False):
|
|
|
|
| 73 |
reload_btn = gr.Button("Reload model", variant="secondary", size="sm")
|
| 74 |
|
| 75 |
if model_dropdown is not None:
|
| 76 |
+
model_dropdown.change(
|
| 77 |
+
fn=select_and_reload_model,
|
| 78 |
+
inputs=model_dropdown,
|
| 79 |
+
outputs=status_md,
|
| 80 |
+
)
|
| 81 |
|
| 82 |
if model_dropdown is not None:
|
| 83 |
reload_btn.click(fn=reload_model, inputs=[model_dropdown], outputs=status_md)
|
| 84 |
else:
|
| 85 |
reload_btn.click(
|
| 86 |
+
fn=lambda: reload_model(get_active_model_key()),
|
| 87 |
outputs=status_md,
|
| 88 |
)
|
| 89 |
|
apps/gradio-space/static/studio/studio.js
CHANGED
|
@@ -26,9 +26,21 @@ function toggleTheme() {
|
|
| 26 |
applyTheme(getPreferredTheme());
|
| 27 |
|
| 28 |
function appOrigin() {
|
| 29 |
-
const { protocol, hostname } = window.location;
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
const SLIDE_PIPELINE_STEPS = [
|
|
@@ -1125,6 +1137,15 @@ async function initVoicePresets() {
|
|
| 1125 |
return initLanguageLessons();
|
| 1126 |
}
|
| 1127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1128 |
async function initSettings() {
|
| 1129 |
const data = await callApi("model_choices", []);
|
| 1130 |
state.modelChoices = data;
|
|
@@ -1147,10 +1168,20 @@ async function initSettings() {
|
|
| 1147 |
if (select) {
|
| 1148 |
select.innerHTML = options;
|
| 1149 |
select.value = data.active_model;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1150 |
}
|
| 1151 |
if (debugSelect) {
|
| 1152 |
debugSelect.innerHTML = options;
|
| 1153 |
debugSelect.value = data.active_model;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1154 |
}
|
| 1155 |
}
|
| 1156 |
}
|
|
|
|
| 26 |
applyTheme(getPreferredTheme());
|
| 27 |
|
| 28 |
function appOrigin() {
|
| 29 |
+
const { protocol, hostname, port } = window.location;
|
| 30 |
+
if (protocol === "https:") {
|
| 31 |
+
return window.location.origin;
|
| 32 |
+
}
|
| 33 |
+
const isLocal =
|
| 34 |
+
hostname === "localhost" ||
|
| 35 |
+
hostname === "127.0.0.1" ||
|
| 36 |
+
hostname === "[::1]" ||
|
| 37 |
+
hostname === "0.0.0.0";
|
| 38 |
+
if (isLocal) {
|
| 39 |
+
return window.location.origin;
|
| 40 |
+
}
|
| 41 |
+
// HF Spaces: TLS terminates at the edge; Gradio client must use https.
|
| 42 |
+
const portSuffix = port ? `:${port}` : "";
|
| 43 |
+
return `https://${hostname}${portSuffix}`;
|
| 44 |
}
|
| 45 |
|
| 46 |
const SLIDE_PIPELINE_STEPS = [
|
|
|
|
| 1137 |
return initLanguageLessons();
|
| 1138 |
}
|
| 1139 |
|
| 1140 |
+
async function selectActiveModel(key) {
|
| 1141 |
+
const data = await callApi("set_active_model", [key]);
|
| 1142 |
+
$("#settings-status").innerHTML = renderMarkdownLite(data.status_markdown || "");
|
| 1143 |
+
const fresh = await callApi("model_choices", []);
|
| 1144 |
+
state.modelChoices = fresh;
|
| 1145 |
+
$("#settings-active-model").textContent = `${fresh.active_label} (${fresh.active_backend})`;
|
| 1146 |
+
return data;
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
async function initSettings() {
|
| 1150 |
const data = await callApi("model_choices", []);
|
| 1151 |
state.modelChoices = data;
|
|
|
|
| 1168 |
if (select) {
|
| 1169 |
select.innerHTML = options;
|
| 1170 |
select.value = data.active_model;
|
| 1171 |
+
select.onchange = () => {
|
| 1172 |
+
const key = select.value;
|
| 1173 |
+
if (debugSelect) debugSelect.value = key;
|
| 1174 |
+
selectActiveModel(key).catch(() => {});
|
| 1175 |
+
};
|
| 1176 |
}
|
| 1177 |
if (debugSelect) {
|
| 1178 |
debugSelect.innerHTML = options;
|
| 1179 |
debugSelect.value = data.active_model;
|
| 1180 |
+
debugSelect.onchange = () => {
|
| 1181 |
+
const key = debugSelect.value;
|
| 1182 |
+
if (select) select.value = key;
|
| 1183 |
+
selectActiveModel(key).catch(() => {});
|
| 1184 |
+
};
|
| 1185 |
}
|
| 1186 |
}
|
| 1187 |
}
|
apps/gradio-space/tests/test_model_loading.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@pytest.fixture
|
| 7 |
+
def model_loading_module(monkeypatch, tmp_path):
|
| 8 |
+
presets = tmp_path / "models.yaml"
|
| 9 |
+
presets.write_text(
|
| 10 |
+
"""
|
| 11 |
+
defaults:
|
| 12 |
+
active_model: alpha
|
| 13 |
+
allow_model_switch: true
|
| 14 |
+
models:
|
| 15 |
+
alpha:
|
| 16 |
+
label: Alpha
|
| 17 |
+
backend: transformers
|
| 18 |
+
model_id: openbmb/MiniCPM5-1B
|
| 19 |
+
beta:
|
| 20 |
+
label: Beta GGUF
|
| 21 |
+
backend: llama_cpp
|
| 22 |
+
model_repo: openbmb/MiniCPM-V-4.6-gguf
|
| 23 |
+
model_file: MiniCPM-V-4.6-Q4_K_M.gguf
|
| 24 |
+
multimodal: true
|
| 25 |
+
"""
|
| 26 |
+
)
|
| 27 |
+
monkeypatch.chdir(tmp_path)
|
| 28 |
+
monkeypatch.delenv("ACTIVE_MODEL", raising=False)
|
| 29 |
+
|
| 30 |
+
import inference.config as inference_config
|
| 31 |
+
import gradio_space.model_loading as model_loading
|
| 32 |
+
|
| 33 |
+
importlib.reload(inference_config)
|
| 34 |
+
importlib.reload(model_loading)
|
| 35 |
+
return model_loading
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_runtime_model_key_override(model_loading_module):
|
| 39 |
+
ml = model_loading_module
|
| 40 |
+
assert ml.get_active_model_key() == "alpha"
|
| 41 |
+
ml.set_runtime_model_key("beta")
|
| 42 |
+
assert ml.get_active_model_key() == "beta"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_set_runtime_model_key_unknown_raises(model_loading_module):
|
| 46 |
+
ml = model_loading_module
|
| 47 |
+
with pytest.raises(KeyError):
|
| 48 |
+
ml.set_runtime_model_key("missing")
|
libs/inference/src/inference/config.py
CHANGED
|
@@ -74,7 +74,7 @@ class AppConfig:
|
|
| 74 |
|
| 75 |
active_model: str
|
| 76 |
models: dict[str, ModelConfig]
|
| 77 |
-
allow_model_switch: bool =
|
| 78 |
model_cache_dir: str | None = None
|
| 79 |
presets_path: Path | None = None
|
| 80 |
|
|
@@ -238,7 +238,7 @@ def load_app_config() -> AppConfig:
|
|
| 238 |
|
| 239 |
allow_model_switch = os.environ.get("ALLOW_MODEL_SWITCH")
|
| 240 |
if allow_model_switch is None:
|
| 241 |
-
allow_switch = bool(defaults.get("allow_model_switch",
|
| 242 |
else:
|
| 243 |
allow_switch = allow_model_switch.lower() in {"1", "true", "yes"}
|
| 244 |
|
|
|
|
| 74 |
|
| 75 |
active_model: str
|
| 76 |
models: dict[str, ModelConfig]
|
| 77 |
+
allow_model_switch: bool = True
|
| 78 |
model_cache_dir: str | None = None
|
| 79 |
presets_path: Path | None = None
|
| 80 |
|
|
|
|
| 238 |
|
| 239 |
allow_model_switch = os.environ.get("ALLOW_MODEL_SWITCH")
|
| 240 |
if allow_model_switch is None:
|
| 241 |
+
allow_switch = bool(defaults.get("allow_model_switch", True))
|
| 242 |
else:
|
| 243 |
allow_switch = allow_model_switch.lower() in {"1", "true", "yes"}
|
| 244 |
|
libs/inference/src/inference/transformers.py
CHANGED
|
@@ -72,16 +72,26 @@ class TransformersBackend:
|
|
| 72 |
)
|
| 73 |
|
| 74 |
if self._config.adapter_path:
|
|
|
|
| 75 |
from pathlib import Path
|
| 76 |
|
| 77 |
from peft import PeftModel
|
| 78 |
|
| 79 |
-
adapter =
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
raise FileNotFoundError(
|
| 82 |
-
f"LoRA adapter not found for preset {self._config.key!r}:
|
|
|
|
| 83 |
)
|
| 84 |
-
self._model = PeftModel.from_pretrained(self._model,
|
| 85 |
|
| 86 |
if plan.device == "cpu":
|
| 87 |
assert self._model is not None
|
|
|
|
| 72 |
)
|
| 73 |
|
| 74 |
if self._config.adapter_path:
|
| 75 |
+
import re
|
| 76 |
from pathlib import Path
|
| 77 |
|
| 78 |
from peft import PeftModel
|
| 79 |
|
| 80 |
+
adapter = self._config.adapter_path
|
| 81 |
+
adapter_dir = Path(adapter)
|
| 82 |
+
if adapter_dir.is_dir():
|
| 83 |
+
# Local adapter (e.g. pulled from the Modal Volume).
|
| 84 |
+
adapter_src = str(adapter_dir)
|
| 85 |
+
elif re.fullmatch(r"[\w.-]+/[\w.-]+", adapter):
|
| 86 |
+
# Hugging Face Hub repo id (e.g. the Modal-published adapter) —
|
| 87 |
+
# PeftModel fetches it remotely; no manual pull required.
|
| 88 |
+
adapter_src = adapter
|
| 89 |
+
else:
|
| 90 |
raise FileNotFoundError(
|
| 91 |
+
f"LoRA adapter not found for preset {self._config.key!r}: "
|
| 92 |
+
f"{adapter} (expected a local dir or a Hub repo id 'org/name')"
|
| 93 |
)
|
| 94 |
+
self._model = PeftModel.from_pretrained(self._model, adapter_src)
|
| 95 |
|
| 96 |
if plan.device == "cpu":
|
| 97 |
assert self._model is not None
|
libs/inference/tests/test_config.py
CHANGED
|
@@ -30,6 +30,51 @@ models:
|
|
| 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(
|
|
@@ -54,6 +99,24 @@ models:
|
|
| 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()
|
|
|
|
| 30 |
assert config.get_model("demo").model_repo == "org/model-GGUF"
|
| 31 |
|
| 32 |
|
| 33 |
+
def test_allow_model_switch_defaults_true_without_env(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 preset
|
| 42 |
+
backend: llama_cpp
|
| 43 |
+
model_repo: org/model-GGUF
|
| 44 |
+
model_file: demo.gguf
|
| 45 |
+
"""
|
| 46 |
+
)
|
| 47 |
+
monkeypatch.chdir(tmp_path)
|
| 48 |
+
monkeypatch.delenv("ALLOW_MODEL_SWITCH", raising=False)
|
| 49 |
+
|
| 50 |
+
config = load_app_config()
|
| 51 |
+
|
| 52 |
+
assert config.allow_model_switch is True
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_allow_model_switch_env_false_overrides(tmp_path, monkeypatch):
|
| 56 |
+
presets = tmp_path / "models.yaml"
|
| 57 |
+
presets.write_text(
|
| 58 |
+
"""
|
| 59 |
+
defaults:
|
| 60 |
+
active_model: demo
|
| 61 |
+
allow_model_switch: true
|
| 62 |
+
models:
|
| 63 |
+
demo:
|
| 64 |
+
label: Demo preset
|
| 65 |
+
backend: llama_cpp
|
| 66 |
+
model_repo: org/model-GGUF
|
| 67 |
+
model_file: demo.gguf
|
| 68 |
+
"""
|
| 69 |
+
)
|
| 70 |
+
monkeypatch.chdir(tmp_path)
|
| 71 |
+
monkeypatch.setenv("ALLOW_MODEL_SWITCH", "false")
|
| 72 |
+
|
| 73 |
+
config = load_app_config()
|
| 74 |
+
|
| 75 |
+
assert config.allow_model_switch is False
|
| 76 |
+
|
| 77 |
+
|
| 78 |
def test_legacy_env_overrides_active_preset(tmp_path, monkeypatch):
|
| 79 |
presets = tmp_path / "models.yaml"
|
| 80 |
presets.write_text(
|
|
|
|
| 99 |
assert model.model_file == "override.gguf"
|
| 100 |
|
| 101 |
|
| 102 |
+
def test_minicpm_v_gguf_preset_from_repo(monkeypatch):
|
| 103 |
+
repo_root = Path(__file__).resolve().parents[3]
|
| 104 |
+
models_yaml = repo_root / "models.yaml"
|
| 105 |
+
if not models_yaml.is_file():
|
| 106 |
+
pytest.skip("repo models.yaml not found")
|
| 107 |
+
|
| 108 |
+
monkeypatch.chdir(repo_root)
|
| 109 |
+
monkeypatch.delenv("ACTIVE_MODEL", raising=False)
|
| 110 |
+
monkeypatch.delenv("ALLOW_MODEL_SWITCH", raising=False)
|
| 111 |
+
|
| 112 |
+
model = load_app_config().get_model("minicpm-v-4.6-gguf")
|
| 113 |
+
|
| 114 |
+
assert model.backend == "llama_cpp"
|
| 115 |
+
assert model.multimodal is True
|
| 116 |
+
assert model.model_repo == "openbmb/MiniCPM-V-4.6-gguf"
|
| 117 |
+
assert model.model_file == "MiniCPM-V-4.6-Q4_K_M.gguf"
|
| 118 |
+
|
| 119 |
+
|
| 120 |
def test_resolve_relative_model_path(tmp_path, monkeypatch):
|
| 121 |
local_dir = tmp_path / "gemma_merged_model"
|
| 122 |
local_dir.mkdir()
|
models.yaml
CHANGED
|
@@ -5,9 +5,8 @@ defaults:
|
|
| 5 |
# active_model: minicpm-v-4.6
|
| 6 |
active_model: minicpm5-1b
|
| 7 |
|
| 8 |
-
#
|
| 9 |
-
|
| 10 |
-
allow_model_switch: false
|
| 11 |
|
| 12 |
models:
|
| 13 |
minicpm-v-4.6:
|
|
@@ -17,6 +16,15 @@ models:
|
|
| 17 |
trust_remote_code: true
|
| 18 |
multimodal: true
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
qwen3b-gguf:
|
| 21 |
label: Qwen 2.5 3B Instruct (GGUF)
|
| 22 |
backend: llama_cpp
|
|
@@ -68,6 +76,24 @@ models:
|
|
| 68 |
model_id: ./models/finetuned/minicpm5-1b-lora-merged
|
| 69 |
trust_remote_code: true
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
tiny-aya-global:
|
| 72 |
label: Tiny Aya Global 3.3B (multilingual coach)
|
| 73 |
backend: transformers
|
|
|
|
| 5 |
# active_model: minicpm-v-4.6
|
| 6 |
active_model: minicpm5-1b
|
| 7 |
|
| 8 |
+
# Default true for local dev (dropdown in Gradio). Space: set ALLOW_MODEL_SWITCH=false.
|
| 9 |
+
allow_model_switch: true
|
|
|
|
| 10 |
|
| 11 |
models:
|
| 12 |
minicpm-v-4.6:
|
|
|
|
| 16 |
trust_remote_code: true
|
| 17 |
multimodal: true
|
| 18 |
|
| 19 |
+
minicpm-v-4.6-gguf:
|
| 20 |
+
label: MiniCPM-V 4.6 (GGUF / llama.cpp)
|
| 21 |
+
backend: llama_cpp
|
| 22 |
+
model_repo: openbmb/MiniCPM-V-4.6-gguf
|
| 23 |
+
model_file: MiniCPM-V-4.6-Q4_K_M.gguf
|
| 24 |
+
multimodal: true
|
| 25 |
+
n_ctx: 8192
|
| 26 |
+
n_gpu_layers: 0
|
| 27 |
+
|
| 28 |
qwen3b-gguf:
|
| 29 |
label: Qwen 2.5 3B Instruct (GGUF)
|
| 30 |
backend: llama_cpp
|
|
|
|
| 76 |
model_id: ./models/finetuned/minicpm5-1b-lora-merged
|
| 77 |
trust_remote_code: true
|
| 78 |
|
| 79 |
+
# Well-Tuned track: base MiniCPM5-1B + a Modal-published LoRA adapter pulled
|
| 80 |
+
# straight from the Hub (no local files needed). These point at the repos the
|
| 81 |
+
# finetune pipeline publishes once a job clears its lm-eval gate
|
| 82 |
+
# (research/modal/experiments.yaml -> publish.hub_repo).
|
| 83 |
+
minicpm5-1b-teaching-hub:
|
| 84 |
+
label: MiniCPM5 1B teaching LoRA (Hub, fine-tuned)
|
| 85 |
+
backend: transformers
|
| 86 |
+
model_id: openbmb/MiniCPM5-1B
|
| 87 |
+
adapter_path: MSGEncrypted/minicpm5-1b-teaching-lora
|
| 88 |
+
trust_remote_code: true
|
| 89 |
+
|
| 90 |
+
minicpm5-1b-math-hub:
|
| 91 |
+
label: MiniCPM5 1B math LoRA (Hub, fine-tuned)
|
| 92 |
+
backend: transformers
|
| 93 |
+
model_id: openbmb/MiniCPM5-1B
|
| 94 |
+
adapter_path: MSGEncrypted/minicpm5-1b-math-lora
|
| 95 |
+
trust_remote_code: true
|
| 96 |
+
|
| 97 |
tiny-aya-global:
|
| 98 |
label: Tiny Aya Global 3.3B (multilingual coach)
|
| 99 |
backend: transformers
|
requirements.txt
CHANGED
|
@@ -3,8 +3,9 @@
|
|
| 3 |
|
| 4 |
# Pinned runtime deps (do not pin gradio, spaces, or huggingface_hub — HF preinstalls them)
|
| 5 |
accelerate==1.13.0
|
| 6 |
-
torch
|
| 7 |
-
|
|
|
|
| 8 |
transformers==5.10.2
|
| 9 |
peft==0.19.1
|
| 10 |
# llama-cpp-python omitted — compiles from source on HF (10+ min / timeout).
|
|
|
|
| 3 |
|
| 4 |
# Pinned runtime deps (do not pin gradio, spaces, or huggingface_hub — HF preinstalls them)
|
| 5 |
accelerate==1.13.0
|
| 6 |
+
# ZeroGPU supports torch 2.8–2.11 only (not 2.12).
|
| 7 |
+
torch==2.11.0
|
| 8 |
+
torchvision==0.26.0
|
| 9 |
transformers==5.10.2
|
| 10 |
peft==0.19.1
|
| 11 |
# llama-cpp-python omitted — compiles from source on HF (10+ min / timeout).
|
research/evals/configs/lm_eval_code.yaml
CHANGED
|
@@ -8,7 +8,11 @@ claim: Better code generation
|
|
| 8 |
tasks:
|
| 9 |
- humaneval
|
| 10 |
- mbpp
|
|
|
|
|
|
|
| 11 |
|
|
|
|
|
|
|
| 12 |
num_fewshot: 0
|
| 13 |
limit: 50
|
| 14 |
seed: 42
|
|
|
|
| 8 |
tasks:
|
| 9 |
- humaneval
|
| 10 |
- mbpp
|
| 11 |
+
- hellaswag # general-capability guard (catch regression from skill tuning)
|
| 12 |
+
- piqa # general-capability guard
|
| 13 |
|
| 14 |
+
# humaneval/mbpp execute model-generated code; opt in explicitly.
|
| 15 |
+
confirm_run_unsafe_code: true
|
| 16 |
num_fewshot: 0
|
| 17 |
limit: 50
|
| 18 |
seed: 42
|
research/evals/configs/lm_eval_math.yaml
CHANGED
|
@@ -7,6 +7,8 @@ claim: Better math reasoning
|
|
| 7 |
tasks:
|
| 8 |
- gsm8k
|
| 9 |
- arc_challenge
|
|
|
|
|
|
|
| 10 |
|
| 11 |
num_fewshot: 5
|
| 12 |
limit: 100
|
|
|
|
| 7 |
tasks:
|
| 8 |
- gsm8k
|
| 9 |
- arc_challenge
|
| 10 |
+
- hellaswag # general-capability guard (catch regression from skill tuning)
|
| 11 |
+
- piqa # general-capability guard
|
| 12 |
|
| 13 |
num_fewshot: 5
|
| 14 |
limit: 100
|
research/evals/src/slm_evals/run_lm_eval.py
CHANGED
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
| 16 |
import argparse
|
| 17 |
import datetime
|
| 18 |
import json
|
|
|
|
| 19 |
import subprocess
|
| 20 |
import sys
|
| 21 |
from pathlib import Path
|
|
@@ -52,6 +53,17 @@ _METRIC_PRIORITY = (
|
|
| 52 |
"bleu,none",
|
| 53 |
)
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
def parse_args() -> argparse.Namespace:
|
| 57 |
parser = argparse.ArgumentParser(
|
|
@@ -347,6 +359,19 @@ def main() -> int:
|
|
| 347 |
|
| 348 |
_ensure_lm_eval_models_registered()
|
| 349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
seed = int(cfg.get("seed", 42))
|
| 351 |
model_args = dict(spec.model_args)
|
| 352 |
eval_device = cfg.get("device")
|
|
@@ -367,6 +392,7 @@ def main() -> int:
|
|
| 367 |
numpy_random_seed=seed,
|
| 368 |
torch_random_seed=seed,
|
| 369 |
fewshot_random_seed=seed,
|
|
|
|
| 370 |
log_samples=False,
|
| 371 |
)
|
| 372 |
|
|
|
|
| 16 |
import argparse
|
| 17 |
import datetime
|
| 18 |
import json
|
| 19 |
+
import os
|
| 20 |
import subprocess
|
| 21 |
import sys
|
| 22 |
from pathlib import Path
|
|
|
|
| 53 |
"bleu,none",
|
| 54 |
)
|
| 55 |
|
| 56 |
+
# lm-eval tasks that execute model-generated code (pass@k). lm-eval refuses to
|
| 57 |
+
# run them unless confirm_run_unsafe_code=True, and the HF `evaluate` code_eval
|
| 58 |
+
# metric additionally requires HF_ALLOW_CODE_EVAL=1.
|
| 59 |
+
_CODE_EXEC_TASK_PREFIXES = ("humaneval", "mbpp")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _requires_code_execution(tasks: list[str], override: bool | None) -> bool:
|
| 63 |
+
if override is not None:
|
| 64 |
+
return bool(override)
|
| 65 |
+
return any(str(t).lower().startswith(_CODE_EXEC_TASK_PREFIXES) for t in tasks)
|
| 66 |
+
|
| 67 |
|
| 68 |
def parse_args() -> argparse.Namespace:
|
| 69 |
parser = argparse.ArgumentParser(
|
|
|
|
| 359 |
|
| 360 |
_ensure_lm_eval_models_registered()
|
| 361 |
|
| 362 |
+
confirm_unsafe_code = _requires_code_execution(
|
| 363 |
+
cfg["tasks"], cfg.get("confirm_run_unsafe_code")
|
| 364 |
+
)
|
| 365 |
+
if confirm_unsafe_code:
|
| 366 |
+
# Required by the HF `evaluate` code_eval metric to compute pass@k.
|
| 367 |
+
os.environ.setdefault("HF_ALLOW_CODE_EVAL", "1")
|
| 368 |
+
print(
|
| 369 |
+
"Enabling code execution for tasks "
|
| 370 |
+
f"{[t for t in cfg['tasks'] if str(t).lower().startswith(_CODE_EXEC_TASK_PREFIXES)]} "
|
| 371 |
+
"(confirm_run_unsafe_code=True, HF_ALLOW_CODE_EVAL=1)",
|
| 372 |
+
file=sys.stderr,
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
seed = int(cfg.get("seed", 42))
|
| 376 |
model_args = dict(spec.model_args)
|
| 377 |
eval_device = cfg.get("device")
|
|
|
|
| 392 |
numpy_random_seed=seed,
|
| 393 |
torch_random_seed=seed,
|
| 394 |
fewshot_random_seed=seed,
|
| 395 |
+
confirm_run_unsafe_code=confirm_unsafe_code,
|
| 396 |
log_samples=False,
|
| 397 |
)
|
| 398 |
|
research/finetune.py
CHANGED
|
@@ -92,7 +92,6 @@ from datasets import load_dataset
|
|
| 92 |
from transformers import (
|
| 93 |
AutoModelForCausalLM,
|
| 94 |
AutoTokenizer,
|
| 95 |
-
DataCollatorForLanguageModeling,
|
| 96 |
Trainer,
|
| 97 |
TrainingArguments,
|
| 98 |
)
|
|
@@ -247,12 +246,36 @@ def parse_args():
|
|
| 247 |
else None,
|
| 248 |
help="Cap examples after loading (useful for Hub smoke tests)",
|
| 249 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
p.add_argument(
|
| 251 |
"--format",
|
| 252 |
type=str,
|
| 253 |
default=os.environ.get("FINETUNE_FORMAT", "chat"),
|
| 254 |
choices=["alpaca", "chat", "prompt", "text"],
|
| 255 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
p.add_argument("--mode", type=str, default="lora",
|
| 257 |
choices=["full", "lora", "qlora"])
|
| 258 |
p.add_argument(
|
|
@@ -273,6 +296,22 @@ def parse_args():
|
|
| 273 |
p.add_argument("--mask_prompt", action="store_true", default=True,
|
| 274 |
help="compute loss only on the response tokens")
|
| 275 |
p.add_argument("--no_mask_prompt", dest="mask_prompt", action="store_false")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
# lora hparams
|
| 277 |
p.add_argument("--lora_r", type=int, default=16)
|
| 278 |
p.add_argument("--lora_alpha", type=int, default=32)
|
|
@@ -398,6 +437,7 @@ def save_training_results(
|
|
| 398 |
"dataset": args.dataset,
|
| 399 |
"dataset_config": args.dataset_config,
|
| 400 |
"dataset_split": args.dataset_split,
|
|
|
|
| 401 |
"format": args.format,
|
| 402 |
"mode": args.mode,
|
| 403 |
"output_dir": out_dir,
|
|
@@ -431,23 +471,29 @@ def save_training_results(
|
|
| 431 |
return path
|
| 432 |
|
| 433 |
|
| 434 |
-
def to_prompt_response(example, fmt, tokenizer):
|
| 435 |
"""Normalize any supported format into a single training string,
|
| 436 |
-
returning (full_text, prompt_text). prompt_text is None for raw text.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
if fmt == "text":
|
| 438 |
-
return example["text"], None
|
| 439 |
|
| 440 |
if fmt == "alpaca":
|
| 441 |
-
instr = example.get("instruction", "")
|
| 442 |
-
inp = example.get("input", "") or ""
|
| 443 |
-
out = example.get("output", "")
|
| 444 |
user = instr if not inp else f"{instr}\n\n{inp}"
|
| 445 |
messages = [{"role": "user", "content": user},
|
| 446 |
{"role": "assistant", "content": out}]
|
| 447 |
|
| 448 |
elif fmt == "prompt":
|
| 449 |
-
prompt = example.get("prompt", "")
|
| 450 |
-
|
|
|
|
|
|
|
| 451 |
messages = [{"role": "user", "content": prompt},
|
| 452 |
{"role": "assistant", "content": resp}]
|
| 453 |
|
|
@@ -471,9 +517,9 @@ def to_prompt_response(example, fmt, tokenizer):
|
|
| 471 |
return full, prompt_only
|
| 472 |
|
| 473 |
|
| 474 |
-
def build_tokenize_fn(tokenizer, fmt, max_len, mask_prompt):
|
| 475 |
def fn(example):
|
| 476 |
-
full, prompt = to_prompt_response(example, fmt, tokenizer)
|
| 477 |
ids = tokenizer(full, truncation=True, max_length=max_len,
|
| 478 |
add_special_tokens=(fmt == "text"))["input_ids"]
|
| 479 |
labels = list(ids)
|
|
@@ -485,6 +531,82 @@ def build_tokenize_fn(tokenizer, fmt, max_len, mask_prompt):
|
|
| 485 |
return fn
|
| 486 |
|
| 487 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
class CausalCollator:
|
| 489 |
"""Pads input_ids with pad_token and labels with IGNORE_INDEX."""
|
| 490 |
def __init__(self, tokenizer):
|
|
@@ -795,7 +917,10 @@ def main():
|
|
| 795 |
print(f"Base model: {args.model}")
|
| 796 |
if preset_key:
|
| 797 |
print(f"Preset: {preset_key}")
|
| 798 |
-
|
|
|
|
|
|
|
|
|
|
| 799 |
print(f"Output: {args.out}")
|
| 800 |
print(f"Device: {args.device}")
|
| 801 |
|
|
@@ -813,17 +938,7 @@ def main():
|
|
| 813 |
if _training_uses_cuda(args):
|
| 814 |
print(f"GPU after model load: {_gpu_memory_summary()}")
|
| 815 |
|
| 816 |
-
ds =
|
| 817 |
-
args.dataset,
|
| 818 |
-
config=args.dataset_config,
|
| 819 |
-
split=args.dataset_split,
|
| 820 |
-
max_samples=args.dataset_max_samples,
|
| 821 |
-
)
|
| 822 |
-
ds = ds.shuffle(seed=args.seed)
|
| 823 |
-
tokenize = build_tokenize_fn(tokenizer, args.format, args.max_len,
|
| 824 |
-
args.mask_prompt)
|
| 825 |
-
ds = ds.map(tokenize, remove_columns=ds.column_names, desc="tokenizing")
|
| 826 |
-
ds = ds.filter(lambda e: len(e["input_ids"]) > 1)
|
| 827 |
|
| 828 |
if args.val_split > 0:
|
| 829 |
split = ds.train_test_split(test_size=args.val_split, seed=args.seed)
|
|
@@ -831,6 +946,16 @@ def main():
|
|
| 831 |
else:
|
| 832 |
train_ds, eval_ds = ds, None
|
| 833 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
targs = TrainingArguments(
|
| 835 |
output_dir=args.out,
|
| 836 |
num_train_epochs=args.epochs,
|
|
@@ -839,28 +964,41 @@ def main():
|
|
| 839 |
per_device_eval_batch_size=args.batch_size,
|
| 840 |
gradient_accumulation_steps=args.grad_accum,
|
| 841 |
learning_rate=lr,
|
| 842 |
-
lr_scheduler_type=
|
| 843 |
warmup_ratio=args.warmup_ratio,
|
| 844 |
-
weight_decay=
|
| 845 |
-
|
|
|
|
| 846 |
eval_strategy="steps" if eval_ds is not None else "no",
|
| 847 |
-
eval_steps=
|
| 848 |
save_strategy="steps",
|
| 849 |
-
save_steps=
|
| 850 |
-
save_total_limit=
|
|
|
|
|
|
|
|
|
|
| 851 |
bf16=bf16_ok,
|
| 852 |
fp16=(not bf16_ok and _training_uses_cuda(args)),
|
| 853 |
gradient_checkpointing=args.gradient_checkpointing,
|
| 854 |
-
|
|
|
|
| 855 |
seed=args.seed,
|
| 856 |
)
|
| 857 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
trainer = Trainer(
|
| 859 |
model=model,
|
| 860 |
args=targs,
|
| 861 |
train_dataset=train_ds,
|
| 862 |
eval_dataset=eval_ds,
|
| 863 |
data_collator=CausalCollator(tokenizer),
|
|
|
|
| 864 |
)
|
| 865 |
|
| 866 |
train_result = trainer.train(resume_from_checkpoint=args.resume)
|
|
@@ -894,7 +1032,7 @@ def main():
|
|
| 894 |
eval_metrics=eval_metrics,
|
| 895 |
)
|
| 896 |
m = json.loads(results_path.read_text())["metrics"]
|
| 897 |
-
print(
|
| 898 |
print(f"loss_score = {m['loss_score']} (lower is better)")
|
| 899 |
print(f"result_score = {m['result_score']} (0–100, higher is better)")
|
| 900 |
print(f"Saved to {results_path}")
|
|
|
|
| 92 |
from transformers import (
|
| 93 |
AutoModelForCausalLM,
|
| 94 |
AutoTokenizer,
|
|
|
|
| 95 |
Trainer,
|
| 96 |
TrainingArguments,
|
| 97 |
)
|
|
|
|
| 246 |
else None,
|
| 247 |
help="Cap examples after loading (useful for Hub smoke tests)",
|
| 248 |
)
|
| 249 |
+
p.add_argument(
|
| 250 |
+
"--mix-json",
|
| 251 |
+
type=str,
|
| 252 |
+
default=os.environ.get("FINETUNE_MIX_JSON"),
|
| 253 |
+
help=(
|
| 254 |
+
"JSON list of dataset source specs to mix/replay; overrides "
|
| 255 |
+
"--dataset/--format. Each spec: "
|
| 256 |
+
'{"dataset":..,"format":..,"columns":{..},"dataset_config":..,'
|
| 257 |
+
'"dataset_split":..,"max_samples":..,"max_len":..,"weight":..}'
|
| 258 |
+
),
|
| 259 |
+
)
|
| 260 |
p.add_argument(
|
| 261 |
"--format",
|
| 262 |
type=str,
|
| 263 |
default=os.environ.get("FINETUNE_FORMAT", "chat"),
|
| 264 |
choices=["alpaca", "chat", "prompt", "text"],
|
| 265 |
)
|
| 266 |
+
# Column-name overrides: let a dataset's own columns map onto a --format
|
| 267 |
+
# without preprocessing (e.g. MetaMathQA query/response -> prompt format,
|
| 268 |
+
# orca-math question/answer -> prompt format).
|
| 269 |
+
p.add_argument("--prompt-key", default=None,
|
| 270 |
+
help="column to use as the prompt (prompt format)")
|
| 271 |
+
p.add_argument("--response-key", default=None,
|
| 272 |
+
help="column to use as the response (prompt format)")
|
| 273 |
+
p.add_argument("--instruction-key", default=None,
|
| 274 |
+
help="column to use as instruction (alpaca format)")
|
| 275 |
+
p.add_argument("--input-key", default=None,
|
| 276 |
+
help="column to use as optional input (alpaca format)")
|
| 277 |
+
p.add_argument("--output-key", default=None,
|
| 278 |
+
help="column to use as output (alpaca format)")
|
| 279 |
p.add_argument("--mode", type=str, default="lora",
|
| 280 |
choices=["full", "lora", "qlora"])
|
| 281 |
p.add_argument(
|
|
|
|
| 296 |
p.add_argument("--mask_prompt", action="store_true", default=True,
|
| 297 |
help="compute loss only on the response tokens")
|
| 298 |
p.add_argument("--no_mask_prompt", dest="mask_prompt", action="store_false")
|
| 299 |
+
# training schedule / regularization (previously hardcoded)
|
| 300 |
+
p.add_argument("--lr_scheduler", type=str, default="cosine",
|
| 301 |
+
help="LR scheduler type: cosine, linear, constant, ...")
|
| 302 |
+
p.add_argument("--weight_decay", type=float, default=0.01)
|
| 303 |
+
p.add_argument("--max_grad_norm", type=float, default=1.0)
|
| 304 |
+
p.add_argument("--logging_steps", type=int, default=10)
|
| 305 |
+
p.add_argument("--eval_steps", type=int, default=None,
|
| 306 |
+
help="eval every N steps (default: max_steps//5, else 200)")
|
| 307 |
+
p.add_argument("--save_steps", type=int, default=500)
|
| 308 |
+
p.add_argument("--save_total_limit", type=int, default=2)
|
| 309 |
+
p.add_argument("--early_stopping_patience", type=int, default=0,
|
| 310 |
+
help=">0 enables early stopping + load_best_model_at_end on eval_loss")
|
| 311 |
+
p.add_argument("--neftune_noise_alpha", type=float, default=None,
|
| 312 |
+
help="NEFTune noise alpha (e.g. 5) — quick instruction-tuning gain")
|
| 313 |
+
p.add_argument("--report_to", type=str, default="none",
|
| 314 |
+
help="trainer reporting: none, wandb, tensorboard, ...")
|
| 315 |
# lora hparams
|
| 316 |
p.add_argument("--lora_r", type=int, default=16)
|
| 317 |
p.add_argument("--lora_alpha", type=int, default=32)
|
|
|
|
| 437 |
"dataset": args.dataset,
|
| 438 |
"dataset_config": args.dataset_config,
|
| 439 |
"dataset_split": args.dataset_split,
|
| 440 |
+
"mix": json.loads(args.mix_json) if args.mix_json else None,
|
| 441 |
"format": args.format,
|
| 442 |
"mode": args.mode,
|
| 443 |
"output_dir": out_dir,
|
|
|
|
| 471 |
return path
|
| 472 |
|
| 473 |
|
| 474 |
+
def to_prompt_response(example, fmt, tokenizer, keys=None):
|
| 475 |
"""Normalize any supported format into a single training string,
|
| 476 |
+
returning (full_text, prompt_text). prompt_text is None for raw text.
|
| 477 |
+
|
| 478 |
+
`keys` optionally remaps a dataset's column names onto the format's
|
| 479 |
+
expected fields (e.g. {"prompt": "query"} for MetaMathQA)."""
|
| 480 |
+
keys = keys or {}
|
| 481 |
if fmt == "text":
|
| 482 |
+
return example[keys.get("text", "text")], None
|
| 483 |
|
| 484 |
if fmt == "alpaca":
|
| 485 |
+
instr = example.get(keys.get("instruction", "instruction"), "")
|
| 486 |
+
inp = example.get(keys.get("input", "input"), "") or ""
|
| 487 |
+
out = example.get(keys.get("output", "output"), "")
|
| 488 |
user = instr if not inp else f"{instr}\n\n{inp}"
|
| 489 |
messages = [{"role": "user", "content": user},
|
| 490 |
{"role": "assistant", "content": out}]
|
| 491 |
|
| 492 |
elif fmt == "prompt":
|
| 493 |
+
prompt = example.get(keys.get("prompt", "prompt"), "")
|
| 494 |
+
rkey = keys.get("response")
|
| 495 |
+
resp = example.get(rkey, "") if rkey else example.get(
|
| 496 |
+
"completion", example.get("response", ""))
|
| 497 |
messages = [{"role": "user", "content": prompt},
|
| 498 |
{"role": "assistant", "content": resp}]
|
| 499 |
|
|
|
|
| 517 |
return full, prompt_only
|
| 518 |
|
| 519 |
|
| 520 |
+
def build_tokenize_fn(tokenizer, fmt, max_len, mask_prompt, keys=None):
|
| 521 |
def fn(example):
|
| 522 |
+
full, prompt = to_prompt_response(example, fmt, tokenizer, keys)
|
| 523 |
ids = tokenizer(full, truncation=True, max_length=max_len,
|
| 524 |
add_special_tokens=(fmt == "text"))["input_ids"]
|
| 525 |
labels = list(ids)
|
|
|
|
| 531 |
return fn
|
| 532 |
|
| 533 |
|
| 534 |
+
def _source_specs(args) -> list[dict]:
|
| 535 |
+
"""Return the list of dataset source specs to train on.
|
| 536 |
+
|
| 537 |
+
With --mix-json, parse the JSON list verbatim. Otherwise synthesize a
|
| 538 |
+
single source from the top-level --dataset/--format/--*-key args."""
|
| 539 |
+
if args.mix_json:
|
| 540 |
+
specs = json.loads(args.mix_json)
|
| 541 |
+
if not isinstance(specs, list) or not specs:
|
| 542 |
+
raise SystemExit("--mix-json must be a non-empty JSON list of source specs")
|
| 543 |
+
return specs
|
| 544 |
+
return [{
|
| 545 |
+
"dataset": args.dataset,
|
| 546 |
+
"format": args.format,
|
| 547 |
+
"dataset_config": args.dataset_config,
|
| 548 |
+
"dataset_split": args.dataset_split,
|
| 549 |
+
"max_samples": args.dataset_max_samples,
|
| 550 |
+
"columns": {k: v for k, v in {
|
| 551 |
+
"prompt": args.prompt_key, "response": args.response_key,
|
| 552 |
+
"instruction": args.instruction_key, "input": args.input_key,
|
| 553 |
+
"output": args.output_key,
|
| 554 |
+
}.items() if v},
|
| 555 |
+
}]
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
def _apply_weight(ds, weight):
|
| 559 |
+
"""Up-sample (weight > 1, with repeats) or sub-sample (weight < 1) a source."""
|
| 560 |
+
if not weight or weight == 1.0 or len(ds) == 0:
|
| 561 |
+
return ds
|
| 562 |
+
target = max(0, int(round(len(ds) * float(weight))))
|
| 563 |
+
if target == 0:
|
| 564 |
+
return ds.select([])
|
| 565 |
+
n = len(ds)
|
| 566 |
+
return ds.select([i % n for i in range(target)]) # repeats when target > n
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
def build_training_dataset(args, tokenizer):
|
| 570 |
+
"""Load, tokenize, weight and concatenate every source into one dataset.
|
| 571 |
+
|
| 572 |
+
Each source carries its own format / columns / split / max_len so a skill
|
| 573 |
+
dataset can be mixed with a general-data replay slice in one run."""
|
| 574 |
+
from datasets import concatenate_datasets
|
| 575 |
+
|
| 576 |
+
specs = _source_specs(args)
|
| 577 |
+
multi = len(specs) > 1
|
| 578 |
+
if multi:
|
| 579 |
+
print(f"Mixing {len(specs)} dataset source(s):")
|
| 580 |
+
|
| 581 |
+
parts = []
|
| 582 |
+
for i, spec in enumerate(specs):
|
| 583 |
+
dataset = spec.get("dataset")
|
| 584 |
+
if not dataset:
|
| 585 |
+
raise SystemExit(f"mix source #{i} is missing 'dataset'")
|
| 586 |
+
fmt = spec.get("format", args.format)
|
| 587 |
+
raw = load_raw_dataset(
|
| 588 |
+
dataset,
|
| 589 |
+
config=spec.get("dataset_config"),
|
| 590 |
+
split=spec.get("dataset_split", "train"),
|
| 591 |
+
max_samples=spec.get("max_samples"),
|
| 592 |
+
)
|
| 593 |
+
raw = raw.shuffle(seed=args.seed)
|
| 594 |
+
keys = spec.get("columns") or {}
|
| 595 |
+
max_len = spec.get("max_len", args.max_len)
|
| 596 |
+
tokenize = build_tokenize_fn(tokenizer, fmt, max_len, args.mask_prompt, keys)
|
| 597 |
+
tok = raw.map(tokenize, remove_columns=raw.column_names,
|
| 598 |
+
desc=f"tokenizing {dataset}")
|
| 599 |
+
tok = tok.filter(lambda e: len(e["input_ids"]) > 1)
|
| 600 |
+
tok = _apply_weight(tok, spec.get("weight"))
|
| 601 |
+
if multi:
|
| 602 |
+
wnote = f" (weight {spec['weight']})" if spec.get("weight") else ""
|
| 603 |
+
print(f" - {dataset} [{fmt}] -> {len(tok)} examples{wnote}")
|
| 604 |
+
parts.append(tok)
|
| 605 |
+
|
| 606 |
+
ds = parts[0] if len(parts) == 1 else concatenate_datasets(parts)
|
| 607 |
+
return ds.shuffle(seed=args.seed)
|
| 608 |
+
|
| 609 |
+
|
| 610 |
class CausalCollator:
|
| 611 |
"""Pads input_ids with pad_token and labels with IGNORE_INDEX."""
|
| 612 |
def __init__(self, tokenizer):
|
|
|
|
| 917 |
print(f"Base model: {args.model}")
|
| 918 |
if preset_key:
|
| 919 |
print(f"Preset: {preset_key}")
|
| 920 |
+
if args.mix_json:
|
| 921 |
+
print(f"Dataset mix: {len(json.loads(args.mix_json))} source(s)")
|
| 922 |
+
else:
|
| 923 |
+
print(f"Dataset: {args.dataset}")
|
| 924 |
print(f"Output: {args.out}")
|
| 925 |
print(f"Device: {args.device}")
|
| 926 |
|
|
|
|
| 938 |
if _training_uses_cuda(args):
|
| 939 |
print(f"GPU after model load: {_gpu_memory_summary()}")
|
| 940 |
|
| 941 |
+
ds = build_training_dataset(args, tokenizer)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 942 |
|
| 943 |
if args.val_split > 0:
|
| 944 |
split = ds.train_test_split(test_size=args.val_split, seed=args.seed)
|
|
|
|
| 946 |
else:
|
| 947 |
train_ds, eval_ds = ds, None
|
| 948 |
|
| 949 |
+
# Default eval cadence to the run length so short (max_steps) runs still
|
| 950 |
+
# evaluate mid-training instead of only at the end.
|
| 951 |
+
eval_steps = args.eval_steps
|
| 952 |
+
if eval_steps is None:
|
| 953 |
+
eval_steps = max(1, args.max_steps // 5) if args.max_steps > 0 else 200
|
| 954 |
+
|
| 955 |
+
use_best = args.early_stopping_patience > 0 and eval_ds is not None
|
| 956 |
+
# load_best_model_at_end needs save_steps aligned to eval_steps.
|
| 957 |
+
save_steps = eval_steps if use_best else args.save_steps
|
| 958 |
+
|
| 959 |
targs = TrainingArguments(
|
| 960 |
output_dir=args.out,
|
| 961 |
num_train_epochs=args.epochs,
|
|
|
|
| 964 |
per_device_eval_batch_size=args.batch_size,
|
| 965 |
gradient_accumulation_steps=args.grad_accum,
|
| 966 |
learning_rate=lr,
|
| 967 |
+
lr_scheduler_type=args.lr_scheduler,
|
| 968 |
warmup_ratio=args.warmup_ratio,
|
| 969 |
+
weight_decay=args.weight_decay,
|
| 970 |
+
max_grad_norm=args.max_grad_norm,
|
| 971 |
+
logging_steps=args.logging_steps,
|
| 972 |
eval_strategy="steps" if eval_ds is not None else "no",
|
| 973 |
+
eval_steps=eval_steps,
|
| 974 |
save_strategy="steps",
|
| 975 |
+
save_steps=save_steps,
|
| 976 |
+
save_total_limit=args.save_total_limit,
|
| 977 |
+
load_best_model_at_end=use_best,
|
| 978 |
+
metric_for_best_model="eval_loss" if use_best else None,
|
| 979 |
+
greater_is_better=False if use_best else None,
|
| 980 |
bf16=bf16_ok,
|
| 981 |
fp16=(not bf16_ok and _training_uses_cuda(args)),
|
| 982 |
gradient_checkpointing=args.gradient_checkpointing,
|
| 983 |
+
neftune_noise_alpha=args.neftune_noise_alpha,
|
| 984 |
+
report_to=args.report_to,
|
| 985 |
seed=args.seed,
|
| 986 |
)
|
| 987 |
|
| 988 |
+
callbacks = []
|
| 989 |
+
if use_best:
|
| 990 |
+
from transformers import EarlyStoppingCallback
|
| 991 |
+
callbacks.append(
|
| 992 |
+
EarlyStoppingCallback(early_stopping_patience=args.early_stopping_patience)
|
| 993 |
+
)
|
| 994 |
+
|
| 995 |
trainer = Trainer(
|
| 996 |
model=model,
|
| 997 |
args=targs,
|
| 998 |
train_dataset=train_ds,
|
| 999 |
eval_dataset=eval_ds,
|
| 1000 |
data_collator=CausalCollator(tokenizer),
|
| 1001 |
+
callbacks=callbacks,
|
| 1002 |
)
|
| 1003 |
|
| 1004 |
train_result = trainer.train(resume_from_checkpoint=args.resume)
|
|
|
|
| 1032 |
eval_metrics=eval_metrics,
|
| 1033 |
)
|
| 1034 |
m = json.loads(results_path.read_text())["metrics"]
|
| 1035 |
+
print("\n--- scores ---")
|
| 1036 |
print(f"loss_score = {m['loss_score']} (lower is better)")
|
| 1037 |
print(f"result_score = {m['result_score']} (0–100, higher is better)")
|
| 1038 |
print(f"Saved to {results_path}")
|
research/modal/_common.py
CHANGED
|
@@ -75,7 +75,12 @@ image = (
|
|
| 75 |
],
|
| 76 |
)
|
| 77 |
.run_commands(
|
| 78 |
-
"cd /repo && uv sync --frozen --group finetune --group lm-eval --no-dev"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
)
|
| 80 |
)
|
| 81 |
|
|
@@ -114,6 +119,35 @@ def apply_defaults(job: dict[str, Any], defaults: dict[str, Any]) -> dict[str, A
|
|
| 114 |
return {**defaults, **job}
|
| 115 |
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
def build_finetune_cmd(job: dict[str, Any], out_dir: str) -> list[str]:
|
| 118 |
cmd = [
|
| 119 |
"uv",
|
|
@@ -124,23 +158,43 @@ def build_finetune_cmd(job: dict[str, Any], out_dir: str) -> list[str]:
|
|
| 124 |
job.get("preset", "minicpm5-1b"),
|
| 125 |
"--mode",
|
| 126 |
job.get("mode", "lora"),
|
| 127 |
-
"--dataset",
|
| 128 |
-
job["dataset"],
|
| 129 |
-
"--format",
|
| 130 |
-
job["format"],
|
| 131 |
"--out",
|
| 132 |
out_dir,
|
| 133 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
if job.get("max_steps") is not None:
|
| 135 |
cmd.extend(["--max_steps", str(int(job["max_steps"]))])
|
| 136 |
if job.get("epochs") is not None:
|
| 137 |
cmd.extend(["--epochs", str(job["epochs"])])
|
| 138 |
-
if job.get("
|
| 139 |
-
cmd.
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
return cmd
|
| 145 |
|
| 146 |
|
|
@@ -313,21 +367,28 @@ def evaluate_gate(
|
|
| 313 |
|
| 314 |
def pull_artifacts(job_name: str, exp_name: str, dest: str = "models/finetuned") -> None:
|
| 315 |
"""Download an adapter and its lm-eval results from the `slm-finetune` Volume (run locally)."""
|
|
|
|
| 316 |
import subprocess
|
| 317 |
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
)
|
| 331 |
|
| 332 |
|
| 333 |
def check_gate_files(
|
|
|
|
| 75 |
],
|
| 76 |
)
|
| 77 |
.run_commands(
|
| 78 |
+
"cd /repo && uv sync --frozen --group finetune --group lm-eval --no-dev",
|
| 79 |
+
# lm-eval's ifeval task (instructions profile) needs these, declared via
|
| 80 |
+
# the lm-eval[ifeval] extra but not activated into the project venv by the
|
| 81 |
+
# frozen group sync. Install the lock-pinned versions into /repo/.venv so
|
| 82 |
+
# `uv run slm-lm-eval` can import them.
|
| 83 |
+
"cd /repo && uv pip install langdetect==1.0.9 immutabledict==4.3.1",
|
| 84 |
)
|
| 85 |
)
|
| 86 |
|
|
|
|
| 119 |
return {**defaults, **job}
|
| 120 |
|
| 121 |
|
| 122 |
+
# Scalar hyperparameters an experiments.yaml job (or its nested `args:` block)
|
| 123 |
+
# may set; each maps 1:1 onto a research/finetune.py flag so any run is tunable
|
| 124 |
+
# from config without code changes.
|
| 125 |
+
_FINETUNE_FLAGS: dict[str, str] = {
|
| 126 |
+
"model": "--model",
|
| 127 |
+
"lr": "--lr",
|
| 128 |
+
"batch_size": "--batch_size",
|
| 129 |
+
"grad_accum": "--grad_accum",
|
| 130 |
+
"max_len": "--max_len",
|
| 131 |
+
"warmup_ratio": "--warmup_ratio",
|
| 132 |
+
"weight_decay": "--weight_decay",
|
| 133 |
+
"max_grad_norm": "--max_grad_norm",
|
| 134 |
+
"lr_scheduler": "--lr_scheduler",
|
| 135 |
+
"logging_steps": "--logging_steps",
|
| 136 |
+
"eval_steps": "--eval_steps",
|
| 137 |
+
"save_steps": "--save_steps",
|
| 138 |
+
"save_total_limit": "--save_total_limit",
|
| 139 |
+
"early_stopping_patience": "--early_stopping_patience",
|
| 140 |
+
"neftune_noise_alpha": "--neftune_noise_alpha",
|
| 141 |
+
"report_to": "--report_to",
|
| 142 |
+
"seed": "--seed",
|
| 143 |
+
"lora_r": "--lora_r",
|
| 144 |
+
"lora_alpha": "--lora_alpha",
|
| 145 |
+
"lora_dropout": "--lora_dropout",
|
| 146 |
+
"lora_targets": "--lora_targets",
|
| 147 |
+
"val_split": "--val_split",
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
def build_finetune_cmd(job: dict[str, Any], out_dir: str) -> list[str]:
|
| 152 |
cmd = [
|
| 153 |
"uv",
|
|
|
|
| 158 |
job.get("preset", "minicpm5-1b"),
|
| 159 |
"--mode",
|
| 160 |
job.get("mode", "lora"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
"--out",
|
| 162 |
out_dir,
|
| 163 |
]
|
| 164 |
+
# Dataset: a `mix:` list (skill data + general replay) takes precedence over
|
| 165 |
+
# a single --dataset/--format source.
|
| 166 |
+
if job.get("mix"):
|
| 167 |
+
cmd.extend(["--mix-json", json.dumps(job["mix"])])
|
| 168 |
+
else:
|
| 169 |
+
cmd.extend(["--dataset", job["dataset"], "--format", job["format"]])
|
| 170 |
+
if job.get("dataset_config"):
|
| 171 |
+
cmd.extend(["--dataset-config", job["dataset_config"]])
|
| 172 |
+
if job.get("dataset_split"):
|
| 173 |
+
cmd.extend(["--dataset-split", str(job["dataset_split"])])
|
| 174 |
+
if job.get("max_samples") is not None:
|
| 175 |
+
cmd.extend(["--dataset-max-samples", str(int(job["max_samples"]))])
|
| 176 |
+
# Optional column remap so a dataset's own columns fit the --format
|
| 177 |
+
# (e.g. MetaMathQA query/response -> prompt format).
|
| 178 |
+
for field, col in (job.get("columns") or {}).items():
|
| 179 |
+
cmd.extend([f"--{field}-key", str(col)])
|
| 180 |
+
|
| 181 |
if job.get("max_steps") is not None:
|
| 182 |
cmd.extend(["--max_steps", str(int(job["max_steps"]))])
|
| 183 |
if job.get("epochs") is not None:
|
| 184 |
cmd.extend(["--epochs", str(job["epochs"])])
|
| 185 |
+
if job.get("mask_prompt") is False:
|
| 186 |
+
cmd.append("--no_mask_prompt")
|
| 187 |
+
|
| 188 |
+
# Scalar hyperparameters: top-level keys plus an optional nested `args:` block.
|
| 189 |
+
overrides = {k: job[k] for k in _FINETUNE_FLAGS if k in job}
|
| 190 |
+
overrides.update(job.get("args") or {})
|
| 191 |
+
for key, value in overrides.items():
|
| 192 |
+
flag = _FINETUNE_FLAGS.get(key, f"--{key}")
|
| 193 |
+
if isinstance(value, bool):
|
| 194 |
+
if value:
|
| 195 |
+
cmd.append(flag)
|
| 196 |
+
else:
|
| 197 |
+
cmd.extend([flag, str(value)])
|
| 198 |
return cmd
|
| 199 |
|
| 200 |
|
|
|
|
| 367 |
|
| 368 |
def pull_artifacts(job_name: str, exp_name: str, dest: str = "models/finetuned") -> None:
|
| 369 |
"""Download an adapter and its lm-eval results from the `slm-finetune` Volume (run locally)."""
|
| 370 |
+
import shutil
|
| 371 |
import subprocess
|
| 372 |
|
| 373 |
+
def _get(remote: str, parent: str) -> None:
|
| 374 |
+
# For a folder REMOTE_PATH, `modal volume get` expects the *parent*
|
| 375 |
+
# directory as the destination and recreates the folder inside it.
|
| 376 |
+
# Passing the full target path (parent/<name>) raises
|
| 377 |
+
# "[Errno 21] Is a directory". Clear the target first for a clean pull.
|
| 378 |
+
name = remote.rsplit("/", 1)[-1]
|
| 379 |
+
shutil.rmtree(Path(parent) / name, ignore_errors=True)
|
| 380 |
+
Path(parent).mkdir(parents=True, exist_ok=True)
|
| 381 |
+
subprocess.run(
|
| 382 |
+
["modal", "volume", "get", "slm-finetune", remote, f"{parent}/", "--force"],
|
| 383 |
+
check=False,
|
| 384 |
+
)
|
| 385 |
|
| 386 |
+
print(f"--- pulling {job_name} -> {dest}/{job_name} ---")
|
| 387 |
+
_get(job_name, dest)
|
| 388 |
+
|
| 389 |
+
exp_dir = f"results/lm_eval/{exp_name}"
|
| 390 |
+
print(f"--- pulling {exp_dir} ---")
|
| 391 |
+
_get(exp_dir, "results/lm_eval")
|
| 392 |
|
| 393 |
|
| 394 |
def check_gate_files(
|
research/modal/experiments.yaml
CHANGED
|
@@ -62,14 +62,32 @@ finetune:
|
|
| 62 |
hub_repo: MSGEncrypted/minicpm5-1b-science-lora
|
| 63 |
private: false
|
| 64 |
|
| 65 |
-
# --- math:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
- name: math-lora
|
| 67 |
category: math
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
eval_profile: math
|
| 74 |
goals:
|
| 75 |
task: gsm8k
|
|
@@ -78,6 +96,10 @@ finetune:
|
|
| 78 |
guard_tasks:
|
| 79 |
- task: arc_challenge
|
| 80 |
max_regress: 0.03
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
publish:
|
| 82 |
hub_repo: MSGEncrypted/minicpm5-1b-math-lora
|
| 83 |
private: false
|
|
@@ -95,6 +117,11 @@ finetune:
|
|
| 95 |
task: mbpp
|
| 96 |
min_score: 0.05
|
| 97 |
min_improve: 0.01
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
publish:
|
| 99 |
hub_repo: MSGEncrypted/minicpm5-1b-coding-lora
|
| 100 |
private: false
|
|
|
|
| 62 |
hub_repo: MSGEncrypted/minicpm5-1b-science-lora
|
| 63 |
private: false
|
| 64 |
|
| 65 |
+
# --- math: GSM8K/MATH natural-language CoT augmentation (MetaMathQA) ---
|
| 66 |
+
# MetaMathQA's NL chain-of-thought matches GSM8K's 5-shot format far better
|
| 67 |
+
# than MathInstruct's program-of-thought answers (which regressed gsm8k).
|
| 68 |
+
# The `mix:` adds a general-data replay slice (alpaca) so skill tuning does
|
| 69 |
+
# not regress the arc/hellaswag/piqa guards. `args:` flows any
|
| 70 |
+
# research/finetune.py hyperparameter straight through.
|
| 71 |
- name: math-lora
|
| 72 |
category: math
|
| 73 |
+
max_steps: 150
|
| 74 |
+
mix:
|
| 75 |
+
- dataset: meta-math/MetaMathQA
|
| 76 |
+
format: prompt
|
| 77 |
+
columns:
|
| 78 |
+
prompt: query
|
| 79 |
+
response: response
|
| 80 |
+
dataset_split: "train[:3000]"
|
| 81 |
+
max_samples: 3000
|
| 82 |
+
- dataset: tatsu-lab/alpaca # general replay: protect guard tasks
|
| 83 |
+
format: alpaca
|
| 84 |
+
dataset_split: "train[:600]"
|
| 85 |
+
max_samples: 600
|
| 86 |
+
args:
|
| 87 |
+
lora_r: 32
|
| 88 |
+
lora_alpha: 64
|
| 89 |
+
neftune_noise_alpha: 5
|
| 90 |
+
description: GSM8K/MATH NL-CoT (MetaMathQA) + alpaca replay, r=32 + NEFTune
|
| 91 |
eval_profile: math
|
| 92 |
goals:
|
| 93 |
task: gsm8k
|
|
|
|
| 96 |
guard_tasks:
|
| 97 |
- task: arc_challenge
|
| 98 |
max_regress: 0.03
|
| 99 |
+
- task: hellaswag
|
| 100 |
+
max_regress: 0.03
|
| 101 |
+
- task: piqa
|
| 102 |
+
max_regress: 0.03
|
| 103 |
publish:
|
| 104 |
hub_repo: MSGEncrypted/minicpm5-1b-math-lora
|
| 105 |
private: false
|
|
|
|
| 117 |
task: mbpp
|
| 118 |
min_score: 0.05
|
| 119 |
min_improve: 0.01
|
| 120 |
+
guard_tasks:
|
| 121 |
+
- task: hellaswag
|
| 122 |
+
max_regress: 0.03
|
| 123 |
+
- task: piqa
|
| 124 |
+
max_regress: 0.03
|
| 125 |
publish:
|
| 126 |
hub_repo: MSGEncrypted/minicpm5-1b-coding-lora
|
| 127 |
private: false
|
research/modal/server_app.py
CHANGED
|
@@ -85,6 +85,7 @@ app = modal.App(APP_NAME, image=image)
|
|
| 85 |
timeout=DEFAULT_WORKER_TIMEOUT,
|
| 86 |
scaledown_window=DEFAULT_SCALEDOWN_WINDOW,
|
| 87 |
min_containers=1,
|
|
|
|
| 88 |
)
|
| 89 |
class GpuWorker:
|
| 90 |
"""Single warm GPU container for sequential finetune / lm-eval / shell commands."""
|
|
@@ -165,6 +166,10 @@ class GpuWorker:
|
|
| 165 |
compare_to: str | None = None,
|
| 166 |
) -> dict[str, Any]:
|
| 167 |
"""Run slm-lm-eval on base model or finetuned checkpoint."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
if adapter_path:
|
| 169 |
adapter_dir = Path(adapter_path)
|
| 170 |
adapter_cfg = adapter_dir / "adapter_config.json"
|
|
|
|
| 85 |
timeout=DEFAULT_WORKER_TIMEOUT,
|
| 86 |
scaledown_window=DEFAULT_SCALEDOWN_WINDOW,
|
| 87 |
min_containers=1,
|
| 88 |
+
max_containers=1, # single warm container; serialize work, never sprawl
|
| 89 |
)
|
| 90 |
class GpuWorker:
|
| 91 |
"""Single warm GPU container for sequential finetune / lm-eval / shell commands."""
|
|
|
|
| 166 |
compare_to: str | None = None,
|
| 167 |
) -> dict[str, Any]:
|
| 168 |
"""Run slm-lm-eval on base model or finetuned checkpoint."""
|
| 169 |
+
# Pick up adapters committed by another container (e.g. a separate
|
| 170 |
+
# eval-only invocation) — the warm container's mount may predate them.
|
| 171 |
+
reload_volumes()
|
| 172 |
+
|
| 173 |
if adapter_path:
|
| 174 |
adapter_dir = Path(adapter_path)
|
| 175 |
adapter_cfg = adapter_dir / "adapter_config.json"
|