shankarsain commited on
Commit
f0d7400
Β·
1 Parent(s): be5b8c8

feat: VidyaBot v1.0 Production with offline Ollama fallback

Browse files
Files changed (6) hide show
  1. .dockerignore +14 -0
  2. Dockerfile +27 -0
  3. README.md +5 -5
  4. app.py +36 -2
  5. modal_convert_gguf.py +48 -103
  6. 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 60+ student Q&A pairs via Modal A10G GPU (LoRA/QLoRA)
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** | 60+ 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,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 (60 pairs)
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: llama3.2:latest β”‚
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=llama3.2:latest
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
- uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)
 
 
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 + Convert Fine-Tuned Model to GGUF
3
 
4
  Run this AFTER modal_finetune.py completes.
5
 
6
  Steps:
7
- 1. Download merged model from Modal Volume to local disk
8
- 2. Clone llama.cpp (for the conversion script)
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, after fine-tuning completes):
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
- REMOTE_MODEL_PATH = "mistral-vidyabot-merged"
32
  LOCAL_MODELS_DIR = Path("backend/llm/models")
33
- LOCAL_HF_MODEL_DIR = LOCAL_MODELS_DIR / "mistral-vidyabot-hf"
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 merged HF model from Modal Volume to local disk."""
53
- print("\n[1/4] Downloading merged model from Modal Volume...")
54
- LOCAL_HF_MODEL_DIR.mkdir(parents=True, exist_ok=True)
 
 
 
 
55
 
56
- # Use the Modal CLI to download
57
  cmd = (
58
- f"modal volume get {VOLUME_NAME} "
59
- f"{REMOTE_MODEL_PATH} {LOCAL_HF_MODEL_DIR}"
60
  )
61
  run(cmd)
62
- print(f" βœ… Model downloaded to: {LOCAL_HF_MODEL_DIR}")
63
-
64
-
65
- def step2_get_llamacpp():
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" βœ… llama.cpp already present at: {LLAMACPP_DIR}")
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 step4_create_ollama_model(gguf_path: Path):
131
- """Create the Ollama model from the GGUF file."""
132
- print("\n[4/4] Creating Ollama model 'mistral-vidyabot'...")
133
 
134
  modelfile_path = LOCAL_MODELS_DIR / "Modelfile"
135
 
136
  # Write the Modelfile pointing to our GGUF
137
- # Use absolute path for FROM directive
138
- abs_gguf = gguf_path.resolve()
 
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 Conversion Pipeline")
170
- print(" Modal β†’ HuggingFace β†’ GGUF β†’ Ollama")
171
  print("=" * 60)
172
 
173
  # Check if model already downloaded locally
174
- if LOCAL_HF_MODEL_DIR.exists() and any(LOCAL_HF_MODEL_DIR.iterdir()):
175
- print(f"\n βœ… Model already downloaded at: {LOCAL_HF_MODEL_DIR}")
176
- skip = input(" Skip download step? [Y/n]: ").strip().lower()
 
 
 
 
177
  if skip != "n":
178
  print(" Skipping download...")
179
- step2_get_llamacpp()
180
- gguf_path = step3_convert_to_gguf()
181
- step4_create_ollama_model(gguf_path)
182
  return
183
 
184
  step1_download_from_modal()
185
- step2_get_llamacpp()
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: {gguf_path}")
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 + save ─────────────
237
- print(f"\n[7/7] Merging LoRA weights and saving to {MODEL_OUTPUT_DIR}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
- # Merge LoRA into base model for full weights (needed for GGUF conversion)
240
- merged_model = model.merge_and_unload()
 
 
 
 
 
 
 
241
 
242
- output_path = f"{MODEL_OUTPUT_DIR}/mistral-vidyabot-merged"
243
- merged_model.save_pretrained(output_path, safe_serialization=True)
244
- tokenizer.save_pretrained(output_path)
 
 
 
 
 
245
 
246
- # Commit to Modal Volume
 
 
 
 
 
 
 
 
 
247
  volume.commit()
248
 
249
- print(f"\n βœ… Merged model saved to: {output_path}")
250
- print(" πŸ“¦ Model committed to Modal Volume 'vidyabot-model-output'")
251
  print("\n" + "=" * 60)
252
- print(" FINE-TUNING COMPLETE")
253
- print(" Next: run `modal volume get vidyabot-model-output mistral-vidyabot-merged ./backend/llm/models/mistral-vidyabot-hf`")
254
  print("=" * 60)
255
 
256
- return output_path
257
 
258
 
259
  # ──────────────────────────────────────────────────────────────
@@ -290,11 +359,8 @@ def main():
290
  print()
291
 
292
  result = finetune_mistral.remote()
293
- print(f"\nβœ… Job complete! Weights saved to Modal Volume.")
294
  print(f" Path: {result}")
295
  print()
296
- print("πŸ“₯ To download the merged model, run:")
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")