Justin-lee commited on
Commit
b65d866
·
verified ·
1 Parent(s): 1211240

Add model export script for Ollama/LM Studio

Browse files
Files changed (1) hide show
  1. export_model.py +350 -0
export_model.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ CodePilot Model Export — 把訓練好的模型匯出給 Ollama / LM Studio
5
+ ================================================================
6
+
7
+ Usage:
8
+ # Step 1+2+3 一鍵完成
9
+ python export_model.py --adapter ~/.codepilot/adapter_20260423 --output ./my-model
10
+
11
+ # 只合併(產生完整模型)
12
+ python export_model.py --adapter ~/.codepilot/adapter_20260423 --output ./merged --merge-only
13
+
14
+ # 只轉 GGUF(已有合併模型)
15
+ python export_model.py --merged-model ./merged --output ./my-model --quantize q4_k_m
16
+
17
+ # 自動註冊到 Ollama
18
+ python export_model.py --adapter ~/.codepilot/adapter_20260423 --output ./my-model --ollama
19
+
20
+ # 上傳到 HuggingFace Hub
21
+ python export_model.py --adapter ~/.codepilot/adapter_20260423 --output ./my-model --push-to-hub USERNAME/my-model
22
+ """
23
+
24
+ import argparse, os, sys, subprocess, shutil
25
+ from pathlib import Path
26
+
27
+ DEFAULT_BASE_MODEL = "Qwen/Qwen2.5-Coder-3B-Instruct"
28
+
29
+
30
+ def step1_merge_adapter(base_model, adapter_path, output_dir):
31
+ """Step 1: 合併 LoRA adapter 到基礎模型"""
32
+ print(f"\n{'='*60}")
33
+ print(f" Step 1: 合併 LoRA Adapter")
34
+ print(f" Base: {base_model}")
35
+ print(f" Adapter: {adapter_path}")
36
+ print(f" Output: {output_dir}")
37
+ print(f"{'='*60}\n")
38
+
39
+ import torch
40
+ from transformers import AutoModelForCausalLM, AutoTokenizer
41
+ from peft import PeftModel
42
+
43
+ print("📥 載入基礎模型...")
44
+ tokenizer = AutoTokenizer.from_pretrained(base_model)
45
+ model = AutoModelForCausalLM.from_pretrained(
46
+ base_model, torch_dtype=torch.float16,
47
+ device_map="cpu", # 合併用 CPU,省 GPU 記憶體
48
+ trust_remote_code=True,
49
+ )
50
+
51
+ print("📥 載入 LoRA adapter...")
52
+ model = PeftModel.from_pretrained(model, adapter_path)
53
+
54
+ print("🔄 合併權重...")
55
+ model = model.merge_and_unload()
56
+
57
+ print(f"💾 保存到 {output_dir}...")
58
+ os.makedirs(output_dir, exist_ok=True)
59
+ model.save_pretrained(output_dir, safe_serialization=True)
60
+ tokenizer.save_pretrained(output_dir)
61
+
62
+ print(f"✅ 合併完成: {output_dir}")
63
+ # 顯示大小
64
+ total_size = sum(f.stat().st_size for f in Path(output_dir).rglob("*") if f.is_file())
65
+ print(f" 大小: {total_size / 1024**3:.1f} GB")
66
+
67
+ return output_dir
68
+
69
+
70
+ def step2_convert_gguf(merged_dir, output_dir, quantize="q4_k_m"):
71
+ """Step 2: 轉換成 GGUF 格式"""
72
+ print(f"\n{'='*60}")
73
+ print(f" Step 2: 轉換 GGUF")
74
+ print(f" Input: {merged_dir}")
75
+ print(f" Output: {output_dir}")
76
+ print(f" Quantize: {quantize}")
77
+ print(f"{'='*60}\n")
78
+
79
+ # 檢查 llama.cpp 是否已安裝
80
+ convert_script = shutil.which("convert_hf_to_gguf.py")
81
+ quantize_bin = shutil.which("llama-quantize")
82
+
83
+ if not convert_script:
84
+ # 嘗試找 llama.cpp 目錄
85
+ llama_cpp_paths = [
86
+ os.path.expanduser("~/llama.cpp"),
87
+ "/opt/llama.cpp",
88
+ os.path.expanduser("~/.local/share/llama.cpp"),
89
+ ]
90
+ for p in llama_cpp_paths:
91
+ if os.path.exists(os.path.join(p, "convert_hf_to_gguf.py")):
92
+ convert_script = os.path.join(p, "convert_hf_to_gguf.py")
93
+ quantize_bin = os.path.join(p, "build", "bin", "llama-quantize")
94
+ break
95
+
96
+ if not convert_script:
97
+ print("⚠️ llama.cpp 未安裝。安裝方式:")
98
+ print()
99
+ print(" # 方式 1: pip(最簡單)")
100
+ print(" pip install llama-cpp-python")
101
+ print()
102
+ print(" # 方式 2: 從原始碼編譯(完整功能)")
103
+ print(" git clone https://github.com/ggml-org/llama.cpp")
104
+ print(" cd llama.cpp && make -j")
105
+ print()
106
+ print(" # 方式 3: 用 Hugging Face 的轉換工具")
107
+ print(" pip install transformers[gguf]")
108
+ print()
109
+
110
+ # 嘗試用 transformers 的 GGUF 導出
111
+ print("🔄 嘗試用 transformers 導出 GGUF...")
112
+ try:
113
+ from transformers import AutoModelForCausalLM, AutoTokenizer
114
+
115
+ os.makedirs(output_dir, exist_ok=True)
116
+ gguf_path = os.path.join(output_dir, "model.gguf")
117
+
118
+ tokenizer = AutoTokenizer.from_pretrained(merged_dir)
119
+ model = AutoModelForCausalLM.from_pretrained(merged_dir, torch_dtype="auto")
120
+ model.save_pretrained(output_dir, safe_serialization=False)
121
+
122
+ # 用 convert script from transformers
123
+ convert_cmd = [
124
+ sys.executable, "-c",
125
+ f"from transformers.convert_slow_tokenizer import convert_gguf; "
126
+ f"convert_gguf('{merged_dir}', '{gguf_path}')"
127
+ ]
128
+ result = subprocess.run(convert_cmd, capture_output=True, text=True)
129
+ if result.returncode == 0:
130
+ print(f"✅ GGUF 轉換完成: {gguf_path}")
131
+ return gguf_path
132
+ except Exception as e:
133
+ pass
134
+
135
+ print()
136
+ print("❌ 自動轉換失敗。請手動安裝 llama.cpp 後重試。")
137
+ print(f" 或者直接用合併後的模型: {merged_dir}")
138
+ return None
139
+
140
+ # 用 llama.cpp 轉換
141
+ os.makedirs(output_dir, exist_ok=True)
142
+ fp16_gguf = os.path.join(output_dir, "model-fp16.gguf")
143
+ quant_gguf = os.path.join(output_dir, f"model-{quantize}.gguf")
144
+
145
+ # Step 2a: HF → GGUF (fp16)
146
+ print("🔄 轉換 HF → GGUF (fp16)...")
147
+ result = subprocess.run(
148
+ [sys.executable, convert_script, merged_dir,
149
+ "--outfile", fp16_gguf, "--outtype", "f16"],
150
+ capture_output=True, text=True)
151
+
152
+ if result.returncode != 0:
153
+ print(f"❌ 轉換失敗:\n{result.stderr[:500]}")
154
+ return None
155
+
156
+ # Step 2b: 量化
157
+ if quantize and quantize != "f16" and quantize_bin and os.path.exists(quantize_bin):
158
+ print(f"🔄 量化 → {quantize}...")
159
+ result = subprocess.run(
160
+ [quantize_bin, fp16_gguf, quant_gguf, quantize.upper()],
161
+ capture_output=True, text=True)
162
+
163
+ if result.returncode == 0:
164
+ # 刪除 fp16 版本節省空間
165
+ os.remove(fp16_gguf)
166
+ gguf_path = quant_gguf
167
+ else:
168
+ print(f"⚠️ 量化失敗,使用 fp16 版本")
169
+ gguf_path = fp16_gguf
170
+ else:
171
+ gguf_path = fp16_gguf
172
+
173
+ size = os.path.getsize(gguf_path) / 1024**3
174
+ print(f"✅ GGUF 完成: {gguf_path} ({size:.1f} GB)")
175
+ return gguf_path
176
+
177
+
178
+ def step3_register_ollama(gguf_path, model_name="codepilot"):
179
+ """Step 3: 註冊到 Ollama"""
180
+ print(f"\n{'='*60}")
181
+ print(f" Step 3: 註冊到 Ollama")
182
+ print(f"{'='*60}\n")
183
+
184
+ if not shutil.which("ollama"):
185
+ print("❌ Ollama 未安裝")
186
+ print(" 安裝: curl -fsSL https://ollama.ai/install.sh | sh")
187
+ return
188
+
189
+ # 建立 Modelfile
190
+ modelfile_path = os.path.join(os.path.dirname(gguf_path), "Modelfile")
191
+ modelfile_content = f"""FROM {os.path.abspath(gguf_path)}
192
+
193
+ TEMPLATE \"\"\"{{{{- if .System }}}}<|im_start|>system
194
+ {{{{ .System }}}}<|im_end|>
195
+ {{{{- end }}}}
196
+ <|im_start|>user
197
+ {{{{ .Prompt }}}}<|im_end|>
198
+ <|im_start|>assistant
199
+ \"\"\"
200
+
201
+ PARAMETER stop "<|im_end|>"
202
+ PARAMETER stop "<|endoftext|>"
203
+ PARAMETER temperature 0.7
204
+ PARAMETER top_p 0.9
205
+ PARAMETER num_ctx 4096
206
+
207
+ SYSTEM \"\"\"You are CodePilot, an expert AI programming assistant.
208
+ Write clean, efficient, well-documented code.\"\"\"
209
+ """
210
+
211
+ Path(modelfile_path).write_text(modelfile_content)
212
+ print(f"📝 Modelfile 已建立: {modelfile_path}")
213
+
214
+ # 註冊到 Ollama
215
+ print(f"🔄 ollama create {model_name}...")
216
+ result = subprocess.run(
217
+ ["ollama", "create", model_name, "-f", modelfile_path],
218
+ capture_output=True, text=True)
219
+
220
+ if result.returncode == 0:
221
+ print(f"\n✅ 已註冊到 Ollama!")
222
+ print(f"\n 使用方式:")
223
+ print(f" ollama run {model_name}")
224
+ print(f" ollama run {model_name} '寫一個快速排序'")
225
+ print(f"\n 在 CodePilot 中使用:")
226
+ print(f" python codepilot_v4.py --provider ollama --cloud-model {model_name}")
227
+ else:
228
+ print(f"❌ 註冊失敗:\n{result.stderr[:300]}")
229
+ print(f"\n 手動註冊:")
230
+ print(f" ollama create {model_name} -f {modelfile_path}")
231
+
232
+
233
+ def step4_push_to_hub(merged_dir, repo_id):
234
+ """(可選)上傳到 HuggingFace Hub"""
235
+ print(f"\n{'='*60}")
236
+ print(f" Step 4: 上傳到 HuggingFace Hub")
237
+ print(f" Repo: {repo_id}")
238
+ print(f"{'='*60}\n")
239
+
240
+ from huggingface_hub import HfApi
241
+ api = HfApi()
242
+
243
+ print("📤 上傳中...")
244
+ api.upload_folder(
245
+ folder_path=merged_dir,
246
+ repo_id=repo_id,
247
+ repo_type="model",
248
+ commit_message="CodePilot fine-tuned model",
249
+ )
250
+ print(f"✅ 已上傳: https://huggingface.co/{repo_id}")
251
+
252
+
253
+ def step_lmstudio(gguf_path):
254
+ """顯示 LM Studio 使用說明"""
255
+ print(f"\n{'='*60}")
256
+ print(f" LM Studio 使用方式")
257
+ print(f"{'='*60}\n")
258
+ print(f" 1. 打開 LM Studio")
259
+ print(f" 2. 左側選「My Models」")
260
+ print(f" 3. 點「Import Model」")
261
+ print(f" 4. 選擇: {os.path.abspath(gguf_path)}")
262
+ print(f" 5. 載入後就可以在 LM Studio 中使用")
263
+ print(f"\n 或者把 GGUF 文件複製到 LM Studio 的模型目錄:")
264
+
265
+ # LM Studio 預設路徑
266
+ import platform
267
+ if platform.system() == "Windows":
268
+ lm_dir = os.path.expanduser("~/.cache/lm-studio/models")
269
+ elif platform.system() == "Darwin":
270
+ lm_dir = os.path.expanduser("~/.cache/lm-studio/models")
271
+ else:
272
+ lm_dir = os.path.expanduser("~/.cache/lm-studio/models")
273
+
274
+ dest = os.path.join(lm_dir, "codepilot")
275
+ print(f" mkdir -p {dest}")
276
+ print(f" cp {os.path.abspath(gguf_path)} {dest}/")
277
+
278
+
279
+ def main():
280
+ parser = argparse.ArgumentParser(description="匯出模型給 Ollama / LM Studio")
281
+ parser.add_argument("--base-model", default=DEFAULT_BASE_MODEL, help="基礎模型")
282
+ parser.add_argument("--adapter", help="LoRA adapter 路徑")
283
+ parser.add_argument("--merged-model", help="已合併的模型路徑(跳過 Step 1)")
284
+ parser.add_argument("--output", default="./exported_model", help="輸出目錄")
285
+ parser.add_argument("--quantize", default="q4_k_m",
286
+ choices=["f16", "q8_0", "q6_k", "q5_k_m", "q4_k_m", "q4_0", "q3_k_m", "q2_k"],
287
+ help="量化等級 (預設: q4_k_m)")
288
+ parser.add_argument("--ollama", action="store_true", help="自動註冊到 Ollama")
289
+ parser.add_argument("--ollama-name", default="codepilot", help="Ollama 模型名稱")
290
+ parser.add_argument("--merge-only", action="store_true", help="只合併,不轉 GGUF")
291
+ parser.add_argument("--push-to-hub", help="上傳到 HF Hub (格式: username/model-name)")
292
+ args = parser.parse_args()
293
+
294
+ print("""
295
+ ╔════════════════════════════════════════════════════════════╗
296
+ ║ CodePilot Model Export ║
297
+ ║ LoRA → 合併 → GGUF → Ollama / LM Studio ║
298
+ ╚════════════════════════════════════════════════════════════╝
299
+ """)
300
+
301
+ merged_dir = args.merged_model
302
+ gguf_path = None
303
+
304
+ # Step 1: 合併
305
+ if not merged_dir:
306
+ if not args.adapter:
307
+ print("❌ 請指定 --adapter 或 --merged-model")
308
+ sys.exit(1)
309
+ merged_dir = os.path.join(args.output, "merged")
310
+ step1_merge_adapter(args.base_model, args.adapter, merged_dir)
311
+
312
+ if args.merge_only:
313
+ print(f"\n✅ 合併完成: {merged_dir}")
314
+ return
315
+
316
+ # Step 2: GGUF
317
+ gguf_dir = os.path.join(args.output, "gguf")
318
+ gguf_path = step2_convert_gguf(merged_dir, gguf_dir, args.quantize)
319
+
320
+ # Step 3: Ollama
321
+ if args.ollama and gguf_path:
322
+ step3_register_ollama(gguf_path, args.ollama_name)
323
+
324
+ # LM Studio 說明
325
+ if gguf_path:
326
+ step_lmstudio(gguf_path)
327
+
328
+ # 上傳
329
+ if args.push_to_hub:
330
+ step4_push_to_hub(merged_dir, args.push_to_hub)
331
+
332
+ print(f"\n{'='*60}")
333
+ print(f" 🎉 匯出完成!")
334
+ print(f"{'='*60}")
335
+ print(f" 合併模型: {merged_dir}")
336
+ if gguf_path: print(f" GGUF: {gguf_path}")
337
+ print()
338
+ print(f" 量化選項說明:")
339
+ print(f" f16 — 最高品質,最大 (~6GB)")
340
+ print(f" q8_0 — 幾乎無損 (~3.5GB)")
341
+ print(f" q6_k — 高品質 (~2.8GB)")
342
+ print(f" q5_k_m — 好的平衡 (~2.4GB)")
343
+ print(f" q4_k_m — 推薦預設 (~2.0GB) ← 品質/大小最佳平衡")
344
+ print(f" q4_0 — 較小 (~1.8GB)")
345
+ print(f" q3_k_m — 很小 (~1.5GB)")
346
+ print(f" q2_k — 最小,品質有損 (~1.2GB)")
347
+
348
+
349
+ if __name__ == "__main__":
350
+ main()