sdcpp_zimage_opt
stable-diffusion.cpp 在 純 CPU 上跑 Z-Image-Turbo 的優化實驗。這個 repo 放的是建置腳本、測速腳本、patch 與結果,不含模型權重(腳本會從上游下載)。
上游 sd.cpp 版本固定為 commit 77b72a2efed78f55de26892c7e909cee001e8b83。
結論
- 同一提示詞、同一 seed、512x512、8 steps。sd.cpp 最快的組合是 Q4_K-ffn13 + AMX 權重佈局(54.2 s),相對 stock(59.7 s)約快 1.10x。
- OpenVINO INT4(同樣 8 steps)總生成 37.5 s,是本批次最快的一列(約 1.59x stock),峰值 RSS 12.6 GiB。但它是不同的權重與實作(optimum-intel + OpenVINO),像素與 sd.cpp 不可直接比較品質。
- 單獨啟用 AMX 權重佈局(
SD_CPU_EXTRA_BUFT=1,Q4_K)相對amx_lto約快 15–20%,是 sd.cpp 內最主要的收益來源。 - AVX512 相對 AVX2 約快 2 倍(118.5 s → 58.1 s)。
- FFN gate/up 融合(w1 + w3 → w13)輸出與原版逐位元相同,但在這台機器上沒有可量測的加速。
- Refiner 慢 2 倍的根因:參考 Q4_K GGUF 把 2 個 noise-refiner 與 2 個 context-refiner 存成 Q8_0(主層是 Q4_K)。在記憶體受限的 CPU GEMM 上,同形狀 w13 每次 89.4 ms(Q8_0)→ 42.9 ms(Q4_K)。用
tools/requant_refiner.py只把這 16 個 refiner 的 attention/FFN 權重改成 Q4_K,每 8 步約省 1.5 s(-rq模型,屬 opt-in,像素差 mean 6.4)。 - flash-attn tiling:tile 由 64x64 改為 128x128 並把 V 打包提升為每頭一次,
FLASH_ATTN_EXT4663 → 4451 ms(-4.5%),輸出逐像素相同。 - Q4_K 是 sd.cpp 在此 CPU 上最快的量化格式。Q8_0 即使開 AMX 也比 Q4_K 慢。
- 峰值 RSS:sd.cpp 7.7–11.1 GiB,OpenVINO 12.6 GiB,皆遠低於記憶體守衛上限。
雜訊說明:同一 stock 設定在不同批次的總生成時間為 56.8 s(前次)與 59.7 s(本次)。排名相近的列(例如 Q4_K-ffn13+AMXw 與 Q4_K+AMXw)差距在 ±5% 內,不宜過度解讀。 負載說明:主機是共用環境,co-tenant 負載會讓所有絕對時間一起上下移動(本 session 觀察到 OpenVINO 從 37.5 s 到 25 s、sd.cpp 從 46 s 到 36 s)。因此跨批次只能比比值或在同一批交錯量測;表中的絕對秒數是記錄當時負載下的結果,不是跨批次可比的定值。
比較表(中位數,n = 2)
| # | 版本 (variant) | 模型 | 額外環境變數 | 採樣 (s) | 總生成 (s) | 相對 stock | 峰值 RSS (GiB) | 與 stock 像素差 (mean/max) |
|---|---|---|---|---|---|---|---|---|
| 1 | stock | Q4_K | - | 49.2 | 59.7 | 1.00x | 7.86 | ref |
| 2 | avx2 | Q4_K | - | 106.7 | 118.5 | 0.50x | 7.86 | 3.18 / 241 |
| 3 | avx512 | Q4_K | - | 47.6 | 58.1 | 1.03x | 7.86 | 3.17 / 239 |
| 4 | amx_lto | Q4_K | - | 52.4 | 63.7 | 0.94x | 7.86 | 0.00 / 0 |
| 5 | amx_lto | Q4_K | SD_CPU_EXTRA_BUFT=1 | 42.0 | 52.7 | 1.13x | 8.16 | 1.28 / 227 |
| 6 | amx_lto | Q4_K-ffn13 | - | 46.5 | 57.1 | 1.05x | 7.86 | 0.00 / 0 |
| 7 | amx_lto | Q4_K-ffn13 | SD_CPU_EXTRA_BUFT=1 | 43.1 | 54.2 | 1.10x | 8.14 | 1.28 / 227 |
| 8 | amx_lto | Q4_0 | - | 172.5 | 183.5 | 0.33x | 7.69 | 22.84 / 252 |
| 9 | amx_lto | Q8_0 | - | 161.4 | 172.0 | 0.35x | 10.38 | 12.51 / 249 |
| 10 | amx_lto | Q8_0 | SD_CPU_EXTRA_BUFT=1 | 55.9 | 66.4 | 0.90x | 11.11 | 12.33 / 249 |
| 11 | openvino | OV-INT4 | - | 36.8 | 37.5 | 1.59x | 12.59 | 27.71 / 254 |
| 12 | llamafile | Q4_K-ffn13 | SD_CPU_EXTRA_BUFT=1 | 39.1 | 46.0 | 1.30x | 8.14 | 1.89 / 227 |
| 13 | llamafile | Q4_K-ffn13 | SD_CPU_EXTRA_BUFT=1 + conv-direct | 40.5 | 45.9 | 1.30x | 7.23 | 1.89 / 227 |
n = 2 run(s) per row (median). Speedup = stock-Q4_K generate time / row generate time.
- 「相對 stock」= stock 的總生成時間 / 該列的總生成時間,大於 1 表示更快。
- OpenVINO 列為 8 steps(與 sd.cpp 相同步數);它的「像素差」是與 stock 的差異,反映的是不同實作,不是品質差距。
- 「像素差」只對權重相同的列有意義。第 6 列與第 1 列完全相同,表示 FFN 融合沒有改變輸出。第 5、7 列是 AMX 的數值差異(平均差 1.28),視覺上與 stock 相同。Q4_0、Q8_0 使用不同權重,差異大是預期的,不代表品質問題。
- AVX2 與 AVX512 的輸出與 stock 不同,原因是浮點運算順序不同。
Pipeline 階段 P1:flash-attn K 打包(已完成,優先執行)
先量測而非猜測:矩陣乘在 pipeline 內與微基準一致,沒有額外開銷;pipeline 的額外開銷是非矩陣乘部分(約 1.4 s/步),主要是 flash-attn。
把 K 的轉置打包改為每個頭只做一次,FLASH_ATTN 從 1607 ms 降到 1226 ms(-24%),輸出與修改前逐像素相同。
詳見 stages/stage_p1_pipe/README.md。
第一階段:VAE 解碼(已完成)
OpenVINO 的 VAE 解碼約 0.55 s,sd.cpp 約 10 s。第一階段嘗試了 conv-direct、VAE tiling、F16 VAE 權重與 llamafile sgemm。
llamafile 版本把 VAE 解碼從 10.7 s 降到 6.6 s(-38%),加上 conv-direct 後為 5.0 s(-53%)。
詳細數據與重現步驟見 stages/stage1_vae/README.md。
VAE 階段 3:conv2d im2col 向量化(已完成)
用 GGML_CONV_PROFILE=1 拆開 direct conv 的三個階段後發現 im2col 與 GEMM 各佔一半。改寫 im2col:
把 sy/sx 與邊界檢查提出迴圈、對齊的內部區塊直接整段複製([ic][ky][kx] 佈局 + 通道最後的來源),
im2col 由 1716 ms 降到 875 ms,VAE 解碼 4.7 → ~3.9 s,輸出逐像素相同。
剩下的 GEMM(2.2 s,F32 llamafile sgemm)才是下一關,需要量化/bf16 AMX 卷積。詳見 stages/stage3_vae_conv/README.md。
Pipeline 階段 P2:refiner 重新量化 + flash-attn tile(已完成)
參考 GGUF 的 refiner 區塊是 Q8_0,是 P1「refiner w13 慢 1.7x」未解之謎的根因。tools/requant_refiner.py
只改 16 個 refiner attention/FFN 權重為 Q4_K(adaLN 保持 Q8_0),同形狀每次 89.4 → 42.9 ms,每 8 步省約 1.5 s。
flash-attn 的 V 打包提升為每頭一次、tile 改 128x128,FLASH_ATTN_EXT 再降 4.5%,且輸出逐像素相同。
詳見 stages/stage_p2_refiner/README.md。
各版本的意義
| variant | CMake 設定 | 說明 |
|---|---|---|
stock |
-DGGML_NATIVE=ON |
上游預設(-march=native) |
avx2 |
GGML_NATIVE=OFF,只開 AVX2,GGML_AVX512=OFF |
量測 AVX512 的貢獻 |
llamafile |
amx_lto + GGML_LLAMAFILE=ON(F32 sgemm,第一階段) |
VAE 與 F32 矩陣乘的 CPU 內核 |
avx512 |
GGML_NATIVE=OFF,明確開 AVX512/VNNI/BF16/AMX,LTO |
與 native 版本效能接近(60.0 s vs 56.8 s,差距在雜訊附近) |
amx_lto |
GGML_NATIVE=ON,AMX_TILE/INT8/BF16,LTO |
目前最佳的編譯選項 |
| openvino | HelloSun/Z-Image-Turbo-OpenVINO-INT4,tools/ov_bench.py(optimum-intel 2.2.0 / OpenVINO 2026.4.0) | 不同推論框架與 INT4 權重,只作為對照,步數與 sd.cpp 相同(8) |
執行期的選項:
SD_CPU_EXTRA_BUFT=1:擴散模型的 2 維權重改用 CPU 的 AMX 佈局(實驗性質,預設關閉)。z_image_turbo-Q4_K-ffn13.gguf:由tools/fuse_ffn.py把 FFN 的 w1、w3 串接成 w13,模型會自動偵測並使用融合路徑。z_image_turbo-Q4_K-ffn13-rq.gguf:再由tools/requant_refiner.py把 refiner 的 16 個 attention/FFN 權重從 Q8_0 改成 Q4_K(速度優先、opt-in,輸出與 base 略有差異)。GGML_CPU_PROFILE=1:逐算子計時,輸出到 stderr(僅供分析,預設關閉)。GGML_CONV_PROFILE=1:VAE direct-conv 的 im2col/gemm/permute 階段計時(僅供分析,預設關閉)。
Profile(Q4_K,優化前)
| 算子 | 佔比 | 備註 |
|---|---|---|
| MUL_MAT | 74% | 主要瓶頸,AMX 佈局主要影響這一項 |
| FLASH_ATTN_EXT | 11% | 已融合 |
| IM2COL | 7% | VAE 卷積 |
| FUSED_RMS_NORM_MUL | 2% | ggml 既有融合 |
| 其他逐元素算子 | 約 5% | 融合的理論上限很低 |
硬體配置
- CPU:Intel Xeon Platinum 8559C,2 sockets × 48 cores,SMT 2,2 個 NUMA node
- 有效 CPU 能力:容器 cgroup 配額 16 CPU(
cpu.max = 1600000 100000),測試綁定 CPU0-15(NUMA node 0),-t 16 - 相關指令集:AVX2、AVX512F、AVX512_VNNI、AVX512_BF16、AMX_INT8、AMX_BF16
- 記憶體:1999 GiB(主機共用,測試時 load average 約 45,結果有 ±3 s 左右的雜訊)
- GPU:無可用(NVML 初始化失敗),純 CPU 推論
- OS:Debian GNU/Linux 13,kernel 6.18
- 編譯器:gcc 14.2.0,cmake 3.31.6
完整資訊見 results/hardware.txt。
重現
# 1. 安裝套件、拉釘選版本的 sd.cpp、套用 patch、建置四個變體、下載模型、融合 FFN
./build.sh --all # 或只建置指定變體:./build.sh amx_lto avx512
./build.sh --list # 查看變體與對應的 CMake 參數
# 2. 用同一組提示詞測速,輸出 results/results.csv、results/results.md
REPEAT=2 ./SmartTest.sh # 每列跑 2 次取中位數
build.sh會建立./work/(原始碼、binary、模型、Python venv)。模型下載約 14 GB。- 模型來源:
leejet/Z-Image-Turbo-GGUF:z_image_turbo-Q4_K.gguf、z_image_turbo-Q8_0.gguf、z_image_turbo-Q4_0.ggufunsloth/Qwen3-4B-Instruct-2507-GGUF:Qwen3-4B-Instruct-2507-Q4_K_M.gguf(文字編碼器)ffxvs/vae-flux:ae.safetensors(FLUX.1 VAE 的鏡像;官方black-forest-labs/FLUX.1-schnell為 gated)
SmartTest.sh的矩陣定義在腳本內的MATRIX陣列,可自行增減。
內容
build.sh:一鍵建置。釘選 sd.cpp commit,套用 patch,依參數建置指定變體。SmartTest.sh:測速與報告。記錄採樣時間、總生成時間、峰值 RSS 與像素差。patches/sdcpp-0001-zimage-ffn-w13-and-cpu-extra-buft.patch:z_image.hpp(FFN w13 融合)、model_manager.cpp(AMX 權重佈局)ggml-0001-cpu-op-profile.patch:ggml-cpu.c(逐算子計時)ggml-0002-flash-attn-kpack.patch:ops.cpp(flash-attn K/V 打包提升為每頭一次)ggml-0003-flash-attn-tiles.patch:common.h(flash-attn tile 128x128)ggml-0004-conv2d-im2col.patch:ops.cpp(VAE direct-conv im2col 向量化 +GGML_CONV_PROFILE/GGML_CONV_PATCH_MB診斷)
tools/fuse_ffn.py:把 GGUF 的 FFN w1、w3 串接成 w13(Q4_K/Q8_0 的位元組串接是合法的)。tools/requant_refiner.py+tools/q4k.c:把 refiner 區塊的 Q8_0 權重重新量化成 Q4_K。stages/stage_p2_refiner/:refiner 根因分析、requant 與 flash-attn tile 的數據。stages/stage3_vae_conv/:VAE direct-conv 的階段拆解與 im2col 向量化數據。tools/measure_rss.py:執行指令並回報行程樹的峰值 RSS 與 wall time。results/:results.csv、results.md、hardware.txt、images/(每次執行的 512x512 輸出)、logs/。
注意事項與限制
- 實驗性質:
SD_CPU_EXTRA_BUFT=1會改變數值(平均像素差 1.28),預設關閉。 - 融合效益有限:FFN 融合的 MUL_MAT 呼叫數減少 272 次(1948 → 1676),但時間沒有改善,在這台機器上屬於「正確但沒有實益」。
- Q8_0 與 Q4_0:在此 CPU 上比 Q4_K 慢,即使 AMX 也一樣。不建議用於速度優先的情境。
- 雜訊:主機為共用環境,每列跑 2 次取中位數。個別執行的差異約 ±3 s。
- 歷史數據:早期未綁核、預設 96 執行緒的 baseline 為 137.7 s,不在上表中,因為執行條件不同。
- 像素差的意義:只有權重相同的列可以直接比較輸出是否改變。
- 模型不包含在此 repo,請依上游授權使用。Z-Image-Turbo 量化檔的授權為 Apache-2.0;FLUX.1 VAE 依 FLUX.1-schnell 為 Apache-2.0;sd.cpp 為 MIT。