Spaces:
Sleeping
Sleeping
Commit Β·
f0d7400
1
Parent(s): be5b8c8
feat: VidyaBot v1.0 Production with offline Ollama fallback
Browse files- .dockerignore +14 -0
- Dockerfile +27 -0
- README.md +5 -5
- app.py +36 -2
- modal_convert_gguf.py +48 -103
- modal_finetune.py +84 -18
.dockerignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.gitignore
|
| 3 |
+
__pycache__
|
| 4 |
+
*.pyc
|
| 5 |
+
.pytest_cache
|
| 6 |
+
*.egg-info
|
| 7 |
+
.env
|
| 8 |
+
htmlcov/
|
| 9 |
+
.venv
|
| 10 |
+
venv/
|
| 11 |
+
data/*.db
|
| 12 |
+
data/textbooks/*
|
| 13 |
+
backend/llm/models/mistral-vidyabot-hf/
|
| 14 |
+
backend/llm/models/llama.cpp/
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
curl \
|
| 8 |
+
wget \
|
| 9 |
+
git \
|
| 10 |
+
build-essential \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Install Ollama
|
| 14 |
+
RUN curl -fsSL https://ollama.com/install.sh | sh
|
| 15 |
+
|
| 16 |
+
# Copy and install python dependencies
|
| 17 |
+
COPY backend/requirements.txt ./requirements.txt
|
| 18 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 19 |
+
|
| 20 |
+
# Copy application code
|
| 21 |
+
COPY . .
|
| 22 |
+
|
| 23 |
+
# Expose port
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
|
| 26 |
+
# Run cross-platform launcher
|
| 27 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -11,7 +11,7 @@ We have successfully earned all **5 merit badges** for the **Build Small 2026**
|
|
| 11 |
* π¦ **Llama Champion** β Runs via standard llama.cpp runtime embedded in local Ollama instance
|
| 12 |
* π¨ **Off-Brand** β Completely custom Gradio frontend with Indian flag aesthetics and responsive layouts
|
| 13 |
* π **Field Notes** β Complete 2,000-word engineering retro published at `docs/field_notes.md`
|
| 14 |
-
* π― **Well-Tuned** β Mistral 7B fine-tuned on
|
| 15 |
|
| 16 |
---
|
| 17 |
|
|
@@ -24,7 +24,7 @@ VidyaBot uses **Mistral 7B Instruct fine-tuned on student Q&A pairs** from NCERT
|
|
| 24 |
| Detail | Value |
|
| 25 |
|--------|-------|
|
| 26 |
| **Base model** | `mistralai/Mistral-7B-Instruct-v0.1` |
|
| 27 |
-
| **Training data** |
|
| 28 |
| **Method** | QLoRA (4-bit quantization + LoRA adapters) |
|
| 29 |
| **LoRA config** | r=8, alpha=16, targets: q/v/k/o_proj |
|
| 30 |
| **Hardware** | Modal A10G GPU (24GB VRAM) |
|
|
@@ -44,7 +44,7 @@ Base `mistral:latest` is a strong general-purpose model, but fine-tuning on NCER
|
|
| 44 |
### Fine-Tuning Pipeline
|
| 45 |
|
| 46 |
```
|
| 47 |
-
Student Q&A Data (
|
| 48 |
β
|
| 49 |
[Modal A10G GPU]
|
| 50 |
QLoRA: Mistral-7B-Instruct + LoRA adapters (r=8)
|
|
@@ -133,7 +133,7 @@ Over **200 million** Indian students use textbooks from national and state board
|
|
| 133 |
βΌ
|
| 134 |
βββββββββββββββββββββββββββββββββ
|
| 135 |
β OLLAMA LOCAL INFERENCE (CPU) β
|
| 136 |
-
β - Model:
|
| 137 |
β - Cost: $0.00 | TTFT: <2s β
|
| 138 |
βββββββββββββββββ¬ββββββββββββββββ
|
| 139 |
β
|
|
@@ -229,7 +229,7 @@ Create a `.env` file in the root directory:
|
|
| 229 |
```env
|
| 230 |
LLM_BACKEND=ollama
|
| 231 |
OLLAMA_BASE_URL=http://localhost:11434
|
| 232 |
-
OLLAMA_MODEL=
|
| 233 |
```
|
| 234 |
|
| 235 |
### 4. Start Unified Application
|
|
|
|
| 11 |
* π¦ **Llama Champion** β Runs via standard llama.cpp runtime embedded in local Ollama instance
|
| 12 |
* π¨ **Off-Brand** β Completely custom Gradio frontend with Indian flag aesthetics and responsive layouts
|
| 13 |
* π **Field Notes** β Complete 2,000-word engineering retro published at `docs/field_notes.md`
|
| 14 |
+
* π― **Well-Tuned** β Mistral 7B fine-tuned on 103 student Q&A pairs via Modal A10G GPU (LoRA/QLoRA)
|
| 15 |
|
| 16 |
---
|
| 17 |
|
|
|
|
| 24 |
| Detail | Value |
|
| 25 |
|--------|-------|
|
| 26 |
| **Base model** | `mistralai/Mistral-7B-Instruct-v0.1` |
|
| 27 |
+
| **Training data** | 103 hand-crafted + synthetic NCERT Q&A pairs |
|
| 28 |
| **Method** | QLoRA (4-bit quantization + LoRA adapters) |
|
| 29 |
| **LoRA config** | r=8, alpha=16, targets: q/v/k/o_proj |
|
| 30 |
| **Hardware** | Modal A10G GPU (24GB VRAM) |
|
|
|
|
| 44 |
### Fine-Tuning Pipeline
|
| 45 |
|
| 46 |
```
|
| 47 |
+
Student Q&A Data (103 pairs)
|
| 48 |
β
|
| 49 |
[Modal A10G GPU]
|
| 50 |
QLoRA: Mistral-7B-Instruct + LoRA adapters (r=8)
|
|
|
|
| 133 |
βΌ
|
| 134 |
βββββββββββββββββββββββββββββββββ
|
| 135 |
β OLLAMA LOCAL INFERENCE (CPU) β
|
| 136 |
+
β - Model: mistral-vidyabot β
|
| 137 |
β - Cost: $0.00 | TTFT: <2s β
|
| 138 |
βββββββββββββββββ¬ββββββββββββββββ
|
| 139 |
β
|
|
|
|
| 229 |
```env
|
| 230 |
LLM_BACKEND=ollama
|
| 231 |
OLLAMA_BASE_URL=http://localhost:11434
|
| 232 |
+
OLLAMA_MODEL=mistral-vidyabot
|
| 233 |
```
|
| 234 |
|
| 235 |
### 4. Start Unified Application
|
app.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
-
# app.py β Main entry point for VidyaBot Gradio Edition
|
| 2 |
import sys
|
| 3 |
import warnings
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
# Enforce UTF-8 encoding on standard streams to prevent Windows console encoding crashes
|
| 6 |
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -11,6 +14,36 @@ if hasattr(sys.stderr, 'reconfigure'):
|
|
| 11 |
# Suppress Gradio's warning about Blocks parameters theme/css moving to launch()
|
| 12 |
warnings.filterwarnings("ignore", category=UserWarning, module="gradio")
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import gradio as gr
|
| 15 |
from backend.main import app as fastapi_app
|
| 16 |
from gradio_app import create_demo
|
|
@@ -20,4 +53,5 @@ app = gr.mount_gradio_app(fastapi_app, demo, path="/")
|
|
| 20 |
|
| 21 |
if __name__ == "__main__":
|
| 22 |
import uvicorn
|
| 23 |
-
|
|
|
|
|
|
| 1 |
+
# app.py β Main entry point for VidyaBot Gradio Edition (Cross-platform local and HF Space)
|
| 2 |
import sys
|
| 3 |
import warnings
|
| 4 |
+
import subprocess
|
| 5 |
+
import time
|
| 6 |
+
import os
|
| 7 |
|
| 8 |
# Enforce UTF-8 encoding on standard streams to prevent Windows console encoding crashes
|
| 9 |
if hasattr(sys.stdout, 'reconfigure'):
|
|
|
|
| 14 |
# Suppress Gradio's warning about Blocks parameters theme/css moving to launch()
|
| 15 |
warnings.filterwarnings("ignore", category=UserWarning, module="gradio")
|
| 16 |
|
| 17 |
+
print("π VidyaBot Launch Sequence")
|
| 18 |
+
print("=" * 50)
|
| 19 |
+
|
| 20 |
+
# Start Ollama daemon on Linux (e.g. Hugging Face Spaces)
|
| 21 |
+
if sys.platform != "win32":
|
| 22 |
+
print("π¦ Starting Ollama daemon...")
|
| 23 |
+
try:
|
| 24 |
+
subprocess.Popen(
|
| 25 |
+
["ollama", "serve"],
|
| 26 |
+
stdout=subprocess.DEVNULL,
|
| 27 |
+
stderr=subprocess.DEVNULL,
|
| 28 |
+
)
|
| 29 |
+
time.sleep(3)
|
| 30 |
+
|
| 31 |
+
# Pull base model in the background as fallback
|
| 32 |
+
print("π₯ Ensuring mistral:latest is available...")
|
| 33 |
+
subprocess.Popen(
|
| 34 |
+
["ollama", "pull", "mistral:latest"],
|
| 35 |
+
stdout=subprocess.DEVNULL,
|
| 36 |
+
stderr=subprocess.DEVNULL,
|
| 37 |
+
)
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"β οΈ Could not start Ollama: {e}")
|
| 40 |
+
else:
|
| 41 |
+
print("π» Windows environment detected. Assumes local Ollama is running.")
|
| 42 |
+
|
| 43 |
+
print("=" * 50)
|
| 44 |
+
print("π― Launching VidyaBot Gradio UI...")
|
| 45 |
+
print("=" * 50)
|
| 46 |
+
|
| 47 |
import gradio as gr
|
| 48 |
from backend.main import app as fastapi_app
|
| 49 |
from gradio_app import create_demo
|
|
|
|
| 53 |
|
| 54 |
if __name__ == "__main__":
|
| 55 |
import uvicorn
|
| 56 |
+
port = int(os.environ.get("PORT", 7860))
|
| 57 |
+
uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
|
modal_convert_gguf.py
CHANGED
|
@@ -1,38 +1,34 @@
|
|
| 1 |
"""
|
| 2 |
-
modal_convert_gguf.py β Download
|
| 3 |
|
| 4 |
Run this AFTER modal_finetune.py completes.
|
| 5 |
|
| 6 |
Steps:
|
| 7 |
-
1. Download
|
| 8 |
-
2.
|
| 9 |
-
3. Convert HuggingFace safetensors β GGUF (Q4_K_M quantization)
|
| 10 |
-
4. Place GGUF file where Ollama can read it
|
| 11 |
|
| 12 |
-
Usage (from project root
|
| 13 |
python modal_convert_gguf.py
|
| 14 |
-
|
| 15 |
-
Requirements:
|
| 16 |
-
pip install modal
|
| 17 |
-
pip install huggingface-hub
|
| 18 |
-
Git + cmake available (for llama.cpp build)
|
| 19 |
"""
|
| 20 |
|
| 21 |
import subprocess
|
| 22 |
import sys
|
| 23 |
-
import os
|
| 24 |
from pathlib import Path
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
# Configuration
|
| 29 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
VOLUME_NAME = "vidyabot-model-output"
|
| 31 |
-
|
| 32 |
LOCAL_MODELS_DIR = Path("backend/llm/models")
|
| 33 |
-
|
| 34 |
-
LOCAL_GGUF_PATH = LOCAL_MODELS_DIR / "mistral-vidyabot.Q4_K_M.gguf"
|
| 35 |
-
LLAMACPP_DIR = LOCAL_MODELS_DIR / "llama.cpp"
|
| 36 |
|
| 37 |
|
| 38 |
def run(cmd: str, cwd: Path = None, check: bool = True) -> subprocess.CompletedProcess:
|
|
@@ -49,93 +45,38 @@ def run(cmd: str, cwd: Path = None, check: bool = True) -> subprocess.CompletedP
|
|
| 49 |
|
| 50 |
|
| 51 |
def step1_download_from_modal():
|
| 52 |
-
"""Download the
|
| 53 |
-
print("\n[1/
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
# Use the Modal CLI to download
|
| 57 |
cmd = (
|
| 58 |
-
f"
|
| 59 |
-
f"{
|
| 60 |
)
|
| 61 |
run(cmd)
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
"""Clone or update llama.cpp for the GGUF conversion script."""
|
| 67 |
-
print("\n[2/4] Setting up llama.cpp conversion tools...")
|
| 68 |
-
LOCAL_MODELS_DIR.mkdir(parents=True, exist_ok=True)
|
| 69 |
-
|
| 70 |
-
if not LLAMACPP_DIR.exists():
|
| 71 |
-
run(
|
| 72 |
-
f"git clone --depth 1 https://github.com/ggerganov/llama.cpp {LLAMACPP_DIR}",
|
| 73 |
-
cwd=LOCAL_MODELS_DIR
|
| 74 |
-
)
|
| 75 |
-
# Install conversion script requirements
|
| 76 |
-
req_file = LLAMACPP_DIR / "requirements.txt"
|
| 77 |
-
if req_file.exists():
|
| 78 |
-
run(f"{sys.executable} -m pip install -r {req_file}", check=False)
|
| 79 |
-
print(f" β
llama.cpp cloned to: {LLAMACPP_DIR}")
|
| 80 |
else:
|
| 81 |
-
print(f"
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def step3_convert_to_gguf():
|
| 85 |
-
"""Convert HuggingFace safetensors model to GGUF format."""
|
| 86 |
-
print("\n[3/4] Converting HuggingFace model to GGUF (Q4_K_M)...")
|
| 87 |
-
|
| 88 |
-
convert_script = LLAMACPP_DIR / "convert_hf_to_gguf.py"
|
| 89 |
-
if not convert_script.exists():
|
| 90 |
-
# Older llama.cpp uses convert.py
|
| 91 |
-
convert_script = LLAMACPP_DIR / "convert.py"
|
| 92 |
-
if not convert_script.exists():
|
| 93 |
-
print(" β llama.cpp conversion script not found.")
|
| 94 |
-
print(" Try: pip install llama-cpp-python and skip this step.")
|
| 95 |
sys.exit(1)
|
| 96 |
|
| 97 |
-
LOCAL_GGUF_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 98 |
-
|
| 99 |
-
# Step A: Convert to unquantized GGUF first
|
| 100 |
-
unquantized_gguf = LOCAL_MODELS_DIR / "mistral-vidyabot-f16.gguf"
|
| 101 |
-
run(
|
| 102 |
-
f"{sys.executable} {convert_script} "
|
| 103 |
-
f"{LOCAL_HF_MODEL_DIR} "
|
| 104 |
-
f"--outfile {unquantized_gguf} "
|
| 105 |
-
f"--outtype f16"
|
| 106 |
-
)
|
| 107 |
-
print(f" β
Unquantized GGUF created: {unquantized_gguf}")
|
| 108 |
-
|
| 109 |
-
# Step B: Quantize to Q4_K_M (4-bit, ~4GB, optimal for CPU inference)
|
| 110 |
-
# Try llama-quantize binary if built, otherwise use Python-based quantization
|
| 111 |
-
quantize_bin = LLAMACPP_DIR / "build" / "bin" / "llama-quantize"
|
| 112 |
-
if not quantize_bin.exists():
|
| 113 |
-
quantize_bin = LLAMACPP_DIR / "quantize" # Older path
|
| 114 |
-
|
| 115 |
-
if quantize_bin.exists():
|
| 116 |
-
run(f"{quantize_bin} {unquantized_gguf} {LOCAL_GGUF_PATH} Q4_K_M")
|
| 117 |
-
# Clean up unquantized file
|
| 118 |
-
unquantized_gguf.unlink(missing_ok=True)
|
| 119 |
-
print(f" β
Quantized to Q4_K_M: {LOCAL_GGUF_PATH}")
|
| 120 |
-
else:
|
| 121 |
-
# Skip quantization β use f16 directly (larger but works)
|
| 122 |
-
print(" β οΈ llama-quantize binary not found β using f16 GGUF (larger, ~13GB)")
|
| 123 |
-
print(" To build quantize: cd backend/llm/models/llama.cpp && cmake -B build && cmake --build build -t llama-quantize")
|
| 124 |
-
LOCAL_GGUF_PATH = unquantized_gguf
|
| 125 |
-
print(f" β
Using f16 GGUF: {LOCAL_GGUF_PATH}")
|
| 126 |
-
|
| 127 |
-
return LOCAL_GGUF_PATH
|
| 128 |
-
|
| 129 |
|
| 130 |
-
def
|
| 131 |
-
"""Create the Ollama model from the GGUF file."""
|
| 132 |
-
print("\n[
|
| 133 |
|
| 134 |
modelfile_path = LOCAL_MODELS_DIR / "Modelfile"
|
| 135 |
|
| 136 |
# Write the Modelfile pointing to our GGUF
|
| 137 |
-
|
| 138 |
-
|
|
|
|
| 139 |
modelfile_content = f"""FROM {abs_gguf}
|
| 140 |
|
| 141 |
# VidyaBot fine-tuned model parameters
|
|
@@ -143,6 +84,10 @@ PARAMETER num_ctx 2048
|
|
| 143 |
PARAMETER temperature 0.7
|
| 144 |
PARAMETER top_p 0.9
|
| 145 |
PARAMETER repeat_penalty 1.1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
# System prompt baked into the model
|
| 148 |
SYSTEM "You are VidyaBot, an expert AI tutor for Indian school students studying NCERT curriculum. You give clear, concise, and accurate answers in 2-4 sentences. Always use correct scientific or mathematical terminology as found in NCERT textbooks."
|
|
@@ -166,31 +111,31 @@ SYSTEM "You are VidyaBot, an expert AI tutor for Indian school students studying
|
|
| 166 |
|
| 167 |
def main():
|
| 168 |
print("=" * 60)
|
| 169 |
-
print(" VidyaBot GGUF
|
| 170 |
-
print(" Modal
|
| 171 |
print("=" * 60)
|
| 172 |
|
| 173 |
# Check if model already downloaded locally
|
| 174 |
-
if
|
| 175 |
-
print(f"\n β
Model already
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
if skip != "n":
|
| 178 |
print(" Skipping download...")
|
| 179 |
-
|
| 180 |
-
gguf_path = step3_convert_to_gguf()
|
| 181 |
-
step4_create_ollama_model(gguf_path)
|
| 182 |
return
|
| 183 |
|
| 184 |
step1_download_from_modal()
|
| 185 |
-
|
| 186 |
-
gguf_path = step3_convert_to_gguf()
|
| 187 |
-
step4_create_ollama_model(gguf_path)
|
| 188 |
|
| 189 |
print("\n" + "=" * 60)
|
| 190 |
print(" β
ALL STEPS COMPLETE")
|
| 191 |
print()
|
| 192 |
print(" Your fine-tuned model is ready:")
|
| 193 |
-
print(f" GGUF: {
|
| 194 |
print(f" Ollama: mistral-vidyabot")
|
| 195 |
print()
|
| 196 |
print(" Update your .env file:")
|
|
|
|
| 1 |
"""
|
| 2 |
+
modal_convert_gguf.py β Download GGUF from Modal Volume & Register with Ollama
|
| 3 |
|
| 4 |
Run this AFTER modal_finetune.py completes.
|
| 5 |
|
| 6 |
Steps:
|
| 7 |
+
1. Download the quantized GGUF model directly from Modal Volume to local disk
|
| 8 |
+
2. Create/update the Ollama model pointing to the downloaded GGUF file
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
Usage (from project root):
|
| 11 |
python modal_convert_gguf.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"""
|
| 13 |
|
| 14 |
import subprocess
|
| 15 |
import sys
|
|
|
|
| 16 |
from pathlib import Path
|
| 17 |
|
| 18 |
+
# Enforce UTF-8 encoding on standard streams to prevent Windows console encoding crashes
|
| 19 |
+
if hasattr(sys.stdout, 'reconfigure'):
|
| 20 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 21 |
+
if hasattr(sys.stderr, 'reconfigure'):
|
| 22 |
+
sys.stderr.reconfigure(encoding='utf-8')
|
| 23 |
+
|
| 24 |
|
| 25 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
# Configuration
|
| 27 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
VOLUME_NAME = "vidyabot-model-output"
|
| 29 |
+
REMOTE_GGUF_FILENAME = "mistral-vidyabot.Q4_K_M.gguf"
|
| 30 |
LOCAL_MODELS_DIR = Path("backend/llm/models")
|
| 31 |
+
LOCAL_GGUF_PATH = LOCAL_MODELS_DIR / REMOTE_GGUF_FILENAME
|
|
|
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
def run(cmd: str, cwd: Path = None, check: bool = True) -> subprocess.CompletedProcess:
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
def step1_download_from_modal():
|
| 48 |
+
"""Download the GGUF model file from Modal Volume to local disk."""
|
| 49 |
+
print(f"\n[1/2] Downloading GGUF model '{REMOTE_GGUF_FILENAME}' from Modal Volume '{VOLUME_NAME}'...")
|
| 50 |
+
LOCAL_MODELS_DIR.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
|
| 52 |
+
# Use the Modal CLI in the virtual environment if available
|
| 53 |
+
python_dir = Path(sys.executable).parent
|
| 54 |
+
modal_bin = python_dir / "modal.exe" if (python_dir / "modal.exe").exists() else (python_dir / "modal" if (python_dir / "modal").exists() else "modal")
|
| 55 |
|
|
|
|
| 56 |
cmd = (
|
| 57 |
+
f'"{modal_bin}" volume get {VOLUME_NAME} '
|
| 58 |
+
f"{REMOTE_GGUF_FILENAME} {LOCAL_MODELS_DIR}"
|
| 59 |
)
|
| 60 |
run(cmd)
|
| 61 |
+
|
| 62 |
+
# Check if download succeeded
|
| 63 |
+
if LOCAL_GGUF_PATH.exists():
|
| 64 |
+
print(f" β
Model downloaded successfully to: {LOCAL_GGUF_PATH}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
else:
|
| 66 |
+
print(f" β GGUF file not found at {LOCAL_GGUF_PATH} after download.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
sys.exit(1)
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
def step2_create_ollama_model():
|
| 71 |
+
"""Create the Ollama model from the downloaded GGUF file."""
|
| 72 |
+
print("\n[2/2] Creating Ollama model 'mistral-vidyabot'...")
|
| 73 |
|
| 74 |
modelfile_path = LOCAL_MODELS_DIR / "Modelfile"
|
| 75 |
|
| 76 |
# Write the Modelfile pointing to our GGUF
|
| 77 |
+
abs_gguf = LOCAL_GGUF_PATH.resolve()
|
| 78 |
+
|
| 79 |
+
# Check if Modelfile exists and update it
|
| 80 |
modelfile_content = f"""FROM {abs_gguf}
|
| 81 |
|
| 82 |
# VidyaBot fine-tuned model parameters
|
|
|
|
| 84 |
PARAMETER temperature 0.7
|
| 85 |
PARAMETER top_p 0.9
|
| 86 |
PARAMETER repeat_penalty 1.1
|
| 87 |
+
PARAMETER stop "<s>"
|
| 88 |
+
PARAMETER stop "</s>"
|
| 89 |
+
PARAMETER stop "[INST]"
|
| 90 |
+
PARAMETER stop "[/INST]"
|
| 91 |
|
| 92 |
# System prompt baked into the model
|
| 93 |
SYSTEM "You are VidyaBot, an expert AI tutor for Indian school students studying NCERT curriculum. You give clear, concise, and accurate answers in 2-4 sentences. Always use correct scientific or mathematical terminology as found in NCERT textbooks."
|
|
|
|
| 111 |
|
| 112 |
def main():
|
| 113 |
print("=" * 60)
|
| 114 |
+
print(" VidyaBot GGUF Download & Registration Pipeline")
|
| 115 |
+
print(" Modal Volume -> Local GGUF -> Ollama")
|
| 116 |
print("=" * 60)
|
| 117 |
|
| 118 |
# Check if model already downloaded locally
|
| 119 |
+
if LOCAL_GGUF_PATH.exists():
|
| 120 |
+
print(f"\n β
GGUF Model already exists at: {LOCAL_GGUF_PATH}")
|
| 121 |
+
if not sys.stdin.isatty():
|
| 122 |
+
print(" Non-interactive mode detected. Skipping download step...")
|
| 123 |
+
skip = "y"
|
| 124 |
+
else:
|
| 125 |
+
skip = input(" Skip download step? [Y/n]: ").strip().lower()
|
| 126 |
if skip != "n":
|
| 127 |
print(" Skipping download...")
|
| 128 |
+
step2_create_ollama_model()
|
|
|
|
|
|
|
| 129 |
return
|
| 130 |
|
| 131 |
step1_download_from_modal()
|
| 132 |
+
step2_create_ollama_model()
|
|
|
|
|
|
|
| 133 |
|
| 134 |
print("\n" + "=" * 60)
|
| 135 |
print(" β
ALL STEPS COMPLETE")
|
| 136 |
print()
|
| 137 |
print(" Your fine-tuned model is ready:")
|
| 138 |
+
print(f" GGUF: {LOCAL_GGUF_PATH}")
|
| 139 |
print(f" Ollama: mistral-vidyabot")
|
| 140 |
print()
|
| 141 |
print(" Update your .env file:")
|
modal_finetune.py
CHANGED
|
@@ -28,6 +28,7 @@ MODEL_OUTPUT_DIR = "/model-output"
|
|
| 28 |
# Container image with all ML dependencies
|
| 29 |
image = (
|
| 30 |
modal.Image.debian_slim(python_version="3.11")
|
|
|
|
| 31 |
.pip_install(
|
| 32 |
"transformers>=4.40.0",
|
| 33 |
"torch>=2.2.0",
|
|
@@ -39,6 +40,7 @@ image = (
|
|
| 39 |
"scipy",
|
| 40 |
"sentencepiece",
|
| 41 |
"protobuf",
|
|
|
|
| 42 |
)
|
| 43 |
.add_local_file(
|
| 44 |
"data/finetuning/student_qa.jsonl",
|
|
@@ -233,27 +235,94 @@ def finetune_mistral():
|
|
| 233 |
print(f" Loss: {train_result.training_loss:.4f}")
|
| 234 |
print(f" Runtime: {train_result.metrics.get('train_runtime', 0):.0f}s")
|
| 235 |
|
| 236 |
-
# ββ Step 7: Merge LoRA into base model +
|
| 237 |
-
print(f"\n[7/7] Merging LoRA weights and
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
-
#
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
volume.commit()
|
| 248 |
|
| 249 |
-
print(f"\n β
Merged model
|
| 250 |
-
print(" π¦
|
| 251 |
print("\n" + "=" * 60)
|
| 252 |
-
print(" FINE-TUNING COMPLETE")
|
| 253 |
-
print(" Next: run `
|
| 254 |
print("=" * 60)
|
| 255 |
|
| 256 |
-
return
|
| 257 |
|
| 258 |
|
| 259 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -290,11 +359,8 @@ def main():
|
|
| 290 |
print()
|
| 291 |
|
| 292 |
result = finetune_mistral.remote()
|
| 293 |
-
print(f"\nβ
Job complete!
|
| 294 |
print(f" Path: {result}")
|
| 295 |
print()
|
| 296 |
-
print("π₯ To download the
|
| 297 |
-
print(" modal volume get vidyabot-model-output mistral-vidyabot-merged ./backend/llm/models/mistral-vidyabot-hf")
|
| 298 |
-
print()
|
| 299 |
-
print("π§ Then convert to GGUF and load into Ollama:")
|
| 300 |
print(" python modal_convert_gguf.py")
|
|
|
|
| 28 |
# Container image with all ML dependencies
|
| 29 |
image = (
|
| 30 |
modal.Image.debian_slim(python_version="3.11")
|
| 31 |
+
.apt_install("git", "build-essential", "cmake")
|
| 32 |
.pip_install(
|
| 33 |
"transformers>=4.40.0",
|
| 34 |
"torch>=2.2.0",
|
|
|
|
| 40 |
"scipy",
|
| 41 |
"sentencepiece",
|
| 42 |
"protobuf",
|
| 43 |
+
"gguf>=0.1.0",
|
| 44 |
)
|
| 45 |
.add_local_file(
|
| 46 |
"data/finetuning/student_qa.jsonl",
|
|
|
|
| 235 |
print(f" Loss: {train_result.training_loss:.4f}")
|
| 236 |
print(f" Runtime: {train_result.metrics.get('train_runtime', 0):.0f}s")
|
| 237 |
|
| 238 |
+
# ββ Step 7: Merge LoRA into base model + GGUF Conversion βββββββββββββ
|
| 239 |
+
print(f"\n[7/7] Merging LoRA weights and converting to GGUF format...")
|
| 240 |
+
|
| 241 |
+
# Save the adapter first (PEFT)
|
| 242 |
+
adapter_path = "/tmp/vidyabot-adapter"
|
| 243 |
+
model.save_pretrained(adapter_path)
|
| 244 |
+
print(f" β
PEFT adapter saved to: {adapter_path}")
|
| 245 |
+
|
| 246 |
+
# Free memory of the 4-bit model to avoid OOM
|
| 247 |
+
del model
|
| 248 |
+
del trainer
|
| 249 |
+
import gc
|
| 250 |
+
gc.collect()
|
| 251 |
+
torch.cuda.empty_cache()
|
| 252 |
+
|
| 253 |
+
# Load base model in float16 (unquantized) to merge
|
| 254 |
+
print("\n Loading base model in float16 to merge LoRA adapters...")
|
| 255 |
+
from peft import PeftModel
|
| 256 |
+
base_model = AutoModelForCausalLM.from_pretrained(
|
| 257 |
+
model_name,
|
| 258 |
+
torch_dtype=torch.float16,
|
| 259 |
+
device_map="auto",
|
| 260 |
+
)
|
| 261 |
+
peft_model = PeftModel.from_pretrained(base_model, adapter_path)
|
| 262 |
+
print(" Merging weights...")
|
| 263 |
+
merged_model = peft_model.merge_and_unload()
|
| 264 |
+
|
| 265 |
+
# Save to /tmp to avoid Volume size exhaustion
|
| 266 |
+
temp_merged_path = "/tmp/mistral-vidyabot-merged"
|
| 267 |
+
print(f" Saving merged float16 model to {temp_merged_path}...")
|
| 268 |
+
merged_model.save_pretrained(temp_merged_path, safe_serialization=True)
|
| 269 |
+
tokenizer.save_pretrained(temp_merged_path)
|
| 270 |
+
|
| 271 |
+
# Free memory again before starting conversion/quantization
|
| 272 |
+
del merged_model
|
| 273 |
+
del peft_model
|
| 274 |
+
del base_model
|
| 275 |
+
gc.collect()
|
| 276 |
+
torch.cuda.empty_cache()
|
| 277 |
+
|
| 278 |
+
# Clone llama.cpp to /tmp
|
| 279 |
+
print("\n Cloning llama.cpp for GGUF conversion...")
|
| 280 |
+
import subprocess
|
| 281 |
+
subprocess.run(
|
| 282 |
+
"git clone --depth 1 https://github.com/ggerganov/llama.cpp /tmp/llama.cpp",
|
| 283 |
+
shell=True,
|
| 284 |
+
check=True
|
| 285 |
+
)
|
| 286 |
|
| 287 |
+
# Convert HF to GGUF f16
|
| 288 |
+
temp_gguf_f16 = "/tmp/mistral-vidyabot-f16.gguf"
|
| 289 |
+
print(f"\n Converting HF model to GGUF f16: {temp_gguf_f16}...")
|
| 290 |
+
subprocess.run(
|
| 291 |
+
f"python /tmp/llama.cpp/convert_hf_to_gguf.py {temp_merged_path} "
|
| 292 |
+
f"--outfile {temp_gguf_f16} --outtype f16",
|
| 293 |
+
shell=True,
|
| 294 |
+
check=True
|
| 295 |
+
)
|
| 296 |
|
| 297 |
+
# Compile llama-quantize
|
| 298 |
+
print("\n Compiling llama-quantize tool...")
|
| 299 |
+
subprocess.run(
|
| 300 |
+
"cmake -B /tmp/llama.cpp/build -S /tmp/llama.cpp -DCMAKE_BUILD_TYPE=Release && "
|
| 301 |
+
"cmake --build /tmp/llama.cpp/build --config Release --target llama-quantize",
|
| 302 |
+
shell=True,
|
| 303 |
+
check=True
|
| 304 |
+
)
|
| 305 |
|
| 306 |
+
# Quantize to Q4_K_M and save directly to persistent volume!
|
| 307 |
+
final_gguf_path = f"{MODEL_OUTPUT_DIR}/mistral-vidyabot.Q4_K_M.gguf"
|
| 308 |
+
print(f"\n Quantizing GGUF f16 -> Q4_K_M: {final_gguf_path}...")
|
| 309 |
+
subprocess.run(
|
| 310 |
+
f"/tmp/llama.cpp/build/bin/llama-quantize {temp_gguf_f16} {final_gguf_path} Q4_K_M",
|
| 311 |
+
shell=True,
|
| 312 |
+
check=True
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# Commit Volume
|
| 316 |
volume.commit()
|
| 317 |
|
| 318 |
+
print(f"\n β
Merged model successfully quantized to GGUF!")
|
| 319 |
+
print(f" π¦ GGUF model committed to Modal Volume 'vidyabot-model-output'")
|
| 320 |
print("\n" + "=" * 60)
|
| 321 |
+
print(" FINE-TUNING & GGUF CONVERSION COMPLETE")
|
| 322 |
+
print(" Next: run `python modal_convert_gguf.py` to download the GGUF file")
|
| 323 |
print("=" * 60)
|
| 324 |
|
| 325 |
+
return final_gguf_path
|
| 326 |
|
| 327 |
|
| 328 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 359 |
print()
|
| 360 |
|
| 361 |
result = finetune_mistral.remote()
|
| 362 |
+
print(f"\nβ
Job complete! GGUF Model saved to Modal Volume.")
|
| 363 |
print(f" Path: {result}")
|
| 364 |
print()
|
| 365 |
+
print("π₯ To download the GGUF model and set up Ollama, run:")
|
|
|
|
|
|
|
|
|
|
| 366 |
print(" python modal_convert_gguf.py")
|