Dimitris Codex commited on
Commit ·
75d3367
1
Parent(s): 026a329
feat(train): Modal MiniCPM-V LoRA fine-tune + GGUF conversion scripts
Browse filesCo-authored-by: Codex <chatgpt-codex-connector[bot]@users.noreply.github.com>
- scripts/convert_to_gguf.sh +42 -0
- scripts/merge_lora.py +46 -0
- train/modal_finetune.py +103 -0
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).")
|