Dimitris Codex commited on
Commit
75d3367
·
1 Parent(s): 026a329

feat(train): Modal MiniCPM-V LoRA fine-tune + GGUF conversion scripts

Browse files

Co-authored-by: Codex <chatgpt-codex-connector[bot]@users.noreply.github.com>

scripts/convert_to_gguf.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Convert the merged, fine-tuned MiniCPM-V into a quantized GGUF + vision projector (mmproj)
3
+ # for llama.cpp, then quantize to Q4_K_M. Produces the two files the offline backend loads:
4
+ # models/minicpmv-lab.Q4_K_M.gguf (LOCAL_MODEL_PATH)
5
+ # models/minicpmv-lab.mmproj.gguf (LOCAL_MMPROJ_PATH)
6
+ #
7
+ # Prereqs: a merged HF model (scripts/merge_lora.py) and a local llama.cpp checkout.
8
+ #
9
+ # ⚠️ MiniCPM-V GGUF conversion lives under llama.cpp's multimodal tooling and the exact script
10
+ # names/paths move between releases (older: examples/llava/*, newer: tools/mtmd/*). Check your
11
+ # llama.cpp version and adjust the three SCRIPT paths below. The flow is stable; the paths drift.
12
+ set -euo pipefail
13
+
14
+ MERGED="${1:-./merged-minicpmv-lab}" # merged HF model dir
15
+ LLAMA="${LLAMA_CPP:-./llama.cpp}" # path to a llama.cpp checkout
16
+ OUT="${OUT_DIR:-./models}"
17
+ VER="${MINICPMV_VERSION:-3}" # MiniCPM-V arch version flag; confirm for 4.6
18
+ mkdir -p "$OUT"
19
+
20
+ echo "==> 1/4 Split vision encoder + LLM (surgery)"
21
+ python "$LLAMA/examples/llava/minicpmv-surgery.py" -m "$MERGED"
22
+
23
+ echo "==> 2/4 Build the vision projector (mmproj) GGUF"
24
+ python "$LLAMA/examples/llava/minicpmv-convert-image-encoder-to-gguf.py" \
25
+ -m "$MERGED" \
26
+ --minicpmv-projector "$MERGED/minicpmv.projector" \
27
+ --output-dir "$OUT" \
28
+ --minicpmv_version "$VER"
29
+ mv "$OUT"/*mmproj*.gguf "$OUT/minicpmv-lab.mmproj.gguf" 2>/dev/null || true
30
+
31
+ echo "==> 3/4 Convert the LLM to GGUF (f16)"
32
+ python "$LLAMA/convert_hf_to_gguf.py" "$MERGED/model" --outfile "$OUT/minicpmv-lab.f16.gguf"
33
+
34
+ echo "==> 4/4 Quantize to Q4_K_M"
35
+ "$LLAMA/llama-quantize" "$OUT/minicpmv-lab.f16.gguf" "$OUT/minicpmv-lab.Q4_K_M.gguf" Q4_K_M
36
+
37
+ echo
38
+ echo "Done. Set in the Space:"
39
+ echo " LOCAL_MODEL_PATH=$OUT/minicpmv-lab.Q4_K_M.gguf"
40
+ echo " LOCAL_MMPROJ_PATH=$OUT/minicpmv-lab.mmproj.gguf"
41
+ echo " EXTRACTOR_BACKEND=local"
42
+ echo "Track both .gguf files with git-lfs and commit them into the Space repo."
scripts/merge_lora.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Merge the LoRA adapters into the MiniCPM-V base → a standalone HF model for GGUF conversion.
3
+
4
+ python scripts/merge_lora.py \
5
+ --base openbmb/MiniCPM-V-4_6 \
6
+ --adapters ./adapters/minicpmv-lab-lora \
7
+ --out ./merged-minicpmv-lab
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+
14
+
15
+ def main() -> int:
16
+ ap = argparse.ArgumentParser()
17
+ ap.add_argument("--base", required=True, help="base model HF id or path")
18
+ ap.add_argument("--adapters", required=True, help="LoRA adapter dir (from Modal volume)")
19
+ ap.add_argument("--out", required=True, help="output dir for the merged model")
20
+ args = ap.parse_args()
21
+
22
+ import torch
23
+ from peft import PeftModel
24
+ from transformers import AutoModel, AutoProcessor, AutoTokenizer
25
+
26
+ print(f"Loading base {args.base} ...")
27
+ model = AutoModel.from_pretrained(
28
+ args.base, trust_remote_code=True, torch_dtype=torch.float16
29
+ )
30
+ print(f"Applying adapters {args.adapters} ...")
31
+ model = PeftModel.from_pretrained(model, args.adapters)
32
+ model = model.merge_and_unload()
33
+
34
+ model.save_pretrained(args.out, safe_serialization=True)
35
+ AutoTokenizer.from_pretrained(args.base, trust_remote_code=True).save_pretrained(args.out)
36
+ try:
37
+ AutoProcessor.from_pretrained(args.base, trust_remote_code=True).save_pretrained(args.out)
38
+ except Exception:
39
+ pass # some MiniCPM-V revisions bundle the processor in the tokenizer
40
+
41
+ print(f"Merged model written to {args.out}")
42
+ return 0
43
+
44
+
45
+ if __name__ == "__main__":
46
+ raise SystemExit(main())
train/modal_finetune.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """LoRA fine-tune of MiniCPM-V for lab extraction, on Modal.
3
+
4
+ Strategy: generate the synthetic dataset **on the GPU box** (it's pure Python + PIL, fully
5
+ reproducible from a seed), convert to the vision-SFT format, then LoRA fine-tune MiniCPM-V with
6
+ ms-swift. No image upload, no dataset drift. Adapters are saved to a Modal Volume; pull them
7
+ down and convert to GGUF (see scripts/convert_to_gguf.sh).
8
+
9
+ modal run train/modal_finetune.py --n 4000
10
+
11
+ Running the fine-tune on Modal also satisfies the Modal prize.
12
+
13
+ ⚠️ VERIFY-ON-FIRST-RUN: the ms-swift `--model_type` for the exact MiniCPM-V 4.6 checkpoint and
14
+ its current dataset-format flags. ms-swift evolves; confirm against `swift sft --help` and the
15
+ MiniCPM-V model card, then pin the value in MODEL_TYPE / MODEL_ID below. The data generation,
16
+ conversion, and plumbing are correct; the trainer invocation is the one thing to confirm.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import modal
22
+
23
+ # TODO(verify): confirm these against the MiniCPM-V 4.6 model card + `swift sft --help`.
24
+ MODEL_ID = "openbmb/MiniCPM-V-4_6" # HF id of the base vision model
25
+ MODEL_TYPE = "minicpm-v-v2_6-chat" # ms-swift model_type; confirm the 4.6 value
26
+
27
+ app = modal.App("blood-test-finetune")
28
+
29
+ image = (
30
+ modal.Image.debian_slim(python_version="3.11")
31
+ .apt_install("git")
32
+ .pip_install(
33
+ "torch",
34
+ "transformers>=4.44",
35
+ "peft>=0.12",
36
+ "accelerate>=0.33",
37
+ "pillow>=10",
38
+ "ms-swift>=2.5",
39
+ "timm",
40
+ "sentencepiece",
41
+ )
42
+ # Mount our generator + converter + marker reference so the box builds its own data.
43
+ .add_local_dir("src", "/root/app/src")
44
+ .add_local_dir("train", "/root/app/train")
45
+ )
46
+
47
+ adapters = modal.Volume.from_name("blood-test-adapters", create_if_missing=True)
48
+ hf_cache = modal.Volume.from_name("blood-test-hf-cache", create_if_missing=True)
49
+
50
+
51
+ @app.function(
52
+ image=image,
53
+ gpu="A100",
54
+ timeout=3 * 60 * 60,
55
+ volumes={"/adapters": adapters, "/root/.cache/huggingface": hf_cache},
56
+ )
57
+ def train(n: int = 4000, epochs: int = 2, seed: int = 13) -> str:
58
+ import subprocess
59
+ import sys
60
+ from pathlib import Path
61
+
62
+ sys.path.insert(0, "/root/app")
63
+ from train.synth_reports import generate
64
+ from train.to_sft_dataset import convert
65
+
66
+ # 1) build the dataset on the box
67
+ data_dir = Path("/root/app/train/data/synth")
68
+ labels = generate(n, data_dir, seed=seed)
69
+ sft_path = Path("/root/app/train/data/sft.jsonl")
70
+ n_examples = convert(labels, sft_path)
71
+ print(f"Generated {n_examples} SFT examples at {sft_path}")
72
+
73
+ # 2) LoRA fine-tune with ms-swift
74
+ out_dir = "/adapters/minicpmv-lab-lora"
75
+ cmd = [
76
+ "swift", "sft",
77
+ "--model_type", MODEL_TYPE,
78
+ "--model_id_or_path", MODEL_ID,
79
+ "--sft_type", "lora",
80
+ "--dataset", str(sft_path),
81
+ "--num_train_epochs", str(epochs),
82
+ "--lora_rank", "16",
83
+ "--lora_alpha", "32",
84
+ "--learning_rate", "1e-4",
85
+ "--batch_size", "2",
86
+ "--gradient_accumulation_steps", "8",
87
+ "--max_length", "2048",
88
+ "--output_dir", out_dir,
89
+ "--save_total_limit", "1",
90
+ ]
91
+ print("Running:", " ".join(cmd))
92
+ subprocess.run(cmd, check=True)
93
+
94
+ adapters.commit()
95
+ return out_dir
96
+
97
+
98
+ @app.local_entrypoint()
99
+ def main(n: int = 4000, epochs: int = 2) -> None:
100
+ path = train.remote(n=n, epochs=epochs)
101
+ print(f"\nLoRA adapters saved to Modal volume 'blood-test-adapters' at {path}")
102
+ print("Next: download adapters, merge into the base model, convert to GGUF + mmproj,")
103
+ print("quantize Q4_K_M, and bundle into the Space (see scripts/convert_to_gguf.sh).")