Spaces:
Running on Zero
Running on Zero
| # 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* |