workbench / HF_PRD_v1.md
GitHub Actions
Initial ZeroGPU deployment with spaces shim
7f9dfed
|
Raw
History Blame Contribute Delete
48.5 kB
# OpenBMB Local AI Workbench β€” Technical PRD v2.0
> Revised: 2026-06-05 | Corrections from online research embedded inline
---
## Table of Contents
1. [Purpose & Design Philosophy](#1-purpose--design-philosophy)
2. [Design Principles (Template Architecture)](#2-design-principles-template-architecture)
3. [System Architecture](#3-system-architecture)
4. [Model Registry](#4-model-registry)
5. [Inference Stack β€” Five Modes](#5-inference-stack--five-modes)
6. [Tracking: Trackio (Corrected API)](#6-tracking-trackio-corrected-api)
7. [MCP Layer (Three Integration Paths)](#7-mcp-layer-three-integration-paths)
8. [Training Pipeline](#8-training-pipeline)
9. [Export & Quantization](#9-export--quantization)
10. [Agent Mode (ml-intern Inspired)](#10-agent-mode-ml-intern-inspired)
11. [UI β€” Tab Specification](#11-ui--tab-specification)
12. [Field Notes & Correction Loop](#12-field-notes--correction-loop)
13. [Directory Structure](#13-directory-structure)
14. [Configuration Schema](#14-configuration-schema)
15. [Dependencies](#15-dependencies)
16. [Hackathon Demo Flow](#16-hackathon-demo-flow)
17. [Corrections from PRD v1](#17-corrections-from-prd-v1)
18. [Roadmap & Extension Points](#18-roadmap--extension-points)
---
## 1. Purpose & Design Philosophy
**One-line:** A modular AI experimentation platform for the OpenBMB model family that covers
dataset ingestion β†’ LoRA training β†’ evaluation β†’ GGUF export β†’ llama.cpp/SGLang deployment β†’
multimodal inference β†’ MCP tool exposure β†’ trace sharing β€” in a single Gradio app.
**Broader framing:** The platform is deliberately designed as a *template*. The `config/models.yaml`
and `config/training.yaml` files drive almost all model-specific behaviour. Replacing the OpenBMB
section with any other model family (Qwen, Phi, Gemma …) should require only config changes and
a matching service class, never core rewrites.
**Why OpenBMB?**
- MiniCPM5-1B (2026-05-19): SOTA 1B on-device, 128K context, LlamaForCausalLM β€” no custom kernels
- MiniCPM4.1-8B (2025-09): sparse InfLLM-v2 attention, ~7Γ— long-context speedup vs Qwen3-8B
- MiniCPM-V-4.6 (2026-05-11): 1.3B VLM, SigLIP2-400M + Qwen3.5-0.8B, 262K ctx, mixed 4Γ—/16Γ— token compression
- MiniCPM-V-4.6-Thinking: same architecture + long-CoT for multimodal reasoning
- MiniCPM-o 4.5 (2026-05-17): omnimodal (vision + audio), real-time conversation
- Apache 2.0 across the board; vLLM / SGLang / llama.cpp / Ollama native support
---
## 2. Design Principles (Template Architecture)
| Principle | Implementation |
|-----------|----------------|
| Config-driven | `models.yaml` + `training.yaml` define all models; no model names hardcoded in logic |
| Event-driven | `core/events.py` β€” typed event bus; all cross-module comms via events |
| Registry pattern | `core/registry.py` β€” all services register by name; swap without restart |
| Separation of concerns | `models/` = loading; `training/` = train/eval; `ui/` = Gradio; `mcp/` = protocol |
| Plugin tools | Any Python function decorated `@mcp.tool()` is auto-exposed; no wiring needed |
| Async-first | FastAPI backend, Gradio queuing, non-blocking inference |
| Observability | Every significant action fires a Trackio log + event |
| Template portability | Replace `config/models.yaml` β†’ different model family; keep all else |
---
## 3. System Architecture
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ OpenBMB Local AI Workbench β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Gradio UI Layer β”‚ Backend Service Layer β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ gr.Blocks / Tabs β”‚ β”‚ β”‚ gradio.Server (FastAPI-based) β”‚ β”‚
β”‚ β”‚ chat_tab.py β”‚ β”‚ β”‚ @app.api() β€” Gradio-queued endpoints β”‚ β”‚
β”‚ β”‚ vision_tab.py β”‚ β”‚ β”‚ @app.mcp.tool() β€” MCP registration β”‚ β”‚
β”‚ β”‚ train_tab.py β”‚ β”‚ β”‚ @app.get("/") β€” static/custom frontend β”‚ β”‚
β”‚ β”‚ dataset_tab.py β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ export_tab.py β”‚ β”‚ β”‚
β”‚ β”‚ traces_tab.py β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ agent_tab.py β”‚ β”‚ β”‚ FastMCP server (standalone / bridged) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ mcp = FastMCP("OpenBMBWorkbench") β”‚ β”‚
β”‚ β”‚ β”‚ Registered tools (see Β§7) β”‚ β”‚
β”‚ SSE MCP endpoint: β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ /gradio_api/mcp/sse β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Model Services β”‚ β”‚ Tracking & Storage β”‚
β”‚ minicpm_text.py β”‚ β”‚ trackio (local/Space) β”‚
β”‚ minicpm_vision.py β”‚ β”‚ field_notes/ (SQLite) β”‚
β”‚ llama_cpp_runner.pyβ”‚ β”‚ exports/ (GGUF files) β”‚
β”‚ sglang_runner.py β”‚ β”‚ data/ (HF datasets) β”‚
β”‚ ollama_runner.py β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Inference Backends β”‚
β”‚ Transformers β”‚ llama.cpp β”‚ SGLang β”‚ vLLM β”‚ Ollamaβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
### 3.1 Event Bus (core/events.py)
```python
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable
import asyncio
class EventType(str, Enum):
DATASET_LOADED = "dataset_loaded"
TRAINING_STARTED = "training_started"
TRAINING_STEP = "training_step"
TRAINING_FINISHED = "training_finished"
EVAL_FINISHED = "eval_finished"
INFERENCE_REQUEST = "inference_request"
INFERENCE_RESPONSE = "inference_response"
TOOL_CALL = "tool_call"
MODEL_LOADED = "model_loaded"
MODEL_SWITCHED = "model_switched"
EXPORT_STARTED = "export_started"
EXPORT_FINISHED = "export_finished"
FIELD_NOTE_SAVED = "field_note_saved"
AGENT_STEP = "agent_step"
@dataclass
class Event:
type: EventType
payload: dict[str, Any] = field(default_factory=dict)
run_id: str = ""
class EventBus:
_handlers: dict[EventType, list[Callable]] = {}
def on(self, event_type: EventType):
"""Decorator to register an event handler."""
def decorator(fn: Callable):
self._handlers.setdefault(event_type, []).append(fn)
return fn
return decorator
async def emit(self, event: Event):
for handler in self._handlers.get(event.type, []):
await handler(event)
bus = EventBus()
```
### 3.2 Service Registry (core/registry.py)
```python
from typing import TypeVar, Generic, Type
T = TypeVar("T")
class Registry(Generic[T]):
def __init__(self):
self._services: dict[str, T] = {}
def register(self, name: str, service: T):
self._services[name] = service
def get(self, name: str) -> T:
if name not in self._services:
raise KeyError(f"Service '{name}' not registered. Available: {list(self._services)}")
return self._services[name]
def list(self) -> list[str]:
return list(self._services.keys())
# Singleton registries
model_registry = Registry() # model services
dataset_registry = Registry() # dataset loaders
tool_registry = Registry() # MCP tools
```
---
## 4. Model Registry
### 4.1 Text Models
| Config ID | HF ID | Architecture | Context | Notes |
|-----------|-------|-------------|---------|-------|
| `minicpm5_1b` | `openbmb/MiniCPM5-1B` | LlamaForCausalLM | 128K | Standard; no custom kernels; XML tool calls; SGLang `minicpm5` parser |
| `minicpm5_1b_thinking` | `openbmb/MiniCPM5-1B-Thinking` | LlamaForCausalLM | 128K | CoT mode; `chat_template_kwargs={"enable_thinking": True}` |
| `minicpm41_8b` | `openbmb/MiniCPM4.1-8B` | Sparse InfLLM-v2 | 128K (YaRN) | `--trust-remote-code` required; 7Γ— faster long-ctx vs Qwen3-8B |
### 4.2 Vision Models
| Config ID | HF ID | Backbone | Context | Notes |
|-----------|-------|---------|---------|-------|
| `minicpm_v46` | `openbmb/MiniCPM-V-4.6` | SigLIP2-400M + Qwen3.5-0.8B | 262K | `transformers>=5.7.0` + `torchcodec`; mixed 4Γ—/16Γ— compression |
| `minicpm_v46_thinking` | `openbmb/MiniCPM-V-4.6-Thinking` | same + CoT head | 262K | `enable_thinking=True` in chat_template_kwargs |
| `minicpm_o45` | `openbmb/MiniCPM-o-4.5` | Omnimodal | - | Vision + Audio; real-time conversation; see Β§4.4 |
### 4.3 Model Architecture Notes (critical for correct code)
**MiniCPM5-1B** β€” standard Llama, loads with:
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("openbmb/MiniCPM5-1B", trust_remote_code=True)
```
**MiniCPM-V-4.6** β€” vision-language, loads with:
```python
# ⚠️ CORRECTION from v1: NOT AutoModelForCausalLM β€” use AutoModelForImageTextToText
from transformers import AutoProcessor, AutoModelForImageTextToText
processor = AutoProcessor.from_pretrained("openbmb/MiniCPM-V-4.6", trust_remote_code=True)
model = AutoModelForImageTextToText.from_pretrained(
"openbmb/MiniCPM-V-4.6",
trust_remote_code=True,
torch_dtype=torch.bfloat16,
device_map="auto"
)
```
Chat format (vision):
```python
messages = [
{
"role": "user",
"content": [
{"type": "image", "image": pil_image}, # PIL Image
{"type": "text", "text": prompt}
]
}
]
inputs = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_tensors="pt",
return_dict=True,
chat_template_kwargs={"enable_thinking": False} # True for Thinking variant
).to(model.device)
```
**MiniCPM4.1-8B** β€” requires `trust_remote_code=True` for sparse attention kernel.
### 4.4 models.yaml Schema
```yaml
# config/models.yaml
# ─── Template: add any HF model here ──────────────────────────────────────
models:
minicpm5_1b:
hf_id: openbmb/MiniCPM5-1B
type: text # text | vision | omnimodal
architecture: llama # llama | minicpm41 | minicpm_v
context_length: 131072
dtype: bfloat16
trust_remote_code: false # LlamaForCausalLM β€” no fork
thinking_mode: false # set true for Thinking checkpoints
tool_call_parser: minicpm5 # for SGLang serving
gguf:
available: true
repo: openbmb/MiniCPM5-1B-GGUF
default_quant: Q4_K_M
minicpm41_8b:
hf_id: openbmb/MiniCPM4.1-8B
type: text
architecture: minicpm41 # sparse InfLLM-v2
context_length: 131072
dtype: bfloat16
trust_remote_code: true
thinking_mode: false
gguf:
available: true
repo: openbmb/MiniCPM4.1-8B-GGUF
default_quant: Q4_K_M
minicpm_v46:
hf_id: openbmb/MiniCPM-V-4.6
type: vision
architecture: minicpm_v
context_length: 262144
dtype: bfloat16
trust_remote_code: true
thinking_mode: false
vision_encoder: siglip2_400m
token_compression: "4x_16x" # mixed compression
gguf:
available: true
repo: openbmb/MiniCPM-V-4.6-gguf # official repo β€” no surgery script needed
main_file: MiniCPM-V-4.6-Q4_K_M.gguf
mmproj: mmproj-MiniCPM-V-4.6-F16.gguf
default_quant: Q4_K_M
minicpm_v46_thinking:
hf_id: openbmb/MiniCPM-V-4.6-Thinking
type: vision
architecture: minicpm_v
context_length: 262144
dtype: bfloat16
trust_remote_code: true
thinking_mode: true
gguf:
available: true
repo: openbmb/MiniCPM-V-4.6-Thinking-gguf
default_quant: Q4_K_M
minicpm_o45:
hf_id: openbmb/MiniCPM-o-4.5
type: omnimodal
architecture: minicpm_o
context_length: 131072
dtype: bfloat16
trust_remote_code: true
thinking_mode: false
note: "Vision + Audio; real-time conversation mode"
```
---
## 5. Inference Stack β€” Five Modes
### Mode A: Transformers (train + eval + fine-tuning)
Used for: LoRA training, evaluation, PEFT
Load text: `AutoModelForCausalLM`
Load vision: `AutoModelForImageTextToText` ← corrected from v1
```python
# models/minicpm_text.py
class MiniCPMTextService:
def __init__(self, model_id: str, cfg: dict):
self.model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
device_map="auto",
trust_remote_code=cfg.get("trust_remote_code", False)
)
self.tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
```
```python
# models/minicpm_vision.py ← new dedicated service
class MiniCPMVisionService:
def __init__(self, model_id: str, cfg: dict):
self.processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
self.model = AutoModelForImageTextToText.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
device_map="auto",
trust_remote_code=cfg.get("trust_remote_code", True)
)
self.thinking = cfg.get("thinking_mode", False)
def chat(self, messages: list[dict], max_new_tokens: int = 512) -> str:
inputs = self.processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_tensors="pt",
return_dict=True,
chat_template_kwargs={"enable_thinking": self.thinking}
).to(self.model.device)
with torch.no_grad():
outputs = self.model.generate(**inputs, max_new_tokens=max_new_tokens)
response = self.processor.decode(
outputs[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True
)
return response
```
### Mode B: llama.cpp (local deployment, CPU/GPU offload)
> ⚠️ CORRECTION from v1: For MiniCPM-V 4.6, official GGUFs are already published.
> Download from `openbmb/MiniCPM-V-4.6-gguf` β€” no need to run `convert_hf_to_gguf.py` yourself.
> The `minicpmv-surgery.py` script was for the older MiniCPM-o-2.6; **do not use it for V4.6**.
**Text models** β†’ `llama-server` (standard OpenAI-compatible endpoint)
**Vision models** β†’ `llama-server --mmproj <mmproj.gguf>` (multimodal server)
```python
# models/llama_cpp_runner.py
import subprocess, requests, time
class LlamaCppDeployment:
def __init__(self, cfg: dict):
self.model_path = cfg["model_path"]
self.mmproj_path = cfg.get("mmproj_path") # None for text models
self.port = cfg.get("port", 8080)
self._proc = None
def start(self):
cmd = [
"llama-server",
"--model", self.model_path,
"--port", str(self.port),
"--ctx-size", "8192",
"--n-gpu-layers", "-1", # full GPU offload if available
]
if self.mmproj_path:
cmd += ["--mmproj", self.mmproj_path] # enables vision
self._proc = subprocess.Popen(cmd)
self._wait_ready()
def _wait_ready(self, timeout: int = 30):
for _ in range(timeout):
try:
r = requests.get(f"http://127.0.0.1:{self.port}/health")
if r.status_code == 200:
return
except Exception:
pass
time.sleep(1)
raise RuntimeError("llama-server did not start in time")
def chat(self, messages: list[dict]) -> str:
r = requests.post(
f"http://127.0.0.1:{self.port}/v1/chat/completions",
json={"messages": messages, "max_tokens": 512}
)
return r.json()["choices"][0]["message"]["content"]
def stop(self):
if self._proc:
self._proc.terminate()
```
**GGUF Export** (only needed if building custom quants):
```bash
# For MiniCPM-V 4.6 β€” convert_hf_to_gguf.py (NOT legacy surgery scripts)
python convert_hf_to_gguf.py openbmb/MiniCPM-V-4.6 --outtype f16
# Output: MiniCPM-V-4.6-F16.gguf + mmproj-MiniCPM-V-4.6-F16.gguf
# Quantize
./llama-quantize MiniCPM-V-4.6-F16.gguf MiniCPM-V-4.6-Q4_K_M.gguf Q4_K_M
```
Supported quants: `Q4_K_M`, `Q5_K_M`, `Q8_0`, `Q2_K`, `F16`
### Mode C: SGLang (tool-use, function calling, recommended for MiniCPM5)
MiniCPM5-1B emits XML-style tool calls. SGLang has a native `minicpm5` parser:
```bash
python -m sglang.launch_server \
--model-path openbmb/MiniCPM5-1B \
--port 30000 \
--tool-call-parser minicpm5 # or: --tool-call-parser auto
```
For MiniCPM-V-4.6 vision:
```bash
python -m sglang.launch_server \
--model-path openbmb/MiniCPM-V-4.6-Thinking \
--port 30000 \
--enable-auto-tool-choice \
--tool-call-parser qwen3_coder \
--default-chat-template-kwargs '{"enable_thinking": true}'
```
### Mode D: vLLM (batch serving, production)
```bash
vllm serve openbmb/MiniCPM4.1-8B --trust-remote-code
vllm serve openbmb/MiniCPM-V-4.6 --trust-remote-code
```
### Mode E: Ollama (zero-config, consumer hardware)
```bash
ollama run openbmb/minicpm-v4.6 # vision + text
ollama run openbmb/minicpm5-1b # text only
```
### Mode Selection Matrix
| Use case | Recommended mode |
|----------|-----------------|
| LoRA training | A (Transformers) |
| Evaluation | A (Transformers) |
| Local chat (fast) | B (llama.cpp) |
| Tool/function calling | C (SGLang) |
| Production batch | D (vLLM) |
| Consumer demo | E (Ollama) |
| Vision inference local | B (llama-server --mmproj) |
| Vision fine-tuning | A (SWIFT / LLaMA-Factory) |
---
## 6. Tracking: Trackio (Corrected API)
> ⚠️ CRITICAL CORRECTION from v1: `trackio.trace()` **does not exist**.
> There is no context-manager trace API in Trackio. The v1 PRD example was wrong.
> Correct API: `init()` / `log()` / `finish()` / `alert()` / `Table()` / `Markdown()`
Trackio is a wandb drop-in replacement, local-first, free, Gradio dashboard, HF Spaces sync.
```python
# tracking/trackio_service.py
import trackio
import time
from core.events import bus, EventType, Event
class TrackioService:
def __init__(self, project: str, space_id: str | None = None):
self.project = project
self.space_id = space_id
self._active = False
self._register_handlers()
def _register_handlers(self):
@bus.on(EventType.TRAINING_STARTED)
async def on_training_started(event: Event):
trackio.init(
project=self.project,
space_id=self.space_id,
run_name=event.payload.get("run_name", "training"),
)
self._active = True
@bus.on(EventType.TRAINING_STEP)
async def on_step(event: Event):
if self._active:
trackio.log(event.payload)
@bus.on(EventType.TRAINING_FINISHED)
async def on_done(event: Event):
if self._active:
trackio.log({"status": "finished", **event.payload})
trackio.finish()
self._active = False
@bus.on(EventType.EVAL_FINISHED)
async def on_eval(event: Event):
trackio.init(
project=self.project,
space_id=self.space_id,
run_name=event.payload.get("eval_run_name", "eval"),
)
trackio.log(event.payload)
trackio.finish()
@bus.on(EventType.INFERENCE_RESPONSE)
async def on_inference(event: Event):
# Log inference latency and token count
trackio.init(
project=self.project,
space_id=self.space_id,
run_name="inference",
)
trackio.log({
"model": event.payload.get("model"),
"latency_ms": event.payload.get("latency_ms"),
"tokens_out": event.payload.get("tokens_out"),
"mode": event.payload.get("mode", "text"),
})
trackio.finish()
@bus.on(EventType.TOOL_CALL)
async def on_tool(event: Event):
trackio.alert(
title=f"Tool call: {event.payload.get('tool_name')}",
level=trackio.AlertLevel.INFO,
)
```
**TRL LoRA integration** (recommended for training):
```python
from trl import SFTConfig
training_args = SFTConfig(
output_dir="./checkpoints",
report_to="trackio", # ← one-liner Trackio integration via TRL
run_name="lora_minicpm5_1b",
)
```
**SQL query via CLI** (LLM-friendly):
```bash
trackio query project --project workbench --sql "SELECT run_name, loss FROM metrics ORDER BY loss LIMIT 5"
```
---
## 7. MCP Layer (Three Integration Paths)
### Path 1: Gradio Blocks + `launch(mcp_server=True)` ← simplest
```python
# app.py (simple path)
import gradio as gr
from mcp.server.fastmcp import FastMCP
demo = gr.Blocks()
# ... tabs ...
demo.launch(
mcp_server=True, # exposes /gradio_api/mcp/sse
server_port=7860,
)
```
Every `gr.Interface` function and `gr.api()` endpoint becomes an MCP tool automatically.
Docstrings become tool descriptions. Type hints become parameter schemas.
### Path 2: `gradio.Server` (custom frontend + MCP)
> New API published 2026-04-01. Extends FastAPI. Better for custom dashboards.
```python
# app.py (Server path)
from gradio import Server
from fastapi.responses import HTMLResponse
app = Server()
@app.api(name="run_inference") # Gradio-queued + MCP-compatible
def run_inference(prompt: str, model_id: str) -> str:
"""Run text inference on a MiniCPM text model."""
svc = model_registry.get(model_id)
return svc.generate(prompt)
@app.mcp.tool() # explicit MCP tool registration
async def export_gguf(model_id: str, quant: str = "Q4_K_M") -> str:
"""Export a Transformers model to GGUF format with specified quantization."""
return await export_service.run(model_id, quant)
@app.get("/", response_class=HTMLResponse) # custom frontend
async def homepage():
return open("ui/index.html").read()
app.launch(show_error=True)
```
### Path 3: FastMCP standalone + Gradio client bridge
Use when you need stateful MCP tools, or when running multiple Gradio apps and want to save memory:
```python
# mcp/server.py
from mcp.server.fastmcp import FastMCP
from gradio_client import Client
mcp = FastMCP("OpenBMBWorkbench")
_clients: dict[str, Client] = {}
def _gradio_client(space_id: str) -> Client:
if space_id not in _clients:
_clients[space_id] = Client(space_id)
return _clients[space_id]
@mcp.tool()
async def calculate(expression: str) -> str:
"""Evaluate a mathematical expression."""
import math
return str(eval(expression, {"__builtins__": {}}, vars(math)))
@mcp.tool()
async def dataset_stats(dataset_name: str, split: str = "train") -> dict:
"""Return basic statistics for a HuggingFace dataset."""
from datasets import load_dataset
ds = load_dataset(dataset_name, split=split)
return {
"rows": len(ds),
"columns": ds.column_names,
"features": str(ds.features),
}
@mcp.tool()
async def search_dataset(query: str, max_results: int = 5) -> list[dict]:
"""Search HuggingFace Hub for datasets matching a query."""
from huggingface_hub import list_datasets
results = list(list_datasets(search=query, limit=max_results))
return [{"id": d.id, "downloads": d.downloads, "tags": d.tags} for d in results]
@mcp.tool()
async def start_training(
model_id: str,
dataset_name: str,
lora_rank: int = 16,
epochs: int = 3,
) -> dict:
"""Start a LoRA fine-tuning run on the specified model and dataset."""
# Dispatch to training service via event bus
await bus.emit(Event(
type=EventType.TRAINING_STARTED,
payload={"model_id": model_id, "dataset": dataset_name,
"lora_rank": lora_rank, "epochs": epochs}
))
return {"status": "started", "model_id": model_id}
@mcp.tool()
async def evaluate_model(model_id: str, dataset_name: str) -> dict:
"""Evaluate a model on a dataset and return metrics."""
svc = model_registry.get(model_id)
return await eval_service.run(svc, dataset_name)
@mcp.tool()
async def export_gguf(model_id: str, quant: str = "Q4_K_M") -> str:
"""Convert a fine-tuned model to GGUF format."""
return await export_service.run(model_id, quant)
@mcp.tool()
async def vision_chat(image_path: str, prompt: str, model_id: str = "minicpm_v46") -> str:
"""Run multimodal inference on an image with a prompt."""
svc: MiniCPMVisionService = model_registry.get(model_id)
from PIL import Image
img = Image.open(image_path).convert("RGB")
messages = [{"role": "user", "content": [
{"type": "image", "image": img},
{"type": "text", "text": prompt},
]}]
return svc.chat(messages)
if __name__ == "__main__":
mcp.run(transport="sse", port=8081)
```
MCP SSE endpoint: `http://localhost:8081/sse`
Gradio MCP SSE: `http://localhost:7860/gradio_api/mcp/sse`
---
## 8. Training Pipeline
### 8.1 LoRA β€” Text Models (PEFT)
```python
# training/lora.py
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer, SFTConfig
class LoRATextTrainer:
DEFAULT_TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"]
def __init__(self, cfg: dict):
self.cfg = cfg
def train(self, model, tokenizer, dataset, run_name: str):
lora_cfg = LoraConfig(
r=self.cfg.get("lora_rank", 16),
lora_alpha=self.cfg.get("lora_alpha", 32),
target_modules=self.cfg.get("target_modules", self.DEFAULT_TARGET_MODULES),
lora_dropout=self.cfg.get("lora_dropout", 0.05),
bias="none",
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, lora_cfg)
model.print_trainable_parameters()
training_args = SFTConfig(
output_dir=f"./checkpoints/{run_name}",
num_train_epochs=self.cfg.get("epochs", 3),
per_device_train_batch_size=self.cfg.get("batch_size", 4),
gradient_accumulation_steps=self.cfg.get("grad_accum", 4),
learning_rate=self.cfg.get("lr", 2e-4),
fp16=False,
bf16=True,
report_to="trackio", # ← Trackio via TRL
run_name=run_name,
save_steps=100,
logging_steps=10,
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
)
trainer.train()
return model
```
### 8.2 LoRA β€” Vision Models (SWIFT / LLaMA-Factory recommended)
> OpenBMB recommends SWIFT or LLaMA-Factory for MiniCPM-V fine-tuning.
> Vision tower is frozen; only language backbone layers are adapted.
```python
# training/lora_vision.py
class LoRAVisionConfig:
"""
For MiniCPM-V 4.6 LoRA:
- Freeze vision encoder (SigLIP2-400M) + projector
- Apply LoRA only to Qwen3.5-0.8B language layers
- Use SWIFT CLI or LLaMA-Factory for full pipeline
"""
LANGUAGE_TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj"]
FROZEN_PREFIXES = ["vpm", "resampler", "vision_model"] # freeze vision
@staticmethod
def freeze_vision(model):
for name, param in model.named_parameters():
if any(name.startswith(pfx) for pfx in LoRAVisionConfig.FROZEN_PREFIXES):
param.requires_grad = False
return model
```
**SWIFT CLI approach** (recommended for MiniCPM-V 4.6):
```bash
swift sft \
--model openbmb/MiniCPM-V-4.6 \
--dataset <your-dataset> \
--lora_rank 16 \
--freeze_vit true \
--output_dir ./checkpoints/minicpm_v46_lora
```
### 8.3 HF Jobs Offload (ml-intern pattern)
For long training runs, offload to HF Jobs (free `cpu-basic`, paid `gpu-a100`):
```python
# training/hf_jobs.py
from huggingface_hub import HfApi
class HFJobsTrainer:
def submit(self, script_path: str, hardware: str = "gpu-a100") -> str:
api = HfApi()
job = api.create_job(
repo_id=f"{api.whoami()['name']}/ml-workbench-jobs",
script=script_path,
hardware=hardware,
)
return job.job_id
```
### 8.4 Evaluation
```python
# training/evaluation.py
class Evaluator:
def run(self, model, tokenizer, dataset, metrics=("perplexity", "accuracy")) -> dict:
results = {}
if "perplexity" in metrics:
results["perplexity"] = self._perplexity(model, tokenizer, dataset)
if "accuracy" in metrics:
results["accuracy"] = self._accuracy(model, tokenizer, dataset)
return results
```
---
## 9. Export & Quantization
```python
# training/export.py
import subprocess
from pathlib import Path
class GGUFExporter:
SUPPORTED_QUANTS = ["F16", "Q4_K_M", "Q5_K_M", "Q8_0", "Q2_K"]
def export(
self,
model_path: str,
output_dir: str,
quant: str = "Q4_K_M",
model_type: str = "text", # "text" | "vision"
) -> list[str]:
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Step 1: Convert HF β†’ GGUF F16
# For MiniCPM-V 4.6: convert_hf_to_gguf.py produces main gguf + mmproj
# For text models: same script, no mmproj
f16_path = output_dir / "model-f16.gguf"
subprocess.run([
"python", "convert_hf_to_gguf.py",
model_path,
"--outtype", "f16",
"--outfile", str(f16_path),
], check=True)
outputs = [str(f16_path)]
# Step 2: Quantize
if quant != "F16":
quant_path = output_dir / f"model-{quant}.gguf"
subprocess.run([
"llama-quantize",
str(f16_path),
str(quant_path),
quant,
], check=True)
outputs.append(str(quant_path))
return outputs
```
> Note: For MiniCPM-V 4.6, official GGUF files are already published at
> `openbmb/MiniCPM-V-4.6-gguf`. Download them directly unless you need custom quants.
---
## 10. Agent Mode (ml-intern Inspired)
The `agent/` module implements a lightweight research-plan-implement loop inspired by HF's
ml-intern (April 2026). This gives the workbench autonomous ML experimentation capabilities.
```
ml-intern architecture (reference):
Research β†’ browse arXiv/HF Papers, read methodology, traverse citations
Plan β†’ break task down, verify resources (models, datasets, hardware)
Implement β†’ execute scripts on HF Jobs / local GPU
Trace β†’ auto-upload sessions as JSONL to HF Dataset
```
```python
# agent/loop.py
from smolagents import CodeAgent, HfApiModel
from huggingface_hub import HfApi
class WorkbenchAgent:
"""
Lightweight agent loop for autonomous experimentation.
Uses smolagents + HF ecosystem + registered MCP tools.
"""
def __init__(self, model_id: str = "claude-sonnet-4-6"):
self.model = HfApiModel(model_id)
self.api = HfApi()
self._session_log = []
def run(self, prompt: str, max_steps: int = 20) -> str:
agent = CodeAgent(
tools=self._get_tools(),
model=self.model,
max_steps=max_steps,
)
result = agent.run(prompt)
self._save_session(prompt, result)
return result
def _save_session(self, prompt: str, result: str):
import json, datetime
session = {
"timestamp": datetime.datetime.utcnow().isoformat(),
"prompt": prompt,
"result": result,
"log": self._session_log,
}
# Auto-upload to private HF dataset (ml-intern pattern)
# Dataset: {username}/workbench-sessions
pass
def _get_tools(self):
# Expose MCP tools as smolagents tools
from mcp.server.fastmcp import FastMCP
# ...
return []
```
---
## 11. UI β€” Tab Specification
Using `gr.Blocks` with tabs. Optionally swap to `gradio.Server` for custom frontend.
```python
# app.py
import gradio as gr
from ui.chat_tab import build_chat_tab
from ui.vision_tab import build_vision_tab
from ui.train_tab import build_train_tab
from ui.dataset_tab import build_dataset_tab
from ui.export_tab import build_export_tab
from ui.traces_tab import build_traces_tab
from ui.agent_tab import build_agent_tab
from ui.notes_tab import build_notes_tab
with gr.Blocks(title="OpenBMB Workbench", theme=gr.themes.Ocean()) as demo:
gr.Markdown("# πŸ”¬ OpenBMB Local AI Workbench")
with gr.Tabs():
with gr.Tab("πŸ’¬ Chat"): build_chat_tab()
with gr.Tab("πŸ‘οΈ Vision"): build_vision_tab()
with gr.Tab("πŸ“Š Dataset"): build_dataset_tab()
with gr.Tab("πŸ‹οΈ Train"): build_train_tab()
with gr.Tab("πŸ“¦ Export"): build_export_tab()
with gr.Tab("πŸ“ˆ Traces"): build_traces_tab()
with gr.Tab("πŸ€– Agent"): build_agent_tab()
with gr.Tab("πŸ“ Field Notes"):build_notes_tab()
demo.launch(
mcp_server=True, # SSE at /gradio_api/mcp/sse
server_port=7860,
share=False,
)
```
### Tab Summary
| Tab | Purpose | Key components |
|-----|---------|----------------|
| Chat | Text inference on all text models | Model selector, streaming output, system prompt |
| Vision | Image + text inference on V-4.6/Thinking | Image upload, thinking toggle, bounding box overlay |
| Dataset | HF Hub browser + local CSV/JSON loader | Search bar, split preview, schema inspector |
| Train | LoRA training launcher | Config form, live loss chart (Trackio), checkpoint browser |
| Export | GGUF export + quantization | Model selector, quant dropdown, file download |
| Traces | Trackio run browser + comparison | Run table, metric plots, SQL query box |
| Agent | ml-intern style agent loop | Prompt, step log, paper browser, HF Jobs monitor |
| Field Notes | Correction capture β†’ retrain trigger | Image+prompt+response+correction form, JSONL export |
### Vision Tab β€” key detail
```python
# ui/vision_tab.py
def build_vision_tab():
with gr.Column():
model_dd = gr.Dropdown(
choices=["minicpm_v46", "minicpm_v46_thinking", "minicpm_o45"],
value="minicpm_v46",
label="Vision Model"
)
thinking_cb = gr.Checkbox(label="Enable Thinking Mode", value=False)
image_in = gr.Image(type="pil", label="Upload Image")
video_in = gr.Video(label="Upload Video (MiniCPM-V-4.6 supports video)")
prompt_in = gr.Textbox(label="Prompt")
submit_btn = gr.Button("Run")
output_txt = gr.Textbox(label="Response", lines=10)
def infer(model_id, thinking, image, video, prompt):
svc = model_registry.get(model_id)
content = []
if image:
content.append({"type": "image", "image": image})
if video:
content.append({"type": "video", "video": video})
content.append({"type": "text", "text": prompt})
msgs = [{"role": "user", "content": content}]
svc.thinking = thinking
return svc.chat(msgs)
submit_btn.click(infer, [model_dd, thinking_cb, image_in, video_in, prompt_in], output_txt)
```
---
## 12. Field Notes & Correction Loop
```python
# datasets/field_notes.py
import sqlite3, json
from dataclasses import dataclass, field, asdict
from datetime import datetime
@dataclass
class FieldNote:
id: str = field(default_factory=lambda: datetime.utcnow().isoformat())
model_id: str = ""
modality: str = "text" # "text" | "image" | "video" | "multimodal"
image_path: str | None = None
video_path: str | None = None
prompt: str = ""
response: str = ""
correction: str = "" # human-corrected output
tags: list = field(default_factory=list)
used_in_train: bool = False
class FieldNoteStore:
def __init__(self, db_path: str = "data/field_notes.db"):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self._init_db()
def _init_db(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
data JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
self.conn.commit()
def save(self, note: FieldNote):
self.conn.execute(
"INSERT OR REPLACE INTO notes (id, data) VALUES (?, ?)",
(note.id, json.dumps(asdict(note)))
)
self.conn.commit()
def to_hf_dataset(self, output_path: str):
"""Export uncorrected notes as HF Dataset for retraining."""
from datasets import Dataset
rows = [json.loads(r[0]) for r in
self.conn.execute("SELECT data FROM notes WHERE json_extract(data,'$.correction') != ''")]
ds = Dataset.from_list(rows)
ds.save_to_disk(output_path)
return ds
```
---
## 13. Directory Structure
```
ml-workbench/
β”œβ”€β”€ app.py # Entry point: gr.Blocks or gradio.Server
β”œβ”€β”€ requirements.txt
β”‚
β”œβ”€β”€ config/
β”‚ β”œβ”€β”€ models.yaml # ← TEMPLATE AXIS: change this for any model family
β”‚ └── training.yaml # LoRA hyperparams, export settings
β”‚
β”œβ”€β”€ core/
β”‚ β”œβ”€β”€ events.py # EventBus, EventType enum
β”‚ β”œβ”€β”€ registry.py # Registry[T] generic
β”‚ └── state.py # Global AppState dataclass
β”‚
β”œβ”€β”€ models/
β”‚ β”œβ”€β”€ base.py # Abstract ModelService
β”‚ β”œβ”€β”€ hf_loader.py # Generic HF Hub downloader
β”‚ β”œβ”€β”€ minicpm_text.py # MiniCPM5-1B, MiniCPM4.1-8B service
β”‚ β”œβ”€β”€ minicpm_vision.py # MiniCPM-V-4.6 / Thinking service
β”‚ β”œβ”€β”€ minicpm_omni.py # MiniCPM-o-4.5 service
β”‚ β”œβ”€β”€ llama_cpp_runner.py # llama-server wrapper
β”‚ β”œβ”€β”€ sglang_runner.py # SGLang server wrapper (tool-use)
β”‚ └── ollama_runner.py # Ollama client
β”‚
β”œβ”€β”€ datasets/
β”‚ β”œβ”€β”€ loader.py # Abstract DatasetLoader
β”‚ β”œβ”€β”€ hf_datasets.py # HF Hub datasets.load_dataset()
β”‚ β”œβ”€β”€ field_notes.py # SQLite-backed correction store
β”‚ └── synthetic.py # ml-intern style synthetic data gen
β”‚
β”œβ”€β”€ training/
β”‚ β”œβ”€β”€ lora.py # LoRA text trainer (PEFT + TRL)
β”‚ β”œβ”€β”€ lora_vision.py # LoRA vision config (SWIFT/LLaMA-Factory)
β”‚ β”œβ”€β”€ evaluation.py # Perplexity, accuracy, custom metrics
β”‚ β”œβ”€β”€ export.py # GGUF exporter (convert_hf_to_gguf.py)
β”‚ └── hf_jobs.py # HF Jobs offload (ml-intern pattern)
β”‚
β”œβ”€β”€ tools/
β”‚ β”œβ”€β”€ calculator.py # Safe math eval tool
β”‚ β”œβ”€β”€ dataset_stats.py # Dataset statistics tool
β”‚ β”œβ”€β”€ hf_search.py # HF Hub search tool
β”‚ └── paper_search.py # arXiv / HF Papers search (ml-intern)
β”‚
β”œβ”€β”€ tracking/
β”‚ └── trackio_service.py # Trackio integration (init/log/finish/alert)
β”‚
β”œβ”€β”€ mcp/
β”‚ β”œβ”€β”€ server.py # FastMCP standalone server
β”‚ └── tools.py # All @mcp.tool() decorators
β”‚
β”œβ”€β”€ agent/
β”‚ β”œβ”€β”€ loop.py # Research β†’ Plan β†’ Implement agent
β”‚ └── prompts/
β”‚ └── system_prompt.yaml # Agent system prompt (ml-intern style)
β”‚
β”œβ”€β”€ ui/
β”‚ β”œβ”€β”€ chat_tab.py
β”‚ β”œβ”€β”€ vision_tab.py
β”‚ β”œβ”€β”€ train_tab.py
β”‚ β”œβ”€β”€ dataset_tab.py
β”‚ β”œβ”€β”€ export_tab.py
β”‚ β”œβ”€β”€ traces_tab.py
β”‚ β”œβ”€β”€ agent_tab.py
β”‚ └── notes_tab.py
β”‚
β”œβ”€β”€ exports/ # GGUF + quantized model outputs
└── data/ # Cached datasets, field_notes.db
```
---
## 14. Configuration Schema
### training.yaml
```yaml
# config/training.yaml
lora:
rank: 16
alpha: 32
dropout: 0.05
target_modules_text:
- q_proj
- k_proj
- v_proj
- o_proj
- gate_proj
- up_proj
- down_proj
target_modules_vision:
- q_proj
- k_proj
- v_proj
- o_proj # language layers only; vision tower frozen
training:
epochs: 3
batch_size: 4
grad_accum: 4
lr: 2.0e-4
bf16: true
report_to: trackio # or: wandb, tensorboard, none
export:
default_quant: Q4_K_M
supported_quants:
- F16
- Q4_K_M
- Q5_K_M
- Q8_0
- Q2_K
trackio:
project: ml-workbench
space_id: null # set to "username/trackio-space" for HF sync
hf_jobs:
enabled: false
hardware: gpu-a100
```
---
## 15. Dependencies
```text
# requirements.txt
# Core
gradio[mcp]>=5.38.0
mcp[cli]>=1.0.0
fastapi
uvicorn[standard]
# Models
transformers>=5.7.0 # required for MiniCPM-V-4.6
torch>=2.1.0
torchvision
torchcodec # video decoding for MiniCPM-V-4.6
accelerate
bitsandbytes
# Training
peft>=0.10.0
trl>=0.9.0
datasets>=2.18.0
# Tracking
trackio>=0.1.0
# MCP + Agents
mcp>=1.0.0
smolagents>=1.0.0
# Serving backends (optional, install as needed)
# sglang # pip install sglang
# vllm # pip install vllm
# ollama # install separately
# Hub
huggingface_hub>=0.23.0
Pillow
requests
pyyaml
```
---
## 16. Hackathon Demo Flow
```
1. DATASET
Load HF dataset via Dataset tab (e.g. "tatsu-lab/alpaca", 10K rows)
β†’ EventType.DATASET_LOADED
β†’ Trackio: trackio.init("demo-run") + trackio.log({"dataset": ..., "rows": ...})
2. TRAIN
Start LoRA on MiniCPM5-1B (rank=16, 1 epoch demo)
β†’ EventType.TRAINING_STARTED
β†’ TRL SFTTrainer + report_to="trackio" β†’ live loss in Traces tab
3. EXPORT
Click "Export GGUF" β†’ Q4_K_M
β†’ convert_hf_to_gguf.py β†’ llama-quantize
β†’ EventType.EXPORT_FINISHED β†’ file download
4. LLAMA.CPP CHAT
Load exported GGUF in llama-server
β†’ Chat tab, backend: llama.cpp
β†’ Verify text generation quality
5. SWITCH TO VISION
Select MiniCPM-V-4.6 in Vision tab
Upload image β†’ enter prompt β†’ Run
6. THINKING MODE
Toggle "Enable Thinking Mode" β†’ switch to MiniCPM-V-4.6-Thinking
β†’ Observe explicit reasoning trace in response
7. FIELD NOTE
Response is wrong β†’ open Field Notes tab
β†’ Enter correction β†’ Save
β†’ EventType.FIELD_NOTE_SAVED
8. AGENT MODE
Agent tab: "Improve MiniCPM-V-4.6 on [domain] using field notes"
β†’ Research phase: browse HF Papers
β†’ Plan: dataset selection, LoRA config
β†’ Implement: trigger SWIFT fine-tune
9. SHARE TRACES
Traces tab β†’ set space_id β†’ Sync to HF Space
β†’ Shareable dashboard URL
```
---
## 17. Corrections from PRD v1
| v1 Claim | Correct v2 |
|----------|-----------|
| `trackio.trace("vision_inference")` context manager | **Does not exist.** Use `trackio.init()` + `trackio.log()` + `trackio.finish()` |
| `AutoModelForCausalLM` for MiniCPM-V-4.6 | **Wrong.** Use `AutoModelForImageTextToText` with `transformers>=5.7.0` |
| `llama-mtmd-cli` for vision deployment | Use `llama-server --mmproj <mmproj.gguf>` for server; `llama-mtmd-cli` for CLI-only |
| Run `convert_hf_to_gguf.py` for V4.6 | Official GGUFs at `openbmb/MiniCPM-V-4.6-gguf` β€” download directly; run script only for custom quants |
| Surgery scripts for V4.6 GGUF | Surgery scripts are for MiniCPM-o-2.6 only; **do not use for V4.6** |
| `future MiniCPM-o` (vague) | **MiniCPM-o-4.5** released 2026-05-17; specific HF ID available |
| `app.launch(mcp_server=True)` only option | Two paths: `gr.Blocks` + `mcp_server=True`, or `gradio.Server` with `@app.mcp.tool()` |
| FastMCP as only MCP approach | Three paths: Gradio native, gradio.Server, standalone FastMCP (see Β§7) |
| MiniCPM4.1-8B: standard architecture | Has sparse InfLLM-v2 attention; requires `--trust-remote-code` |
| LoRA for vision: PEFT only | OpenBMB recommends **SWIFT** or **LLaMA-Factory** for MiniCPM-V fine-tuning |
| No HF Jobs mention | HF Jobs integration (ml-intern pattern) enables GPU cloud offload |
| No SGLang mention | SGLang is the **recommended** backend for MiniCPM5-1B tool calling |
| No video support | MiniCPM-V-4.6 supports **video understanding** (`torchcodec` required) |
---
## 18. Roadmap & Extension Points
### Swap the model family (template portability)
To build a "Qwen3 Workbench":
1. Edit `config/models.yaml` β€” add Qwen3 model IDs
2. Create `models/qwen3_service.py` extending `ModelService`
3. Register in `app.py` β€” done
No changes to training, export, MCP, Trackio, or UI layers.
### Planned extensions
| Feature | Module | Notes |
|---------|--------|-------|
| vLLM serving tab | `models/vllm_runner.py` | Batch inference, production |
| Ollama quick-start | `models/ollama_runner.py` | Zero-config consumer demo |
| Reward model eval | `training/reward_eval.py` | For RLHF experiments |
| Synthetic data gen | `datasets/synthetic.py` | ml-intern pattern: LLM writes training data |
| Paper-to-code agent | `agent/paper_agent.py` | Read arXiv β†’ implement in workbench |
| HF Spaces deploy | `deploy/spaces.py` | One-click push as HF Space |
| VINDEX integration | `tools/vindex_tool.py` | Knowledge editing via your VINDEX engine |
| OCR pipeline hook | `datasets/ocr_loader.py` | Feed your OCR pipeline output as field notes |
| MiniCPM Desk-Pet | `agent/desk_pet.py` | LoRA persona switching via MiniCPM5-1B |
| MiniCPM-o audio | `ui/audio_tab.py` | Real-time omnimodal via MiniCPM-o-4.5 |
---
*PRD v2.0 β€” Christof Kaller / ki-fusion-labs.de β€” 2026-06-05*
*Research sources: openbmb/MiniCPM-V, openbmb/MiniCPM, gradio-app/trackio,*
*huggingface/ml-intern, huggingface.co/blog/introducing-gradio-server*