
昨天訓練出一個 adapter,304 MiB,掛在 4-bit 底模上用 Unsloth 推論是成功的,但它的那個狀態離掛進閘道給大家用還有一段距離,原因是 Unsloth 的推論路徑不是服務引擎,decode 掉到 9.65 t/s,LoRA 的額外矩陣乘法每一輪都在支付額外的時間成本,而且它不是 Day 18 文章中採用 LiteLLM 閘道認得的端點。
今天把它變成 zh-writer 那個用途名背後的新模型,路線是這樣,先合併回底層模型轉 GGUF、走 Day 12 文章中提到的量化階梯、掛 llama-server、切閘道別名。每一步都有一個要驗的東西,最後一步要驗的最重要,就是去看昨天學到的行為,經過合併與量化之後還剩多少實力呢 ?
先講結論,因為它跟我動手前的預期相反,行為一點都沒掉,在 q4_k_m 上甚至還好一點。 但合併這段的測試,我有碰到一些問題,其中一次是有顯示成功訊息,卻產生一份壞掉模型的情況。
(單位體例,本篇所有記憶體與檔案大小都沿用工具回報的 GiB,free、/proc/meminfo、vLLM、llama-bench 與 bytes / 2^30 是同一個口徑,不做十進位換算。系列前面幾天混用過 GB,那是 Day 15 到 24 的體例,合先敘明。)
LoRA 這種小模型的優點,是它的旁路矩陣可以在數學上合併進原始權重,合併後的模型結構跟底模一模一樣,推論時也沒有額外計算。Unsloth 提供一行式:
model.save_pretrained_merged(
"~/models/Qwen3.8-27B-zhwriter-v1",
tokenizer, save_method="merged_16bit")
我原本的理解是「合併時要先把底模反量化回 BF16 再加上 adapter」,結果實際上不是這樣。
adapter 是在 bitsandbytes 4-bit 底模上訓練的,也就是對著 dequant(W4) 學。但 merged_16bit 預設把差量疊回原始的 16 位元底模 W16,兩者差一個量化誤差 q = W16 - dequant(W4),那個 +q 是 adapter 從來沒看過的。原始碼講得很清楚,unsloth_zoo/saving_utils.py:193:
_DEQUANT_MERGE_BASE_MODEL_TYPES = frozenset({"falcon_h1"})
只有這個集合裡的架構才會改疊到 dequant(W4) 上,qwen3_5 不在裡面。註解說明這是刻意的,疊到 dequant 基底會把量化雜訊烤進 checkpoint,讓一般模型退步,以 Qwen2.5 為例,其 PPL 會惡化約 20%。只有「底模權重範數很大而 MuP 乘數很小」的深層 hybrid 堆疊(Falcon-H1)才需要那條路。
所以合併產物確實帶著一個 adapter 沒看過的誤差,BF16 那一欄量的就是這個 q 的代價。
save_pretrained_merged(save_method="merged_16bit") 跑完印出:
Unsloth: Model saved successfully to '~/models/Qwen3.8-27B-zhwriter-v1'
底模載入 404.0 秒,合併與寫檔 109.2 秒,系統記憶體峰值 72.96 GiB,看起來一切正常。唯一的線索是產物只有 17.05 GiB,而 27B 的 BF16 應該是 51.75 GiB。
拆開來看,產生出來的那份 checkpoint 根本不是 16 位元的版本:
absmax、quant_map、nested_absmax、quant_state.bitsandbytes__nf4)config.json 裡的 quantization_config.quant_method 還是 bitsandbytes
model.language_model.language_model.language_model.layers.0...
model.language_model.visual.* 底下換句話說它沒有反量化,只是把 4-bit 的權重換個地方存,順便把 key 名弄壞。成功訊息不等於成功,這一格如果不去檢查,會一路帶著壞模型資料,走到量化時才發現。
於是改走標準路徑來做,透過 transformers 直接以 BF16 載入 Qwen3_5ForConditionalGeneration,掛 adapter,merge_and_unload,save_pretrained:
| 項目 | 值 |
|---|---|
| BF16 底模載入 | 339.7 秒 |
| adapter 命中 | 256 個 LoRA 模組 |
merge_and_unload |
0.3 秒 |
| 存檔 | 118.7 秒 |
| 系統記憶體峰值 | 106.89 GiB |
| torch 峰值 | 50.96 GiB |
| 產物 | 50.97 GiB、全 BF16、量化殘留 0 |
權重這次是對的,但 key 名還是壞的,而且壞法跟第一次一樣,前綴被類別包裝多疊一層,視覺塔變成 model.language_model.visual,張量數從底模的 1199 掉到 1184,少了 15 個。存出來的東西載不回去。
所以那個重複前綴不是 Unsloth 的錯,是這款 ForConditionalGeneration 在 transformers 5.5 上,模型類別與 checkpoint 的巢狀層級對不齊,兩條正規路徑都走不通。
另外注意那個 106.89 GiB 的峰值。這台機器具備 121.63 GiB 統一記憶體,而且 Day 25 才因為記憶體超配把整台弄到只剩 ping 有回應,這條路徑的餘裕只有 15 GiB。
最後的做法是完全繞過 transformers 的模型語意,直接在 safetensors 層級做合併:
W += (lora_alpha / r) * (B @ A) # scale = 32 / 16 = 2.0
逐個 shard 讀進來,命中的張量改掉,其他原樣複製,shard 切法與 model.safetensors.index.json 沿用底層模型。
adapter 命中的 256 個模組是有結構的,Qwen3.8-27B 是 hybrid linear/full attention,full_attention_interval 是 4,所以 64 層裡只有 16 層有 self_attn。
LoRA 的 target 正則要求模組名含 q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj,linear_attn.in_proj_a 那類對不上。算出來就是 64 層乘 3 個 mlp 模組,加上 16 層乘 4 個注意力模組,等於 256,adapter 的 512 個張量正好是 256 乘 A、B 兩支。
| 項目 | 值 |
|---|---|
| 合併耗時 | 44.7 秒(18 個 shard) |
| 系統記憶體峰值 | 10.07 GiB |
| torch 峰值 | 0.67 GiB |
| 輸出大小 | 51.77 GiB(55,586,114,625 bytes) |
| 張量數 | 1199,與底模逐一相同 |
| shard 配置 | 與底模逐一相同 |
| dtype | 全部 BF16 |
| 量化殘留 key | 0 |
| 視覺塔 | 完整保留,333 個 model.visual.* 張量 |
測試結果不錯,比第二條路徑快七倍,記憶體峰值是它的十分之一,而且結構可驗證。一次只碰一個 shard,就不需要把 27B 整個放進記憶體。
至於 Unsloth 有沒有打算保留視覺塔,意圖上是有的,unsloth/save.py:710 有專門的 _is_qwen3_5_vlm() 判斷與 _qwen3_5_vlm_state_dict_for_save() 重映射函式,把 visual.* 改寫成 model.visual.* 一起寫出,只是那條路徑在這款模型上沒有產出可用的東西。

合併完立刻做 Day 5 的老規矩,MANIFEST 寫上底模來源與授權(Apache 2.0,衍生模型跟著)、adapter 的 sha256 與訓練日期、資料集版本,以及合併基底是 W16 的資訊。
python convert_hf_to_gguf.py ~/models/Qwen3.8-27B-zhwriter-v1 \
--outfile ~/models/gguf/zhwriter/zhwriter-v1-bf16.gguf --outtype bf16
Qwen3_5ForConditionalGeneration 的支援是完整的。conversion/qwen.py:630 把它註冊到 MODEL_ARCH.QWEN35(純文字),conversion/qwen3vl.py:16 註冊同一個架構的 mmproj 分支,進行轉檔。

| 項目 | 值 |
|---|---|
| 耗時 | 82.1 秒 |
| 記憶體峰值 | 10.75 GiB |
| 輸出 | 50.90 GiB(54,657,734,048 bytes) |
| 張量數 | 866 |
general.architecture |
qwen35 |
context_length |
262144 |
866 這個數字呢,如果看底模 1199 減掉視覺塔的 333,正好 866。視覺塔在純文字 GGUF 這一側被精準地丟掉了。
我原本的預期是「GGUF 就是純文字模型,視覺能力在這條路徑上不存在,這是已知取捨」,而且進一步預期 --mmproj 會被 convert_hf_to_gguf.py:251 那行 assert hparams.get("vision_encoder") is not None 擋下來,因為這款的 config 用的是 vision_config 而不是 vision_encoder。
實測是我錯了,--mmproj 能順利跑完:
mmproj-zhwriter-v1-f16.gguf: n_tensors = 334, total_size = 884.6 MiB
所以正確的說法不是「視覺能力不存在」,而是視覺能力被拆成獨立的一款 884.6 MiB 檔案,要用的話就再掛上去,不用的話就可以少載這一份較小的檔案。866 加 334 等於 1200,比原本的 1199 多一個,正是因為 v.patch_embd.weight 在轉檔時被拆成兩個張量。
這比取捨好多了,值得一試,由於檔案轉得出來跟視覺功能是否能用其實是兩回事,所以我把它掛載起來,提供圖片給它測試看看。
llama-server -m zhwriter-v1-q8_0.gguf --mmproj mmproj-zhwriter-v1-f16.gguf,日誌回一句 load_model: loaded multimodal model,然後提供 Day 24 那張夜市影片中的圖片,它辨識出圖片來並提供說明文字:
這張照片拍攝於夜晚的街頭小吃街,充滿了熱鬧的市井氣息。畫面中,兩旁是密集的攤位和商店,掛滿了五顏六色、亮著燈的招牌⋯⋯前景左側有一個不鏽鋼製的攤車,攤主正在煎製食物,鍋中冒著熱氣,旁邊有顧客在等待或點餐⋯⋯街道中央走著許多行人,有的提著購物袋,有的低頭看手機。
攤車、蒸氣、購物袋、低頭看手機,全部對得上。視覺塔不只是檔案存在,功能也存在。
但這段描述沒有標準答案,所以我又餵了一張有標準答案的,也就是本文上面第三步那張量化階梯的終端機截圖,要它把表格照抄出來。六個關鍵數字全中,包含三個十二位數的 bytes:
| bf16 | 51G | 54,657,734,048 | 15.984 |
| q8_0 | 28G | 29,047,084,448 | 8.494 |
| q4_k_m | 16G | 16,810,714,528 | 4.916 |
1,983 個 token 進、153 個出、24.1 秒。這個測法的好處是,我們已經知道每一格的正確數字該是什麼,不需要對著圖猜模型有沒有在自己編故事或提供幻覺訊息。
有讀者提問,語言模型被我用純文字資料微調過,視覺塔一個參數都沒動,兩邊還接得起來嗎。
接得起來,而且微調學到的行為會完整帶到影像路徑上。第二題我開了思考模式問它這是哪裡的街景,它產出 495 個字的思考,lang_profile 判定 zh-Hant,簡體洩漏 0 個字。資料集 A 全部是純文字題目,從來沒有一筆帶圖,但「用繁體中文思考」的習慣照樣生效。
不過這個案例有一個有趣的地方,它的思考行為正確,但是答題它答錯了。 它說圖片是在中國的海南島,理由是「招牌上有海南字樣」與「賣的是清補涼、竹升麵」。問題是,那張圖是 Day 24 文章中提到,在 DGX Spark 上用 LTX 產生的,圖片風景則是台灣的夜市,招牌文字是合成字形,可以確定的是它判斷本身是錯的,但有他自己的合理理由,這正是視覺模型最容易騙到人的失敗形態。用它去讀有標準答案的東西(截圖、表格、文件)很可靠,用它做開放式的地理或身分判斷就不行了,你要自己檢驗和修正才行。
GGUF 沒有丟掉視覺塔,它只是不把兩件事放在同一個檔案裡,而且拆完之後兩邊都還能用,這樣想想也挺務實的說。
llama-quantize zhwriter-v1-bf16.gguf zhwriter-v1-q8_0.gguf q8_0
llama-quantize zhwriter-v1-bf16.gguf zhwriter-v1-q4_k_m.gguf q4_k_m
llama-quantize 與 llama-imatrix 在我服務用的那款 llama.cpp(PR 27742 分支,commit 6c5afc86a)裡沒有編,補編兩個 target 就好,build cache 還在。刻意在同一款編而不是借用另一款現成的 binary,是為了讓量化與服務走同一個 commit。
| 配方 | 耗時 | 大小 | bytes | BPW |
|---|---|---|---|---|
| bf16 | 82.1 秒(轉檔) | 50.90 GiB | 54,657,734,048 | 15.984 |
| q8_0 | 26.0 秒 | 27.05 GiB | 29,047,084,448 | 8.494 |
| q4_k_m | 80.2 秒 | 15.66 GiB | 16,810,714,528 | 4.916 |
BPW 的分母是實測的 27,356,728,560 個參數,不是 27B 這個標稱值。

imatrix 版沒有做,原因是 Day 12 的四檔階梯我從來沒有自己跑過 llama-quantize,那四款 GGUF 是從 bartowski 直接下載的,同一個量化者、同一份 imatrix,才能當受控比較。今天是第一次在本機量化,所以我的 q4_k_m 沒有 imatrix,雖然 bartowski 那款有,但這兩個數字不能直接比。要做 imatrix 版的話,校準語料只能用訓練組那 12 篇,不能用 Day 12 那份,因為那份是從我全部文章生出來的,會蓋到保留組,PPL 那一列就只好作廢了。
接下來,這是今天的正題。昨天的測試在 Unsloth 的 4-bit 加 adapter 路徑上做,今天同一套探針與測試題目提供給 llama-server,測試模組(zhtw.py)與考卷(exam.py)都是原樣復用,一個字沒改。
| 指標 | 昨天(4-bit 加 adapter) | 合併 BF16 | 合併 q8_0 | 合併 q4_k_m |
|---|---|---|---|---|
| 思考產出完整區塊(100 題) | 99 | 100 | 100 | 100 |
| 思考是繁中 | 83.8% | 87.0% | 87.0% | 89.0% |
| 思考區塊簡體洩漏字數 | 30 | 26 | 26 | 22 |
| 考卷繁中十題 | 20 / 20 | 20 / 20 | 20 / 20 | 20 / 20 |
| 工具呼叫十輪 | 10 / 10 | 10 / 10 | 10 / 10 | 10 / 10 |
| 多步 agent 三題 | 3 / 3 | 3 / 3 | 3 / 3 | 3 / 3 |
| 規範違規(有給規則,65 段) | 1(乾淨 64/65) | 2(乾淨 63/65) | 2(乾淨 63/65) | 3(乾淨 62/65) |
| 繁中 PPL(保留組) | 16.5078 | 12.0912 | 12.0988 | 12.4444 |
| PPL 量測路徑 | transformers W=512 | llama-perplexity -c 512 | 同左 | 同左 |
| decode,考卷(t/s) | 9.65 | 4.49 | 7.78 | 11.81 |
| decode,llama-bench 單串流(t/s) | 不適用 | 4.61 | 7.82 | 12.11 |
測試的部分這邊也先說明一下,不然有人會誤解數字的意涵。
在 PPL 那一列有兩條路徑,只有右邊三欄可以互相比。
昨天是 transformers 走 bnb 4-bit、W=512 視窗、在 21 個 chunk 上算的,今天是 llama-perplexity -c 512 在同一份保留組語料上算的。16.51 對 12.09 不是「變好了 27%」,是兩把不同的尺。同樣的道理,decode 那兩列跨了引擎,昨天是 transformers 單串流,今天是 llama.cpp 在 -np 8 的伺服器上循序量測,只有 token 數的算法一致,三邊都取引擎自報的 completion_tokens。
三個 GGUF 配方之間則是完全受控的。
同一款合併產物、同一支轉檔工具、同一個 commit 的 llama.cpp、同一組貪婪解碼參數、同一份保留組。

答案是沒有,看一下 BF16 那欄對昨天那欄,差異來自那個 q = W16 - dequant(W4)。實測每一項都不比昨天差,完整區塊 99 進步到 100,繁中比率 83.8% 上升到 87.0%,簡體洩漏 30 字降到 26 字,考卷與工具呼叫全滿。
理論上 +q 應該讓模型偏離 adapter 學到的位置,實務上這個偏離小到被淹沒在別的差異裡(引擎不同、取樣實作不同)。對 qwen3_5 這種一般模型,Unsloth 那個「疊回 W16 就好」的預設是對的。
速度那一格稍微有一點不同。q8_0 的 7.78 t/s 比昨天的 9.65 t/s 慢,但那不是合併失敗,是權重從 17 GiB(4-bit)變成 27 GiB(q8_0),而我們這系列的文章都有實測和證明一個許多人都耳熟能詳的概念,也就是推論是頻寬受限的,只要模型檔案權重大就是會慢。
因此,這格真正乾淨的對照組,是 Day 14 量過的底模 q8_0:7.93 t/s,今天的合併版是 7.82 t/s,同一個引擎、同一個量化、同一台機器,差 1.4%。LoRA 的額外矩陣乘法確實消失了,合併本身不花任何推論成本。
要速度就往下走一格,q4_k_m 的 12.11 t/s 比昨天的 4-bit 加 adapter 快 22%,權重檔案大小還差不多。
這是本篇的核心疑問。LoRA 學到的是權重上很小的差量,4-bit 量化的格子很粗,直覺會擔心那個差量被四捨五入掉。我動手前的預期是 q4_k_m 的簡體洩漏會回升,實測則發現微調後的模型要用比底模更保守的量化,類似這樣的結論。
實測推翻了這個預期。 簡體洩漏字數從 BF16 的 26 個,到 q8_0 的 26 個,到 q4_k_m 的 22 個。繁中思考比率從 87.0% 到 87.0% 到 89.0%。往低位元走,微調學到的行為不但沒有掉,量到的還更好一點。
嚴格來看,26 對 22 這個差距在 100 題的樣本上,其實並不足以宣稱 q4_k_m 比較好,那更可能是量化改變了取樣路徑造成的雜訊。但它足以否定「回升」,如果 4-bit 的格子真的會吃掉 LoRA 的差量,26 到 22 應該是 26 到 100 以上才對,Day 25 沒微調的底模是 652 個字,這個差量完全還在。
為什麼呢?因為 LoRA 的差量並不小。合併是 W += 2.0 * B @ A,scale 是 2.0,改的是 256 個模組的完整權重矩陣。量化誤差是相對於合併後的權重算的,它不會特別針對「哪一部分是微調加上去的」去捨棄。畢竟,量化壓縮的是整個矩陣,不是只有壓矩陣的某個成分。
真正被量化吃掉的東西在 PPL 那一列,BF16 的 12.0912,q8_0 的 12.0988(差 0.06%),q4_k_m 的 12.4444(差 2.9%)。這條階梯的形狀跟 Day 12 在底模上量到的一致,q8_0 幾乎無損,q4_k_m 付出約 3% 的困惑度。差別是,今天量的是我自己的保留組文章,而且模型才剛用我的文章訓練過。
編輯規範那一列從 1 個違規變成 2 到 3 個,看起來變差,但這一格從 Day 25 開始就沒什麼空間,底模光靠提示詞就已經 61/65 乾淨。1 對 2 對 3 這個範圍在 65 段上就是雜訊。
上線實作挑的是 q8_0 這款。理由是 PPL:12.0988 對 BF16 的 12.0912 只差 0.06%,而 q4_k_m 是 12.4444,差 2.9%。微調行為三個配方都完整撐住,所以這一格是拿速度換 PPL,不是拿品質換速度。體積從 50.90 GiB 降到 27.05 GiB,decode 從 4.61 升到 7.82,PPL 幾乎沒動,這是模型量化階梯上最划算的一格。在 Day 12 那天文章,其實就有說對底模的結論是「日常用 q8_0」,而這兩天的微調版並沒有推翻這個結論。
然後回到 Day 18 那份閘道設定。這裡有一件事跟我以為的不一樣,zh-writer 這個別名其實還不存在。 Day 18 只示範了「用途命名不用模型命名」這個做法,並沒有真的建過 zh-writer,它只出現在 Day 25 文章最後的明日預告裡。所以這一步需要新增一個區塊:
- model_name: zh-writer
litellm_params:
model: openai/qwen38-27b-zhwriter-v1-q8
api_base: http://127.0.0.1:8080/v1
api_key: none
model_info:
max_input_tokens: 16384
max_output_tokens: 8192
max_output_tokens 刻意小於 max_input_tokens。閘道重載有一個已知的問題,litellm-config.yaml 是 bind mount,compose 檔本身沒變的話 docker compose up -d 只會回一句 Container gb10-proxy Running 然後什麼都不做,proxy 繼續跑舊設定,要 restart 或 --force-recreate。而且 clamp_max_tokens.py 是在 import 時就把設定讀成常數,不重啟連 clamp 的別名表都不會更新。
重載後三段測試都通過了:
[clamp] per-alias ceilings: deepseek-v4-flash=8192, deepseek-v4-pro=8192,
ds4-flash-0731-dspark=pass-through, zh-writer=8192 (default 8192)
/v1/models 出現第四個別名,然後透過 :8100 用 zh-writer 這個名字跑一輪考卷:繁中十題 20/20、工具呼叫 10/10、多步 agent 3/3、decode 7.72 t/s。對直連 :8080 的 7.78 t/s,閘道那一層的開銷是 0.8%。
回滾路徑是把整個區塊註解掉再 restart,而不是改回某個舊目標。註解掉而不是留著加註解,會這樣做的原因請見下一節。
拿今天真的發生的事寫一段原始草稿,丟給 zh-writer 跑看看,302 token 進、158 token 出、20.5 秒,句子重組得也自然,沒有變成機械式的斷句。
但「前沿」那一格沒有改。 這正好是 Day 25 那條限制的現場重演:「frontier 一律譯為尖端或先進」這條規則在我自己的語料裡幾乎不存在,訓練組出現「前沿」0 次、保留組也是 0 次,反向生成器沒有素材可以把它變成違規,228 筆訓練輸入裡只有 4 筆含「前沿」。訓練不到的東西,上線之後一樣訓練不到。 這條規則到今天為止還是要靠提示詞管,而且看起來連提示詞都管不動它。
體感上,這款模型可以直接接進日常辦公室文字流程,前提是必須知道它會尊守的是哪些規則、不守哪些規則。都規範好後,上線就比較不會出問題。
(這一段的「可以」有效期只有幾個小時,下一節會說明為什麼。)
這台機器有 121.63 GiB 統一記憶體。hermes 背後的 DeepSeek-V4-Flash 雙機叢集在 --gpu-memory-utilization 0.835 下起來之後,KV cache 拿到 17.48 GiB(2,578,830 個 token),系統只剩 7.7 GiB 可用。zh-writer 的 q8_0 光權重就是 27.05 GiB。
所以今天整條路線是在 hermes 停機的狀態下跑完的,從合併開始到考卷結束,:8100 的三個 DeepSeek 別名都不沒辦法用。
所以,我接著試著讓兩個共存,調降 DS4-Flash 的記憶體配額,兩次都失敗:
| 配額 | 結果 |
|---|---|
| 0.62 | 連起都起不來。Free memory on device cuda:0 (72.92/121.63 GiB) on startup is less than desired GPU memory utilization (0.62, 75.41 GiB),差 2.5 GiB |
| 0.58 | 過了記憶體檢查,但 KV cache 只分到 24.15 GiB,裝不下 MAX_MODEL_LEN=1048576 的一條序列,ValueError: No available memory for the cache blocks |
第二次那個嘗試還差點把機器弄停擺。權重載完進到 torch.compile 階段時,MemAvailable 掉到 2 GiB,那正是 Day 25 那次「ping 通但 SSH 死、只能按電源鍵」的前兆,我趕快把 llama-server 停掉釋放 24 GiB 才把它救回來。
換 q4_k_m 也不行,hermes 起來後只剩 7.7 GiB,而 q4_k_m 的權重是 15.66 GiB。唯一的共存路徑是把 hermes 的 MAX_MODEL_LEN 從 1,048,576 砍下來,那是另一天的題目。
所以今天的最終狀態是:zh-writer 建好了、驗過了,然後被註解掉,暫時不能用。 litellm-config.yaml 裡留著整個區塊與復原步驟,要用的時候停 hermes、起 llama-server、把五行的註解拿掉、docker compose restart,一分鐘的事。
這裡有一個不能偷懶的細節。死掉的別名必須註解掉,不能只留著加註解:LiteLLM 的 /health 會探測每一個活著的 entry,實測留著會讓 proxy 回報 healthy 3 / unhealthy 1,然後探測 /health 的 agent 會判定閘道整個掛了,註解掉之後回到 healthy 3 / unhealthy 0。
「用途命名不用模型命名」這件事情很重要,可以讓客戶端零改動地自由切換各種模型來測試或日常使用,這張支票今天兌現了。但它兌現的是切換的成本,不是共存的成本,單機的容量沒有因為命名策略而變多,只是你切換比較方便而已。
順帶一提,切換的代價也不只是改設定,把 DS4-Flash 帶回來花了 40 分鐘,因為投機解碼要把 48 個 shard 載兩趟。這個頻繁切換模型的時間成本,個人是還好,辦公室就麻煩了,因此你做這些規劃和決策要完善才行。
如果你的服務引擎是 vLLM,其實可以用上面提到的合併方式,因為 vLLM 支援執行期載入 LoRA adapter(--enable-lora 加 --lora-modules),一份底模掛多個 adapter,請求時指定用哪一個。這對同一款底模、不同客戶的風格的 MSP 場景來說,是很吸引人的用法。
但這樣的用法也是有代價的,其一是併版的 q8_0 是 7.82 t/s,Day 14 的底模 q8_0 是 7.93 t/s,兩者差 1.4%,所以合併省下來的就是 Day 25 量到的 10.08 對 9.65 那個 4.3%。第二個代價是精度會,今天的 adapter 是在 bnb 4-bit 底模上訓的,如果P8 底模上,那個 q 會變成另一個數字,而且沒有人替你檢查。
今天的記憶體預算,用在三個配方的完整測試與視覺塔的功能測試上了,而每多跑一項就要多停 hermes 一次。
微調學到的行為完全撐得過量化,而且撐得比我預期的好。
簡體洩漏字數在 BF16、q8_0、q4_k_m 三個量化級距上是 26、26、22,Day 25 沒微調的底模是 652。我原本準備寫「低位元量化會吃掉微調差量,所以微調後的模型要用比底模更保守的量化」,實測完全不支持這個說法。原因回頭看很直觀:合併之後那個差量已經是權重的一部分,量化壓縮的是整個矩陣,不是矩陣裡「微調加上去的那一份」。被量化吃掉的是困惑度,q8_0 對 BF16 差 0.06%、q4_k_m 差 2.9%,那條階梯的形狀跟底模上量到的一樣。
最終選 q8_0,因為 PPL 幾乎無損而體積只有一半,Day 12 對底模的結論在微調版上沒有被推翻。
回到「128GB 統一記憶體能不能做微調」這個問題,走完 Day 25 到 26 的完整迴圈之後,答案是能,而且瓶頸不在訓練。訓練只花 37 分鐘、峰值 38.43 GiB。今天這一天花掉的時間裡,最久的是三個量化配方的測試(q8_0 與 q4_k_m 各約 46 分鐘,BF16 約兩個半小時),而合併本身只要 45 秒。真正的風險不在容量而在工具鏈:兩條官方路徑都寫出了載不回去的 checkpoint,其中一條還印著成功訊息,所以有些東西自己還是要檢查一遍。
但「能做」跟「能養」是兩件事。
一款 27B 的微調模型在這台機器上可以訓練、可以合併、可以量化、可以上線,就是不能跟既有的生產模型同時活著。今天最終的狀態是 zh-writer 建好、驗過、然後記憶體不夠沒辦法兩個一起跑,因為 hermes 要用掉 113 GiB。單機做微調的真正上限不是你訓練不訓練得動,是你有沒有第二台機器來放訓練出來的東西。
而我剛好有第二台。
第四週的媒體與微調到此完成。明天回到硬體,把系列開頭就埋的那條 100GbE 直連線正式講完,雙 Spark 合體的分散式推論,其實它已經在 Day 21 之後默默服役了一週,明天把維護成本來做計算,那也正是今天「養不起第二款模型」這個問題唯一的實體解法。順便讓 Day 10 那位跟我搏過感情的 TensorRT-LLM 回歸,帶著 2026 IFA 剛發布的新版最佳化再戰一場。
我們 Day 27 見囉。
Day 1|為什麼 2026 年是地端 AI 部署元年:系列規劃與硬體總覽
Day 2|DGX Spark GB10 深度解析:128GB 統一記憶體到底解決了什麼問題
Day 3|網路與儲存規劃:10GbE 骨幹、雙 Spark 直連與 NFS 集中模型庫
Day 4|開箱之後:DGX OS 初始環境建置與 CUDA、Docker 生態確認
Day 5|NFS 模型庫實戰:下載工具、權限設計與版本管理
Day 6|llama.cpp、vLLM、TensorRT-LLM 、Ollama、DS4 與 Unsloth 的定位與取捨
Day 7|第一個模型上線:gpt-oss-120b 從模型庫到 API 的完整流程
Day 8|llama.cpp 實測:先定量尺與 SOP
Day 9|vLLM 實測:官方容器、同時處理吞吐曲線,與剛出爐的新模型 Ornith 1.5
Day 10|TensorRT-LLM 實測:我花了一個下午,跟它的預設值搏感情
Day 11|地端 AI 三大推論引擎綜合評測,同場加映神秘嘉賓
Day 12|量化格式解析:「4-bit」兩個字,古今多少事 ? 都付笑談中
Day 13|Perplexity 把整套 Agent 搬上 DGX Spark:地端元年的官方背書
Day 14|Qwen3.8-27B 與 Flash-Next 加碼實測:地端模型世代對決與落地評估
Day 15|Ornith 1.5 實測:為 AI agent 而生的 35B-A3B
Day 16|模型選型方法論:五個問題幫你跳脫排行榜迷失,找到適合自己任務用的 AI 模型
Day 17|ds4 深度解析:Redis 之父只為一種模型造了一個引擎
Day 18|OpenAI 相容 API 層:地端 AI 多模型常駐好助手
Day 19|地端 agent 框架總覽:它們到底對你的端點做了什麼呢?
Day 20|DeepSeek Harness 實戰:21 萬顆星的外掛式 harness 接上地端
Day 21|herdr 是地端 AI 同時跑多個 AI agent 的最佳解
Day 22|語意錨點對上 prefix cache,FreeToken VS vLLM,以及三個關於統一記憶體的教訓
Day 23|ComfyUI 部署與 NFS 模型庫整合:把集中管理的哲學帶進生圖世界
Day 24|MiniMax-H3 地端影片生成:h3-ui 與四步蒸餾版的三段對決
Day 25|Unsloth 微調實戰:教一款模型用繁體中文思考,並遵守台灣的編輯規範
Day 26|微調的最後一哩路:合併、轉檔、量化、上線
Day 27|雙 DGX Spark GB10 合體與分工合作的部署與實測