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 知識庫」相關對話)
  • 翻譯任務(中英日韓互譯)

我們的目標是讓模型理解:

  1. DXAI 代幣經濟學(總量 21M、halving、礦工貢獻怎麼分)
  2. 去中心化 AI 的概念(為什麼鏈上、為什麼免費)
  3. 程式 / 翻譯 / 解釋等通用能力

樣本格式統一成 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:

所有人現在可以呼叫 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 會:

  1. 從 https://supertools.tw/api/decentrai/miner/adapter-assignment 拿到 signed URL
  2. 下載 dxai-shadow-v1-Q4_K_M.gguf(HMAC 簽名 1h TTL)
  3. 寫 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|>"
  1. 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 回答你 :) )。


延伸閱讀