Qwen2.5-14B LoRA 實戰:用 9k 對話訓出去中心化 AI 完整 pipeline
如果你 Google 搜「Qwen LoRA tutorial」,前 10 個結果幾乎都是抄 PEFT 官方範例 README、把參數複製貼上、訓 50 步就喊「完成!」的速食文。
這篇不一樣。我們公開真的訓出來、上 mainnet、目前 4 台礦工 24/7 在跑的 dxai-shadow-v1 完整 pipeline——所有 hyperparameter、訓練時間、merge 步驟、GGUF 量化指令、檔案 sha256,每一段都可被讀者用同樣資料 reproduce + 跟 Arbitrum One 上的 Registry 合約 鏈上 modelHash 比對。
預設讀者:有訓過小模型、會用 Hugging Face Trainer 或 transformers + bitsandbytes、想知道實際生產環境怎麼從 base model 推到 GGUF 部署的工程師。
Pipeline 總覽
Qwen/Qwen2.5-14B-Instruct (base, 28GB FP16)
↓ LoRA fine-tune (PEFT, r=16, 3 epochs, 9174 samples)
checkpoint-1722 (LoRA adapter, 263MB safetensors)
↓ merge_and_unload()
merged_model/ (14.8B params, 28GB FP16, 6-shard safetensors)
↓ llama.cpp convert_hf_to_gguf.py --outtype f16
dxai-shadow-v1-f16.gguf (28GB)
↓ llama-quantize Q4_K_M
dxai-shadow-v1-Q4_K_M.gguf (8.4GB) ← sha256 上鏈
↓ 鏈上 registerModel + activateModel
Arbitrum One DXAIModelRegistry (active)
↓ install.sh + Ollama create
全網礦工跑這個版本
底下逐段拆。
Stage 1:訓練資料(9174 筆對話)
規格
{
"train_samples": 9174,
"val_samples": 1020,
"max_seq_length": 1024,
"language_distribution": {
"zh-TW": 0.42,
"en": 0.38,
"ja": 0.12,
"ko": 0.08
}
}
資料來源混合:
- DecentrAI 公開測試期間的對話樣本(去識別化、移除任何含個人資訊的對話)
- ShareGPT 的 zh-TW 子集(人工篩選,過濾低品質)
- 自製合成資料(用 Claude / GPT-4 生成「DecentrAI 知識庫」相關對話)
- 翻譯任務(中英日韓互譯)
我們的目標是讓模型理解:
- DXAI 代幣經濟學(總量 21M、halving、礦工貢獻怎麼分)
- 去中心化 AI 的概念(為什麼鏈上、為什麼免費)
- 程式 / 翻譯 / 解釋等通用能力
樣本格式統一成 ChatML:
<|im_start|>system
You are DXAI-Shadow, the native AI model of DecentrAI ...
<|im_end|>
<|im_start|>user
{question}<|im_end|>
<|im_start|>assistant
{answer}<|im_end|>
Stage 2:LoRA 訓練(PEFT)
完整 hyperparameter(從 training_summary.json 讀回來):
{
"base_model": "Qwen/Qwen2.5-14B-Instruct",
"lora_r": 16,
"lora_alpha": 32,
"lora_dropout": 0.05,
"lora_target_modules": ["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],
"epochs": 3,
"batch_size": 1,
"gradient_accumulation_steps": 16,
"effective_batch_size": 16,
"learning_rate": 2e-4,
"warmup_ratio": 0.1,
"max_seq_length": 1024,
"training_minutes": 2058.5
}
幾個你應該知道為什麼這樣選的決定:
lora_r=16, lora_alpha=32(α/r=2)
通常從 r=8 開始 ramp。我們選 16 是因為 9k 樣本夠養,且 14B 的隱層維度 5120,r=16 給了 5120×16×2 = 163K trainable params per linear layer,對 9k 樣本不會 underfit、也不到 overfit 邊界。alpha=32 讓 lora_scaling=2,比 1.0 更激進但訓練曲線還穩。
target_modules = 7 個全 linear
只 attention 不夠(會學表面模仿不會學風格),加 FFN 三個(gate/up/down)才能讓 instruction-following 行為內化。代價是 trainable params 從 7M 漲到 25M,但 RTX 4090 24GB 跑得起。
batch_size=1, grad_accum=16
14B Q-LoRA 配 1024 seq_len,VRAM 卡得緊。bs=1 + accum=16 能維持 effective bs=16,跟全參數 fine-tune 同 step 表現可比。
epochs=3
我們先跑 1 epoch 看 loss 收斂、再 2 epochs 看 val_loss 走勢。第 3 epoch 結束時 train/val gap 約 0.06(沒 over),所以收手。實際 checkpoint-1722 = step 1722 = 全部訓完。
lr=2e-4, warmup=0.1
LoRA 最常見組合。AdamW + cosine decay。warmup 0.1 比 linear 收斂得更平。
訓練硬體:RTX 4090 24GB(PC,WSL2 Ubuntu)。耗時 2058 分鐘 ≈ 34.3 小時。
Stage 3:Checkpoint → Merge
訓完拿到 LoRA adapter(263MB),但部署到 Ollama / llama.cpp 需要單一 model file。所以要 merge:
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-14B-Instruct",
torch_dtype=torch.float16,
device_map="cpu", # merge 不需要 GPU
)
peft_model = PeftModel.from_pretrained(base, "checkpoints/checkpoint-1722")
# 關鍵:merge_and_unload 把 LoRA 矩陣 W_a @ W_b 加回 base
merged = peft_model.merge_and_unload()
merged.save_pretrained("merged_model", safe_serialization=True, max_shard_size="5GB")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-14B-Instruct")
tokenizer.save_pretrained("merged_model")
產出 merged_model/ 是 6 個 safetensors shard 共 28GB。這是 14.8B 參數的 FP16 weights,跟原 base 同樣大小但已經吃了 LoRA 的調校。
Stage 4:GGUF 量化(CPU 7 分鐘)
GPU inference 不需要 FP16 全精度。Ollama 標配走 GGUF 格式 + Q4_K_M 量化(每個 weight 平均 4.5 bits),把 28GB 壓到 8.4GB。
4a: HF safetensors → GGUF f16
cd /path/to/llama.cpp
python3 convert_hf_to_gguf.py /path/to/merged_model \
--outfile dxai-shadow-v1-f16.gguf \
--outtype f16
這步只是格式轉換,沒精度損失。產出 28GB 的 GGUF(依然 FP16)。耗時約 5-10 分鐘(CPU 主要在 disk I/O)。
4b: f16 → Q4_K_M
./build/bin/llama-quantize \
dxai-shadow-v1-f16.gguf \
dxai-shadow-v1-Q4_K_M.gguf \
Q4_K_M
Q4_K_M 是 llama.cpp 推薦的「品質/大小」最佳折衷量化方法:
- 大部分 weight 用 4-bit symmetric quantization
- attention.wv 跟 ffn.down 用 6-bit(敏感層多分配 bits)
- 平均 bpw = 4.85
實際耗時 2 分 40 秒。產出 8.4GB,比 f16 小 3.3 倍。
4c: 計算 sha256
shasum -a 256 dxai-shadow-v1-Q4_K_M.gguf
# b76bb715a21319ad13545debc6b0f86f3b17a635bd3aa7333e90a61fc0ebb90b
這個值就是 modelHash,等下要上鏈。
Stage 5:上 Arbitrum One Registry
DXAI 設計上把「目前網路在跑哪個 model」記在鏈上,避免礦工偷換假模型騙獎勵。流程:
// scripts/auto-register-shadow-v1.js (簡化版)
import { ethers } from "ethers";
const REGISTRY = "0x5cBBf1a12df98A2b686F6fecCC9D9C437B7191F7";
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const registry = new ethers.Contract(REGISTRY, ABI, wallet);
const modelHash = "0xb76bb715a21319ad13545debc6b0f86f3b17a635bd3aa7333e90a61fc0ebb90b";
const adapterHash = ethers.ZeroHash; // 沒有 separate adapter(已 merge 進 base)
const baseModel = "Qwen/Qwen2.5-14B-Instruct";
const metadataURI = "https://supertools.tw/dxai/manifests/v1.json";
await registry.registerModel("dxai-shadow-v1", modelHash, adapterHash, baseModel, metadataURI);
await registry.activateModel("dxai-shadow-v1");
await registry.bindEpochModel(currentEpoch, "dxai-shadow-v1");
實際 tx:
- registerModel:
0xdae48f4c... - activateModel:
0x7e94a2f2... - bindEpoch(391):
0x597de8f7...
所有人現在可以呼叫 getActiveModel() 拿到 modelHash + 自己 sha256 GGUF 比對:
cast call 0x5cBBf1a12df98A2b686F6fecCC9D9C437B7191F7 \
"getActiveModel()(string,bytes32,bytes32,string,string)" \
--rpc-url https://arb1.arbitrum.io/rpc
Stage 6:礦工部署(Ollama Modelfile)
最後一步——讓全網礦工自動拉這個 model。我們的 install.sh 會:
- 從
https://supertools.tw/api/decentrai/miner/adapter-assignment拿到 signed URL - 下載
dxai-shadow-v1-Q4_K_M.gguf(HMAC 簽名 1h TTL) - 寫 Modelfile:
FROM /path/to/dxai-shadow-v1-Q4_K_M.gguf
TEMPLATE """<|im_start|>system
{{ .System }}<|im_end|>
<|im_start|>user
{{ .Prompt }}<|im_end|>
<|im_start|>assistant
"""
SYSTEM """You are DXAI-Shadow, the native AI model of DecentrAI..."""
PARAMETER num_ctx 8192
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER stop "<|im_end|>"
ollama create dxai-shadow-v1 -f Modelfile— Ollama 會 copy GGUF 進它的 blob store,blob 的 sha256 就是我們上鏈的 modelHash(鏈上 hash = 礦工跑的 hash = 訓練產出 hash——audit chain 三方對齊)
Reproducibility 檢查表
讀者想要驗證或重訓,每個 stage 都可獨立 reproduce:
- [x] 訓練資料:除了 DecentrAI 對話那部分(涉及隱私不公開)外,其他來源公開
- [x] Hyperparameter:完整公開於 v1.json manifest
- [x] 訓練 code:用 PEFT + transformers + Trainer,皆 open source
- [x] GGUF 轉換:llama.cpp 開源
- [x] modelHash:sha256 任何人 reproduce
- [x] 鏈上 tx:Arbitrum One 公開
- [x] GGUF 檔案:supertools.tw/static/adapters/dxai-shadow-v1.gguf(公開)
學到的教訓
訓 14B + 部署到 mainnet 一輪下來,幾個非顯而易見的坑:
1. CPU 主機 RAM 比 GPU VRAM 重要
merge_and_unload 把 28GB 模型放 RAM,PC 要至少 64GB RAM 才不爆。我們的 PC 是 112GB,過程中峰值 60GB。雲端 GPU 機器常給 RTX A6000 + 16GB RAM 是不夠的。
2. GGUF 轉換不能用 Mac Silicon
llama.cpp convert_hf_to_gguf.py 在 macOS arm64 上有 known issue 處理某些 tokenizer 元資料。我們是在 PC(x86_64 Ubuntu in WSL2)上轉的。
3. Q4_K_M vs Q5_K_M 的選擇
Q5_K_M 大 ~30%(約 11GB),perplexity 改善約 5%。對我們的 use case(聊天 + 程式碼),Q4_K_M 已經夠用,且 8GB VRAM 礦工還能跑。如果哪天網路全部礦工都 16GB+,可以推 Q5。
4. Merkle proof 的 leaf format
DecentrAI 鏈上結算用 OpenZeppelin double-hash:
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(addr, amount))));
不是 single-hash。這個細節決定 off-chain script 怎麼算 proof。我們在 scripts/decentrai_settle.js 完整實作了 chain-format Merkle tree 給礦工自助 claim。
接下來:dxai-shadow-v2 規劃
已 ship v1,社群正在討論 v2 方向。候選方案:
- 方案 A:base model 升 Qwen 3.0(如果 Apache 授權)
- 方案 B:base 不變,但訓練資料擴到 50k 筆 + 加多輪對話
- 方案 C:訓 32B 版本(需要 Tier 3+ 礦工門檻)
任何方案都會走 DAO 治理流程 投票決定。Stake DXAI 即可投票。
把這篇 reproduce 一次,你會學到什麼
- LoRA 在中等資料量(< 10k)的最佳實踐
- 中模型(14B)量化部署的整條工具鏈
- 用 Arbitrum L2 做 model audit trail 的設計模式
- 為什麼「模型權重 sha256 + 鏈上 commitment」是去中心化 AI 的必要組件
歡迎 fork 我們的 pipeline。有問題開 issue 或 直接問 DecentrAI(你問的時候它就在用自己的 weights 回答你 :) )。
延伸閱讀