Mehdi commited on
Commit Β·
8087133
1
Parent(s): 7a3f6e0
feat: Well-Tuned + Llama Champion groundwork
Browse files- finetune/train_modal.py: QLoRA fine-tune of MiniCPM4-8B on SQuAD pairs
formatted with the production prompt, merged and pushed to the Hub
- finetune/convert_gguf_modal.py: GGUF f16 + Q4_K_M conversion of the
fine-tuned model, pushed to a -GGUF repo
- model/llm.py: llama.cpp runtime backend (LlamaCppLLM) selected via
PAPERPROF_RUNTIME=llamacpp, transformers stays the default
- finetune/convert_gguf_modal.py +121 -0
- finetune/train_modal.py +224 -0
- model/llm.py +40 -4
finetune/convert_gguf_modal.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
finetune/convert_gguf_modal.py β Convert the fine-tuned model to GGUF for llama.cpp.
|
| 3 |
+
|
| 4 |
+
Pulls build-small-hackathon/MiniCPM4-8B-PaperProf, converts to GGUF f16 with
|
| 5 |
+
llama.cpp's convert script, quantizes to Q4_K_M, and pushes both files to
|
| 6 |
+
build-small-hackathon/MiniCPM4-8B-PaperProf-GGUF.
|
| 7 |
+
|
| 8 |
+
Run (after the fine-tune has been pushed):
|
| 9 |
+
modal run finetune/convert_gguf_modal.py
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import modal
|
| 13 |
+
|
| 14 |
+
SRC_REPO = "build-small-hackathon/MiniCPM4-8B-PaperProf"
|
| 15 |
+
GGUF_REPO = "build-small-hackathon/MiniCPM4-8B-PaperProf-GGUF"
|
| 16 |
+
|
| 17 |
+
app = modal.App("paperprof-gguf")
|
| 18 |
+
|
| 19 |
+
image = (
|
| 20 |
+
modal.Image.debian_slim(python_version="3.12")
|
| 21 |
+
.apt_install("git", "cmake", "build-essential", "curl")
|
| 22 |
+
.run_commands(
|
| 23 |
+
"git clone --depth 1 https://github.com/ggml-org/llama.cpp /llama.cpp",
|
| 24 |
+
"cmake -S /llama.cpp -B /llama.cpp/build -DGGML_NATIVE=OFF",
|
| 25 |
+
"cmake --build /llama.cpp/build --target llama-quantize -j",
|
| 26 |
+
)
|
| 27 |
+
.pip_install(
|
| 28 |
+
"torch==2.6.0",
|
| 29 |
+
"transformers==4.57.1",
|
| 30 |
+
"sentencepiece",
|
| 31 |
+
"huggingface_hub",
|
| 32 |
+
)
|
| 33 |
+
.run_commands("pip install -r /llama.cpp/requirements/requirements-convert_hf_to_gguf.txt")
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
MODEL_CARD = f"""---
|
| 37 |
+
license: apache-2.0
|
| 38 |
+
base_model: {SRC_REPO}
|
| 39 |
+
tags:
|
| 40 |
+
- gguf
|
| 41 |
+
- llama.cpp
|
| 42 |
+
- question-generation
|
| 43 |
+
- education
|
| 44 |
+
- paperprof
|
| 45 |
+
language:
|
| 46 |
+
- en
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
# MiniCPM4-8B-PaperProf-GGUF
|
| 50 |
+
|
| 51 |
+
GGUF quantizations of [{SRC_REPO}](https://huggingface.co/{SRC_REPO}),
|
| 52 |
+
the fine-tuned exam-question generator behind
|
| 53 |
+
[PaperProf](https://huggingface.co/spaces/build-small-hackathon/PaperProf).
|
| 54 |
+
|
| 55 |
+
| File | Quant | Size | Use |
|
| 56 |
+
|---|---|---|---|
|
| 57 |
+
| `minicpm4-8b-paperprof-Q4_K_M.gguf` | Q4_K_M | ~4.9 GB | recommended, used by the Space |
|
| 58 |
+
| `minicpm4-8b-paperprof-f16.gguf` | F16 | ~16 GB | full precision reference |
|
| 59 |
+
|
| 60 |
+
## Usage with llama.cpp
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
llama-cli -hf {GGUF_REPO} -p "your prompt"
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Usage with llama-cpp-python
|
| 67 |
+
|
| 68 |
+
```python
|
| 69 |
+
from llama_cpp import Llama
|
| 70 |
+
llm = Llama.from_pretrained("{GGUF_REPO}", filename="*Q4_K_M.gguf", n_gpu_layers=-1)
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
Built for the Build Small Hackathon, June 2026, by Team PaperProf (EPITA).
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@app.function(
|
| 78 |
+
image=image,
|
| 79 |
+
timeout=2 * 60 * 60,
|
| 80 |
+
cpu=8,
|
| 81 |
+
memory=65536,
|
| 82 |
+
ephemeral_disk=120 * 1024,
|
| 83 |
+
secrets=[modal.Secret.from_name("paperprof-hf")],
|
| 84 |
+
)
|
| 85 |
+
def convert():
|
| 86 |
+
import os
|
| 87 |
+
import subprocess
|
| 88 |
+
from huggingface_hub import snapshot_download, HfApi
|
| 89 |
+
|
| 90 |
+
token = os.environ["HF_TOKEN"]
|
| 91 |
+
api = HfApi(token=token)
|
| 92 |
+
|
| 93 |
+
print(f"[pull] downloading {SRC_REPO}β¦")
|
| 94 |
+
src = snapshot_download(SRC_REPO, token=token, local_dir="/tmp/src")
|
| 95 |
+
|
| 96 |
+
f16 = "/tmp/minicpm4-8b-paperprof-f16.gguf"
|
| 97 |
+
q4 = "/tmp/minicpm4-8b-paperprof-Q4_K_M.gguf"
|
| 98 |
+
|
| 99 |
+
print("[convert] HF β GGUF f16β¦")
|
| 100 |
+
subprocess.run(
|
| 101 |
+
["python", "/llama.cpp/convert_hf_to_gguf.py", src, "--outfile", f16, "--outtype", "f16"],
|
| 102 |
+
check=True,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
print("[quantize] f16 β Q4_K_Mβ¦")
|
| 106 |
+
subprocess.run(
|
| 107 |
+
["/llama.cpp/build/bin/llama-quantize", f16, q4, "Q4_K_M"],
|
| 108 |
+
check=True,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
print(f"[push] uploading to {GGUF_REPO}β¦")
|
| 112 |
+
api.create_repo(GGUF_REPO, exist_ok=True)
|
| 113 |
+
api.upload_file(path_or_fileobj=q4, path_in_repo=os.path.basename(q4), repo_id=GGUF_REPO)
|
| 114 |
+
api.upload_file(path_or_fileobj=f16, path_in_repo=os.path.basename(f16), repo_id=GGUF_REPO)
|
| 115 |
+
api.upload_file(path_or_fileobj=MODEL_CARD.encode(), path_in_repo="README.md", repo_id=GGUF_REPO)
|
| 116 |
+
print(f"[done] https://huggingface.co/{GGUF_REPO}")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@app.local_entrypoint()
|
| 120 |
+
def main():
|
| 121 |
+
convert.remote()
|
finetune/train_modal.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
finetune/train_modal.py β QLoRA fine-tune of MiniCPM4-8B for PaperProf.
|
| 3 |
+
|
| 4 |
+
Task: exam-question generation. The training data is SQuAD passages and
|
| 5 |
+
questions reformatted into the EXACT production prompt used by
|
| 6 |
+
core/questioner.py, so the model learns PaperProf's task, not generic QA.
|
| 7 |
+
|
| 8 |
+
Run:
|
| 9 |
+
modal run finetune/train_modal.py
|
| 10 |
+
|
| 11 |
+
Output:
|
| 12 |
+
Merged bf16 model pushed to hf.co/build-small-hackathon/MiniCPM4-8B-PaperProf
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import modal
|
| 16 |
+
|
| 17 |
+
APP_NAME = "paperprof-finetune"
|
| 18 |
+
BASE_MODEL = "openbmb/MiniCPM4-8B"
|
| 19 |
+
HUB_REPO = "build-small-hackathon/MiniCPM4-8B-PaperProf"
|
| 20 |
+
N_SAMPLES = 3000
|
| 21 |
+
MAX_LEN = 1024
|
| 22 |
+
|
| 23 |
+
app = modal.App(APP_NAME)
|
| 24 |
+
|
| 25 |
+
image = (
|
| 26 |
+
modal.Image.debian_slim(python_version="3.12")
|
| 27 |
+
.pip_install(
|
| 28 |
+
"torch==2.6.0",
|
| 29 |
+
"transformers==4.57.1",
|
| 30 |
+
"datasets==3.2.0",
|
| 31 |
+
"peft==0.14.0",
|
| 32 |
+
"bitsandbytes==0.46.0",
|
| 33 |
+
"accelerate>=1.8.0",
|
| 34 |
+
"huggingface_hub",
|
| 35 |
+
"sentencepiece",
|
| 36 |
+
)
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Mirror of core/questioner.py _PROMPT_TEMPLATE (Normal difficulty)
|
| 40 |
+
PROMPT_TEMPLATE = """\
|
| 41 |
+
You are a university professor creating exam questions.
|
| 42 |
+
Given the following excerpt from a course, write ONE focused question.
|
| 43 |
+
|
| 44 |
+
Difficulty β Ask for conceptual understanding (Explain X. Why does X happen?).
|
| 45 |
+
|
| 46 |
+
Rules:
|
| 47 |
+
- ONE question only, on ONE concept
|
| 48 |
+
- Maximum 25 words
|
| 49 |
+
- No sub-questions, no "and", no compound questions
|
| 50 |
+
- IMPORTANT: Always write the question in English, even if the source text is in another language
|
| 51 |
+
- Output only the question, nothing else
|
| 52 |
+
|
| 53 |
+
Excerpt:
|
| 54 |
+
{chunk}
|
| 55 |
+
|
| 56 |
+
Question:"""
|
| 57 |
+
|
| 58 |
+
MODEL_CARD = f"""---
|
| 59 |
+
license: apache-2.0
|
| 60 |
+
base_model: {BASE_MODEL}
|
| 61 |
+
tags:
|
| 62 |
+
- question-generation
|
| 63 |
+
- education
|
| 64 |
+
- lora
|
| 65 |
+
- paperprof
|
| 66 |
+
datasets:
|
| 67 |
+
- squad
|
| 68 |
+
language:
|
| 69 |
+
- en
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
# MiniCPM4-8B-PaperProf
|
| 73 |
+
|
| 74 |
+
Fine-tuned from [{BASE_MODEL}](https://huggingface.co/{BASE_MODEL}) for
|
| 75 |
+
**exam-question generation** in [PaperProf](https://huggingface.co/spaces/build-small-hackathon/PaperProf),
|
| 76 |
+
an AI study buddy that turns course PDFs into interactive quiz sessions.
|
| 77 |
+
|
| 78 |
+
## Training
|
| 79 |
+
|
| 80 |
+
- **Method:** QLoRA (4-bit NF4, r=16, alpha=32, all-linear targets), merged to bf16
|
| 81 |
+
- **Data:** {N_SAMPLES} SQuAD passage/question pairs reformatted into PaperProf's
|
| 82 |
+
production prompt template, so the model is optimized for the exact task it
|
| 83 |
+
serves: one focused, concise exam question per course excerpt.
|
| 84 |
+
- **Epochs:** 1, lr 2e-4 cosine, bf16 compute
|
| 85 |
+
|
| 86 |
+
## Usage
|
| 87 |
+
|
| 88 |
+
Drop-in replacement for the base model:
|
| 89 |
+
|
| 90 |
+
```python
|
| 91 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 92 |
+
tok = AutoTokenizer.from_pretrained("{HUB_REPO}", trust_remote_code=True)
|
| 93 |
+
model = AutoModelForCausalLM.from_pretrained("{HUB_REPO}", trust_remote_code=True, torch_dtype="bfloat16")
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
Built for the Build Small Hackathon, June 2026, by Team PaperProf (EPITA).
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@app.function(
|
| 101 |
+
image=image,
|
| 102 |
+
gpu="A100-80GB",
|
| 103 |
+
timeout=3 * 60 * 60,
|
| 104 |
+
secrets=[modal.Secret.from_name("paperprof-hf")],
|
| 105 |
+
)
|
| 106 |
+
def train():
|
| 107 |
+
import os
|
| 108 |
+
import torch
|
| 109 |
+
from datasets import load_dataset
|
| 110 |
+
from transformers import (
|
| 111 |
+
AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig,
|
| 112 |
+
Trainer, TrainingArguments, DataCollatorForLanguageModeling,
|
| 113 |
+
)
|
| 114 |
+
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
| 115 |
+
from huggingface_hub import HfApi
|
| 116 |
+
|
| 117 |
+
token = os.environ["HF_TOKEN"]
|
| 118 |
+
|
| 119 |
+
# ββ Data: SQuAD β production prompt format ββββββββββββββββββββββββββββ
|
| 120 |
+
print("[data] loading SQuADβ¦")
|
| 121 |
+
ds = load_dataset("squad", split="train")
|
| 122 |
+
seen, rows = set(), []
|
| 123 |
+
for ex in ds:
|
| 124 |
+
ctx = ex["context"].strip()
|
| 125 |
+
if not (300 <= len(ctx) <= 1500) or ctx in seen:
|
| 126 |
+
continue
|
| 127 |
+
seen.add(ctx)
|
| 128 |
+
q = ex["question"].strip()
|
| 129 |
+
if not q.endswith("?") or len(q.split()) > 25:
|
| 130 |
+
continue
|
| 131 |
+
rows.append({"chunk": ctx, "question": q})
|
| 132 |
+
if len(rows) >= N_SAMPLES:
|
| 133 |
+
break
|
| 134 |
+
print(f"[data] {len(rows)} training pairs")
|
| 135 |
+
|
| 136 |
+
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True, token=token)
|
| 137 |
+
if tokenizer.pad_token is None:
|
| 138 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 139 |
+
|
| 140 |
+
def to_text(row):
|
| 141 |
+
messages = [
|
| 142 |
+
{"role": "user", "content": PROMPT_TEMPLATE.format(chunk=row["chunk"])},
|
| 143 |
+
{"role": "assistant", "content": row["question"]},
|
| 144 |
+
]
|
| 145 |
+
return tokenizer.apply_chat_template(messages, tokenize=False)
|
| 146 |
+
|
| 147 |
+
texts = [to_text(r) for r in rows]
|
| 148 |
+
|
| 149 |
+
def tokenize(batch):
|
| 150 |
+
return tokenizer(batch["text"], truncation=True, max_length=MAX_LEN, padding=False)
|
| 151 |
+
|
| 152 |
+
from datasets import Dataset
|
| 153 |
+
train_ds = Dataset.from_dict({"text": texts}).map(
|
| 154 |
+
tokenize, batched=True, remove_columns=["text"]
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# ββ Model: 4-bit base + LoRA ββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
+
print("[model] loading base in 4-bitβ¦")
|
| 159 |
+
bnb = BitsAndBytesConfig(
|
| 160 |
+
load_in_4bit=True,
|
| 161 |
+
bnb_4bit_quant_type="nf4",
|
| 162 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 163 |
+
bnb_4bit_use_double_quant=True,
|
| 164 |
+
)
|
| 165 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 166 |
+
BASE_MODEL, quantization_config=bnb, device_map="auto",
|
| 167 |
+
trust_remote_code=True, token=token,
|
| 168 |
+
)
|
| 169 |
+
model = prepare_model_for_kbit_training(model)
|
| 170 |
+
lora = LoraConfig(
|
| 171 |
+
r=16, lora_alpha=32, lora_dropout=0.05,
|
| 172 |
+
target_modules="all-linear", task_type="CAUSAL_LM",
|
| 173 |
+
)
|
| 174 |
+
model = get_peft_model(model, lora)
|
| 175 |
+
model.print_trainable_parameters()
|
| 176 |
+
|
| 177 |
+
# ββ Train βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 178 |
+
args = TrainingArguments(
|
| 179 |
+
output_dir="/tmp/out",
|
| 180 |
+
num_train_epochs=1,
|
| 181 |
+
per_device_train_batch_size=4,
|
| 182 |
+
gradient_accumulation_steps=4,
|
| 183 |
+
learning_rate=2e-4,
|
| 184 |
+
lr_scheduler_type="cosine",
|
| 185 |
+
warmup_ratio=0.03,
|
| 186 |
+
bf16=True,
|
| 187 |
+
logging_steps=10,
|
| 188 |
+
save_strategy="no",
|
| 189 |
+
report_to=[],
|
| 190 |
+
gradient_checkpointing=True,
|
| 191 |
+
)
|
| 192 |
+
collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
|
| 193 |
+
trainer = Trainer(model=model, args=args, train_dataset=train_ds, data_collator=collator)
|
| 194 |
+
print("[train] startingβ¦")
|
| 195 |
+
trainer.train()
|
| 196 |
+
|
| 197 |
+
# ββ Merge LoRA into bf16 base and push βββββββββββββββββββββββββββββββ
|
| 198 |
+
print("[merge] reloading base in bf16 and merging adapterβ¦")
|
| 199 |
+
model.save_pretrained("/tmp/adapter")
|
| 200 |
+
del model, trainer
|
| 201 |
+
torch.cuda.empty_cache()
|
| 202 |
+
|
| 203 |
+
from peft import PeftModel
|
| 204 |
+
base = AutoModelForCausalLM.from_pretrained(
|
| 205 |
+
BASE_MODEL, torch_dtype=torch.bfloat16, device_map="auto",
|
| 206 |
+
trust_remote_code=True, token=token,
|
| 207 |
+
)
|
| 208 |
+
merged = PeftModel.from_pretrained(base, "/tmp/adapter")
|
| 209 |
+
merged = merged.merge_and_unload()
|
| 210 |
+
|
| 211 |
+
print(f"[push] uploading to {HUB_REPO}β¦")
|
| 212 |
+
merged.push_to_hub(HUB_REPO, token=token, private=False)
|
| 213 |
+
tokenizer.push_to_hub(HUB_REPO, token=token)
|
| 214 |
+
HfApi(token=token).upload_file(
|
| 215 |
+
path_or_fileobj=MODEL_CARD.encode(),
|
| 216 |
+
path_in_repo="README.md",
|
| 217 |
+
repo_id=HUB_REPO,
|
| 218 |
+
)
|
| 219 |
+
print(f"[done] https://huggingface.co/{HUB_REPO}")
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
@app.local_entrypoint()
|
| 223 |
+
def main():
|
| 224 |
+
train.remote()
|
model/llm.py
CHANGED
|
@@ -12,9 +12,13 @@ Model choice:
|
|
| 12 |
Requires transformers >= 4.50.0.
|
| 13 |
|
| 14 |
Environment variables:
|
| 15 |
-
PAPERPROF_MODEL
|
| 16 |
-
|
| 17 |
-
PAPERPROF_DEVICE
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
Public API:
|
| 20 |
get_llm() -> LLM β return the singleton instance
|
|
@@ -101,9 +105,41 @@ class LLM:
|
|
| 101 |
return output[0]["generated_text"]
|
| 102 |
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
@lru_cache(maxsize=1)
|
| 105 |
-
def get_llm()
|
| 106 |
"""Return the singleton LLM, loading the model on first call."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
model_id = os.environ.get("PAPERPROF_MODEL", DEFAULT_MODEL_ID)
|
| 108 |
device = os.environ.get("PAPERPROF_DEVICE", "auto")
|
| 109 |
return LLM(model_id=model_id, device=device)
|
|
|
|
| 12 |
Requires transformers >= 4.50.0.
|
| 13 |
|
| 14 |
Environment variables:
|
| 15 |
+
PAPERPROF_MODEL Override the default model ID (e.g. "openbmb/MiniCPM3-4B"
|
| 16 |
+
for a smaller fallback during local testing).
|
| 17 |
+
PAPERPROF_DEVICE "cuda", "mps", or "cpu" (default: auto-detected).
|
| 18 |
+
PAPERPROF_RUNTIME "transformers" (default) or "llamacpp" to run the GGUF
|
| 19 |
+
model through the llama.cpp runtime instead.
|
| 20 |
+
PAPERPROF_GGUF_REPO GGUF repo for the llamacpp runtime
|
| 21 |
+
(default: build-small-hackathon/MiniCPM4-8B-PaperProf-GGUF).
|
| 22 |
|
| 23 |
Public API:
|
| 24 |
get_llm() -> LLM β return the singleton instance
|
|
|
|
| 105 |
return output[0]["generated_text"]
|
| 106 |
|
| 107 |
|
| 108 |
+
DEFAULT_GGUF_REPO = "build-small-hackathon/MiniCPM4-8B-PaperProf-GGUF"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class LlamaCppLLM:
|
| 112 |
+
"""Same .generate() interface as LLM, backed by the llama.cpp runtime."""
|
| 113 |
+
|
| 114 |
+
def __init__(self, repo_id: str):
|
| 115 |
+
from llama_cpp import Llama
|
| 116 |
+
|
| 117 |
+
n_gpu_layers = -1 if torch.cuda.is_available() else 0
|
| 118 |
+
print(f"[LlamaCppLLM] loading {repo_id} (n_gpu_layers={n_gpu_layers})")
|
| 119 |
+
self._llm = Llama.from_pretrained(
|
| 120 |
+
repo_id=repo_id,
|
| 121 |
+
filename="*Q4_K_M.gguf",
|
| 122 |
+
n_gpu_layers=n_gpu_layers,
|
| 123 |
+
n_ctx=4096,
|
| 124 |
+
verbose=False,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
def generate(self, prompt: str, max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS) -> str:
|
| 128 |
+
out = self._llm.create_chat_completion(
|
| 129 |
+
messages=[{"role": "user", "content": prompt}],
|
| 130 |
+
max_tokens=max_new_tokens,
|
| 131 |
+
temperature=0.0,
|
| 132 |
+
)
|
| 133 |
+
return out["choices"][0]["message"]["content"]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
@lru_cache(maxsize=1)
|
| 137 |
+
def get_llm():
|
| 138 |
"""Return the singleton LLM, loading the model on first call."""
|
| 139 |
+
runtime = os.environ.get("PAPERPROF_RUNTIME", "transformers").lower()
|
| 140 |
+
if runtime == "llamacpp":
|
| 141 |
+
repo_id = os.environ.get("PAPERPROF_GGUF_REPO", DEFAULT_GGUF_REPO)
|
| 142 |
+
return LlamaCppLLM(repo_id=repo_id)
|
| 143 |
model_id = os.environ.get("PAPERPROF_MODEL", DEFAULT_MODEL_ID)
|
| 144 |
device = os.environ.get("PAPERPROF_DEVICE", "auto")
|
| 145 |
return LLM(model_id=model_id, device=device)
|