快轉到主要內容
  1. 教學文章/

MLX-LM 模型轉換與量化:4/8-bit、Mixed Quant 與品質評測

·9 分鐘· loading · loading · ·
Mlx MLX-LM LLM Quantization Apple-Silicon Model Conversion Local AI
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
科技觀點 - 本文屬於一個選集。
§ 25: 本文

featured

一. 前言:模型能塞進記憶體,不代表量化工作做完了
#

在 Apple Silicon 上跑本地 LLM,最常見的第一個建議就是:「下載 4-bit 版本。」 這句話沒錯,卻省略了真正麻煩的部分。 同一個模型可以轉成 8-bit、4-bit,甚至讓不同 layer 使用不同 bit 數。 檔案變小以後,輸出品質、載入時間和生成速度也不一定等比例變化。 所以真正該問的不是「4-bit 好不好」,而是:

  • 這個模型能不能正確轉成 MLX 格式?
  • 4-bit 和 8-bit 各省下多少磁碟與記憶體?
  • group_size 改變後,品質差異值得那一點空間嗎?
  • mixed-bit recipe 是否比統一 4-bit 更適合自己的任務?
  • 要怎麼用同一組 prompts 公平比較?

今天拍拍君會做一條可重跑的量化流水線。 我們不只執行 mlx_lm.convert,還會保存設定、檢查產物,並建立小型 A/B 評測。 如果你只想先把模型跑起來,請看 MLX-LM 本地模型推論。 如果你還不熟 MLX 的 array、lazy evaluation 與基本 API,可以先看 Python MLX 入門。 這篇從「原始 Hugging Face checkpoint 已經選好」開始,只處理轉換與量化。

二. 先建立可重現的量化專案
#

先準備乾淨環境:

uv init mlx-quant-lab
cd mlx-quant-lab
uv add mlx-lm psutil

確認 CLI:

uv run mlx_lm.convert --help
uv run mlx_lm.generate --help

建議的目錄如下:

mlx-quant-lab/
├── models/
├── prompts.jsonl
├── results/
├── scripts/
│   ├── inspect_model.py
│   └── benchmark.py
└── README.md

把來源模型固定成 shell 變數:

MODEL_ID="Qwen/Qwen3-0.6B"

正式專案還應該記錄:

uv run python -c "from importlib.metadata import version; print(version('mlx-lm'))"
git rev-parse HEAD 2>/dev/null || true

模型倉庫也可能持續更新。 如果結果要長期可重現,請另外保存 Hugging Face revision,而不是只記 repo 名稱。

三. 先做不量化的 MLX 基準版本
#

轉換與量化是兩件事。 第一步先把 Hugging Face checkpoint 轉成 MLX 可載入的格式,但不壓低 bit 數:

uv run mlx_lm.convert \
  --hf-path "$MODEL_ID" \
  --mlx-path models/qwen3-0.6b-bf16 \
  --dtype bfloat16

這個版本是後面比較的基準。 --dtype 常見選項包含: float16bfloat16float32

不要把 dtype 轉換和整數量化混為一談。 bfloat16 仍是浮點權重;4-bit affine quantization 則會把可量化 layer 轉成低 bit 表示。 先確認產物:

find models/qwen3-0.6b-bf16 -maxdepth 1 -type f -print
du -sh models/qwen3-0.6b-bf16

一般會看到 config.json、tokenizer 檔案和一個或多個 safetensors。 轉換器會拒絕寫入已存在的輸出資料夾。 這其實是好事:它避免你不小心把兩次實驗混在一起。 請為每個設定使用不同路徑,不要先刪目錄再假裝它們是同一次實驗。

四. 建立 8-bit 版本:先求穩,再往下壓
#

8-bit 通常是很實用的第一個量化基準:

uv run mlx_lm.convert \
  --hf-path "$MODEL_ID" \
  --mlx-path models/qwen3-0.6b-8bit-g64 \
  --quantize \
  --q-bits 8 \
  --q-group-size 64

命名裡直接寫出 8bit-g64,未來比較時會省很多力氣。 兩個核心參數是:

  • --q-bits:每個量化權重使用的 bit 數
  • --q-group-size:多少個權重共享一組量化參數

bit 數越低,模型通常越小。 但「理論上每個權重 4 bit」不代表整個目錄正好縮成四分之一。 原因包含 tokenizer、config、scale、bias,以及未被量化的參數。 所以請量實際產物,不要只用心算。

du -sk models/qwen3-0.6b-* | sort -n

五. 建立 4-bit 版本:本機推論的甜蜜點候選
#

接著做統一 4-bit:

uv run mlx_lm.convert \
  --hf-path "$MODEL_ID" \
  --mlx-path models/qwen3-0.6b-4bit-g64 \
  --quantize \
  --q-bits 4 \
  --q-group-size 64

4-bit 常被當成本機 LLM 的起點,原因很直覺: 權重占用明顯下降;較容易放進 unified memory;同一台機器可以測更大的模型;下載與保存成本較低。

但它不是自動勝出。 小模型本來容量就有限,再壓低精度後,某些任務可能更容易出現: 固定格式漏欄位;數字或短推理不穩;少見詞彙輸出退化;同一 prompt 的結果變動更大。

所以 4-bit 是「值得測的候選」,不是「不用測的答案」。

六. group_size 到底在控制什麼?
#

量化不是替整個矩陣只找一個 scale。 MLX 會按 group 對權重做量化。 較小的 group 通常能更細緻地貼近局部權重分布,但需要更多 scale 等附加資料。 較大的 group 附加成本較低,卻可能讓同一組裡差異較大的權重共用參數。 你可以再建立一個 4-bit、group size 32 的候選:

uv run mlx_lm.convert \
  --hf-path "$MODEL_ID" \
  --mlx-path models/qwen3-0.6b-4bit-g32 \
  --quantize \
  --q-bits 4 \
  --q-group-size 32

比較時至少保留這三組:

版本 bits group size 角色
BF16 16 品質基準
Q8 8 64 保守量化
Q4 4 64 空間優先

如果 Q4 的品質邊界很接近,再加入 Q4/G32。 不要一開始排列組合十幾組。 先用三個候選找到趨勢,再針對有疑問的區域加實驗。

七. Mixed Quant:不是每一層都該被同樣對待
#

統一 4-bit 很容易理解,但不同 layer 對量化誤差的敏感度可能不同。 MLX-LM 的轉換器內建 mixed-bit recipes: mixed_2_6mixed_3_4mixed_3_6mixed_4_6

例如建立 3/4-bit 混合版本:

uv run mlx_lm.convert \
  --hf-path "$MODEL_ID" \
  --mlx-path models/qwen3-0.6b-mixed-3-4 \
  --quantize \
  --q-group-size 64 \
  --quant-predicate mixed_3_4

這類 recipe 不是把整個模型平均切成兩半。 轉換器會按 layer 位置與 module 名稱套用不同精度,對部分投影層與輸出 head 保留較高 bit 數。 因此 mixed quant 的真正問題是:

在接近的模型大小下,它是否比統一低 bit 更能保住你的任務品質? 請不要只看 recipe 名稱猜答案。 模型架構不符合預期 key 時,轉換器也可能拒絕套用 recipe。 這時應該保存錯誤訊息並換策略,不要假裝它已經混合量化。

八. 讀 config.json,確認不是只有資料夾名稱改了
#

實驗命名可以寫錯,模型設定比較難騙人。 建立 scripts/inspect_model.py

from __future__ import annotations

import json
import sys
from pathlib import Path


def main() -> None:
    model_dir = Path(sys.argv[1])
    config = json.loads(
        (model_dir / "config.json").read_text(encoding="utf-8")
    )
    quant = config.get("quantization")
    legacy = config.get("quantization_config")
    print(json.dumps(quant or legacy, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()

逐一檢查:

for model in models/*; do
  echo "=== $model"
  uv run python scripts/inspect_model.py "$model"
done

統一量化通常會看到 group_sizebitsmode。 mixed-bit 模型則可能保存更細的 per-layer 設定。 如果輸出是 null,先不要進行 benchmark。 你可能做的是 dtype 轉換,而不是自己以為的低 bit 量化。

九. 先做 smoke test,避免拿壞產物跑半天
#

每個模型先用同一個短 prompt:

for model in \
  models/qwen3-0.6b-bf16 \
  models/qwen3-0.6b-8bit-g64 \
  models/qwen3-0.6b-4bit-g64 \
  models/qwen3-0.6b-mixed-3-4
do
  uv run mlx_lm.generate \
    --model "$model" \
    --prompt "只回答一個 JSON:{\"status\":\"ok\"}" \
    --max-tokens 40
done

smoke test 要確認: tokenizer 可以載入;權重沒有 shape mismatch;prompt 可以送入;至少能生成文字;process 沒有立刻吃光記憶體。

它不負責證明品質。 它只負責把壞產物擋在正式評測門外。

十. 建立固定 prompts:別用聊天感覺決定勝負
#

建立 prompts.jsonl

{"id":"format-01","prompt":"只輸出 JSON,欄位為 title 與 priority。內容:登入頁按鈕失效,影響全部使用者。"}
{"id":"summary-01","prompt":"把這句話壓成 20 字內:部署成功但健康檢查持續 timeout,需要檢查反向代理與應用程式 port。"}
{"id":"classify-01","prompt":"只回答 bug、feature 或 question:希望匯出結果能支援 Parquet。"}
{"id":"reason-01","prompt":"盒子有紅藍球各 3 顆,不放回抽 2 顆。用兩步驟說明抽到同色球的機率。"}

評測集應該來自你真正的使用情境。 建議至少混合: 格式遵循;摘要;分類;短推理;中英文或專有名詞;自己最在意的 domain prompt。

十筆真實任務,通常比一百筆隨便湊的問題更有判斷力。

十一. 寫一個可比較的 benchmark runner
#

建立 scripts/benchmark.py

from __future__ import annotations

import argparse
import json
import time
from pathlib import Path

import mlx.core as mx
from mlx_lm import generate, load


def read_jsonl(path: Path) -> list[dict[str, str]]:
    return [
        json.loads(line)
        for line in path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--model", required=True)
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args()

    rows = read_jsonl(Path("prompts.jsonl"))
    started = time.perf_counter()
    model, tokenizer = load(args.model)
    mx.eval(model.parameters())
    load_seconds = time.perf_counter() - started

    results = []
    for row in rows:
        prompt = tokenizer.apply_chat_template(
            [{"role": "user", "content": row["prompt"]}],
            tokenize=False,
            add_generation_prompt=True,
        )
        t0 = time.perf_counter()
        answer = generate(
            model,
            tokenizer,
            prompt=prompt,
            max_tokens=160,
            verbose=False,
        )
        results.append(
            {
                "id": row["id"],
                "seconds": round(time.perf_counter() - t0, 4),
                "answer": answer,
            }
        )

    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(
        json.dumps(
            {"model": args.model, "load_seconds": load_seconds, "rows": results},
            ensure_ascii=False,
            indent=2,
        ),
        encoding="utf-8",
    )


if __name__ == "__main__":
    main()

執行每個版本:

for name in bf16 8bit-g64 4bit-g64 mixed-3-4; do
  uv run python scripts/benchmark.py \
    --model "models/qwen3-0.6b-$name" \
    --output "results/$name.json"
done

為了公平,請關閉其他吃記憶體的程式,並讓每個版本都跑相同 prompts 與 token 上限。

十二. 速度要分開看:載入、prefill、generation
#

只記總秒數會把不同成本混在一起。 本地推論至少有三段:

  1. 模型載入
  2. prompt prefill
  3. token generation

短 prompt 加長輸出,主要看 generation。 長文件加短回答,則可能主要卡在 prefill。 mlx_lm.generate 的 verbose 輸出可以提供 prompt 與 generation throughput。 正式測速時,可另外保留一輪 CLI 原始輸出:

uv run mlx_lm.generate \
  --model models/qwen3-0.6b-4bit-g64 \
  --prompt "請列出五個量化模型評測項目。" \
  --max-tokens 200 \
  2>&1 | tee results/q4-speed.txt

第一次執行還可能包含檔案 cache 與系統暖身成本。 至少跑三次,分開報 cold run 和 warm run。

十三. 記憶體評測:看峰值,不只看模型目錄
#

磁碟大小只是靜態權重的一部分。 執行時還需要: 模型參數;tokenizer 與 Python process;prompt tokens;KV cache;MLX/Metal 配置與暫存空間。

macOS 可以先用 /usr/bin/time -l 包住命令:

/usr/bin/time -l uv run mlx_lm.generate \
  --model models/qwen3-0.6b-4bit-g64 \
  --prompt "用三點說明量化的風險。" \
  --max-tokens 256 \
  > results/q4-memory.txt 2>&1

所有版本要使用相同 prompt 長度與 max_tokens。 KV cache 會隨 context 增長;用短 prompt 測出的峰值不能代表長文件工作流。 如果你的真實任務會塞 8K tokens,就應該另外建立長 context 測試。

十四. 品質評測:先做規則,再做人工盲測
#

量化品質不應只看「答案讀起來還行」。 對固定格式任務,可以自動檢查: JSON 能否 parse;必要欄位是否存在;enum 是否落在允許集合;摘要是否超過長度;分類是否命中標籤。

對開放式回答,建議把模型名稱隱藏後人工比較。 可以用這個簡單評分表:

指標 0 分 1 分 2 分
正確性 錯誤 部分正確 正確
格式 無法使用 可修復 完全符合
完整性 漏重點 少量遺漏 足夠完整
穩定性 多次差很大 偶有偏移 大致一致

如果 BF16、Q8 和 Q4 在你的 30 筆任務都同分,就選更小或更快的版本。 如果 Q4 在關鍵格式任務明顯掉分,Q8 或 mixed quant 才是更合理的答案。

十五. 不要踩的幾個坑
#

1. 直接量化別人已經量化過的模型
#

優先從原始或較高精度 checkpoint 建立候選。 反覆低 bit 轉換可能把誤差再疊上去。

2. 只測一個漂亮 prompt
#

單筆 demo 很容易被運氣騙。 至少覆蓋幾種任務,並重跑重要樣本。

3. 不保存轉換參數
#

把完整命令、來源 revision、mlx-lm 版本和輸出路徑寫進 README。 否則三週後只剩「這個資料夾好像是 4-bit」。

4. 以為低 bit 一定更快
#

速度會受模型、序列長度、記憶體壓力和 kernel 支援影響。 請量測,不要用檔名推論效能。

5. 忽略模型授權
#

轉換格式不會消除原始模型 license。 要上傳到 Hugging Face 或散布給別人前,先確認使用與再發布條款。

十六. 如何做最後選擇?
#

拍拍君會用這個順序:

  1. 先淘汰無法載入或格式錯誤的版本
  2. 再淘汰關鍵任務品質不合格的版本
  3. 比較剩餘版本的峰值記憶體
  4. 比較真實 prompt 的 warm throughput
  5. 最後才看磁碟大小與分享成本

一個實用的結果表可能長這樣:

版本 大小 峰值記憶體 速度 任務通過率 決定
BF16 實測 實測 實測 實測 品質基準
Q8/G64 實測 實測 實測 實測 保守候選
Q4/G64 實測 實測 實測 實測 空間候選
Mixed 3/4 實測 實測 實測 實測 平衡候選

別把網路上的 benchmark 數字直接填進來。 這張表的價值就在於它反映你的 Mac、模型和工作負載。

結語
#

量化不是按下 --quantize 就結束,而是一個取捨實驗。 MLX-LM 已經把 Hugging Face 到 MLX 的轉換、統一低 bit 與 mixed-bit recipe 包成好用的 CLI。 真正需要你負責的,是建立可信的比較方式。 先保留 BF16 基準,再建立 Q8、Q4 與一個 mixed quant 候選。 檢查 config.json、做 smoke test,最後用同一組 prompts 比較大小、峰值記憶體、速度和品質。 如果 Q4 通過所有關鍵任務,那就開心省下記憶體。 如果它掉分,也不是量化失敗,而是你成功找到自己的品質邊界。 這才是工程上真正有用的答案。🔭

延伸閱讀
#

科技觀點 - 本文屬於一個選集。
§ 25: 本文

相關文章

MLX-LM 批次推論實戰:Prompt Template、抽樣參數與本機評測流程
·9 分鐘· loading · loading
Mlx MLX-LM LLM Batch Inference Apple-Silicon Local AI
MLX-LM 實戰:在 Apple Silicon 上跑本地模型推論
·9 分鐘· loading · loading
Mlx MLX-LM LLM Apple-Silicon Python Local AI
MLX-LM 本機 API Server:OpenAI 相容介面、Prompt Cache 與 Tool Calling
·8 分鐘· loading · loading
Mlx MLX-LM LLM OpenAI API Prompt Cache Tool Calling Apple-Silicon
MLX-LM LoRA 微調入門:Adapter、資料格式與 Apple Silicon 本機評測
·13 分鐘· loading · loading
Mlx MLX-LM LoRA Fine-Tuning Apple-Silicon Local AI
在 Mac/iPhone 生態跑本地 AI:Ollama、MLX 與行動端工作流
·9 分鐘· loading · loading
LLM Ollama Mlx Apple-Silicon IPhone Local AI
本地 AI App 架構:Streamlit、Ollama、MLX 怎麼分工
·10 分鐘· loading · loading
LLM Local AI Streamlit Ollama Mlx Python Architecture