VAE stage 3: vectorize direct-conv im2col (VAE decode ~4.7s -> ~3.9s, pixel-identical)
Browse files- profile the direct conv into im2col / gemm / permute (GGML_CONV_PROFILE=1);
im2col and gemm were each ~half, permute small
- rewrite im2col: hoist sy/sx and the padding check out of the inner loop, and
for the interior (dilation 1, whole knl_w run in bounds, F32 src) copy the
contiguous run directly; hoist the F32/F16 store branch out too
-> im2col 1716 ms -> 875 ms, output diff mean 0 / max 0
- add GGML_CONV_PATCH_MB diagnostic knob (capping the im2col buffer to L3 made
no difference, left unlimited)
- add stages/stage3_vae_conv notes; README + build.sh updated
- README.md +10 -0
- build.sh +1 -0
- patches/ggml-0004-conv2d-im2col.patch +157 -0
- stages/stage3_vae_conv/README.md +64 -0
README.md
CHANGED
|
@@ -71,6 +71,13 @@ OpenVINO 的 VAE 解碼約 0.55 s,sd.cpp 約 10 s。第一階段嘗試了 conv
|
|
| 71 |
llamafile 版本把 VAE 解碼從 10.7 s 降到 6.6 s(-38%),加上 conv-direct 後為 5.0 s(-53%)。
|
| 72 |
詳細數據與重現步驟見 `stages/stage1_vae/README.md`。
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
## Pipeline 階段 P2:refiner 重新量化 + flash-attn tile(已完成)
|
| 75 |
|
| 76 |
參考 GGUF 的 refiner 區塊是 Q8_0,是 P1「refiner w13 慢 1.7x」未解之謎的根因。`tools/requant_refiner.py`
|
|
@@ -96,6 +103,7 @@ flash-attn 的 V 打包提升為每頭一次、tile 改 128x128,`FLASH_ATTN_EX
|
|
| 96 |
- `z_image_turbo-Q4_K-ffn13.gguf`:由 `tools/fuse_ffn.py` 把 FFN 的 w1、w3 串接成 w13,模型會自動偵測並使用融合路徑。
|
| 97 |
- `z_image_turbo-Q4_K-ffn13-rq.gguf`:再由 `tools/requant_refiner.py` 把 refiner 的 16 個 attention/FFN 權重從 Q8_0 改成 Q4_K(速度優先、opt-in,輸出與 base 略有差異)。
|
| 98 |
- `GGML_CPU_PROFILE=1`:逐算子計時,輸出到 stderr(僅供分析,預設關閉)。
|
|
|
|
| 99 |
|
| 100 |
## Profile(Q4_K,優化前)
|
| 101 |
|
|
@@ -146,9 +154,11 @@ REPEAT=2 ./SmartTest.sh # 每列跑 2 次取中位數
|
|
| 146 |
- `ggml-0001-cpu-op-profile.patch`:`ggml-cpu.c`(逐算子計時)
|
| 147 |
- `ggml-0002-flash-attn-kpack.patch`:`ops.cpp`(flash-attn K/V 打包提升為每頭一次)
|
| 148 |
- `ggml-0003-flash-attn-tiles.patch`:`common.h`(flash-attn tile 128x128)
|
|
|
|
| 149 |
- `tools/fuse_ffn.py`:把 GGUF 的 FFN w1、w3 串接成 w13(Q4_K/Q8_0 的位元組串接是合法的)。
|
| 150 |
- `tools/requant_refiner.py` + `tools/q4k.c`:把 refiner 區塊的 Q8_0 權重重新量化成 Q4_K。
|
| 151 |
- `stages/stage_p2_refiner/`:refiner 根因分析、requant 與 flash-attn tile 的數據。
|
|
|
|
| 152 |
- `tools/measure_rss.py`:執行指令並回報行程樹的峰值 RSS 與 wall time。
|
| 153 |
- `results/`:`results.csv`、`results.md`、`hardware.txt`、`images/`(每次執行的 512x512 輸出)、`logs/`。
|
| 154 |
|
|
|
|
| 71 |
llamafile 版本把 VAE 解碼從 10.7 s 降到 6.6 s(-38%),加上 conv-direct 後為 5.0 s(-53%)。
|
| 72 |
詳細數據與重現步驟見 `stages/stage1_vae/README.md`。
|
| 73 |
|
| 74 |
+
## VAE 階段 3:conv2d im2col 向量化(已完成)
|
| 75 |
+
|
| 76 |
+
用 `GGML_CONV_PROFILE=1` 拆開 direct conv 的三個階段後發現 im2col 與 GEMM 各佔一半。改寫 im2col:
|
| 77 |
+
把 `sy/sx` 與邊界檢查提出迴圈、對齊的內部區塊直接整段複製(`[ic][ky][kx]` 佈局 + 通道最後的來源),
|
| 78 |
+
im2col 由 1716 ms 降到 875 ms,VAE 解碼 ~4.7 → ~3.9 s,輸出逐像素相同。
|
| 79 |
+
剩下的 GEMM(~2.2 s,F32 llamafile sgemm)才是下一關,需要量化/bf16 AMX 卷積。詳見 `stages/stage3_vae_conv/README.md`。
|
| 80 |
+
|
| 81 |
## Pipeline 階段 P2:refiner 重新量化 + flash-attn tile(已完成)
|
| 82 |
|
| 83 |
參考 GGUF 的 refiner 區塊是 Q8_0,是 P1「refiner w13 慢 1.7x」未解之謎的根因。`tools/requant_refiner.py`
|
|
|
|
| 103 |
- `z_image_turbo-Q4_K-ffn13.gguf`:由 `tools/fuse_ffn.py` 把 FFN 的 w1、w3 串接成 w13,模型會自動偵測並使用融合路徑。
|
| 104 |
- `z_image_turbo-Q4_K-ffn13-rq.gguf`:再由 `tools/requant_refiner.py` 把 refiner 的 16 個 attention/FFN 權重從 Q8_0 改成 Q4_K(速度優先、opt-in,輸出與 base 略有差異)。
|
| 105 |
- `GGML_CPU_PROFILE=1`:逐算子計時,輸出到 stderr(僅供分析,預設關閉)。
|
| 106 |
+
- `GGML_CONV_PROFILE=1`:VAE direct-conv 的 im2col/gemm/permute 階段計時(僅供分析,預設關閉)。
|
| 107 |
|
| 108 |
## Profile(Q4_K,優化前)
|
| 109 |
|
|
|
|
| 154 |
- `ggml-0001-cpu-op-profile.patch`:`ggml-cpu.c`(逐算子計時)
|
| 155 |
- `ggml-0002-flash-attn-kpack.patch`:`ops.cpp`(flash-attn K/V 打包提升為每頭一次)
|
| 156 |
- `ggml-0003-flash-attn-tiles.patch`:`common.h`(flash-attn tile 128x128)
|
| 157 |
+
- `ggml-0004-conv2d-im2col.patch`:`ops.cpp`(VAE direct-conv im2col 向量化 + `GGML_CONV_PROFILE`/`GGML_CONV_PATCH_MB` 診斷)
|
| 158 |
- `tools/fuse_ffn.py`:把 GGUF 的 FFN w1、w3 串接成 w13(Q4_K/Q8_0 的位元組串接是合法的)。
|
| 159 |
- `tools/requant_refiner.py` + `tools/q4k.c`:把 refiner 區塊的 Q8_0 權重重新量化成 Q4_K。
|
| 160 |
- `stages/stage_p2_refiner/`:refiner 根因分析、requant 與 flash-attn tile 的數據。
|
| 161 |
+
- `stages/stage3_vae_conv/`:VAE direct-conv 的階段拆解與 im2col 向量化數據。
|
| 162 |
- `tools/measure_rss.py`:執行指令並回報行程樹的峰值 RSS 與 wall time。
|
| 163 |
- `results/`:`results.csv`、`results.md`、`hardware.txt`、`images/`(每次執行的 512x512 輸出)、`logs/`。
|
| 164 |
|
build.sh
CHANGED
|
@@ -141,6 +141,7 @@ if [[ ${#SELECTED[@]} -gt 0 ]]; then
|
|
| 141 |
apply_patch "$SRC/ggml" "$ROOT/patches/ggml-0001-cpu-op-profile.patch"
|
| 142 |
apply_patch "$SRC/ggml" "$ROOT/patches/ggml-0002-flash-attn-kpack.patch"
|
| 143 |
apply_patch "$SRC/ggml" "$ROOT/patches/ggml-0003-flash-attn-tiles.patch"
|
|
|
|
| 144 |
|
| 145 |
# ------------------------------------------------------------ build
|
| 146 |
for v in "${SELECTED[@]}"; do
|
|
|
|
| 141 |
apply_patch "$SRC/ggml" "$ROOT/patches/ggml-0001-cpu-op-profile.patch"
|
| 142 |
apply_patch "$SRC/ggml" "$ROOT/patches/ggml-0002-flash-attn-kpack.patch"
|
| 143 |
apply_patch "$SRC/ggml" "$ROOT/patches/ggml-0003-flash-attn-tiles.patch"
|
| 144 |
+
apply_patch "$SRC/ggml" "$ROOT/patches/ggml-0004-conv2d-im2col.patch"
|
| 145 |
|
| 146 |
# ------------------------------------------------------------ build
|
| 147 |
for v in "${SELECTED[@]}"; do
|
patches/ggml-0004-conv2d-im2col.patch
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
diff --git a/src/ggml-cpu/ops.cpp b/src/ggml-cpu/ops.cpp
|
| 2 |
+
index bc6918ca..cb67c9b9 100644
|
| 3 |
+
--- a/src/ggml-cpu/ops.cpp
|
| 4 |
+
+++ b/src/ggml-cpu/ops.cpp
|
| 5 |
+
@@ -12,6 +12,7 @@
|
| 6 |
+
#include <algorithm>
|
| 7 |
+
#include <cfloat>
|
| 8 |
+
#include <cmath>
|
| 9 |
+
+#include <cstdlib>
|
| 10 |
+
|
| 11 |
+
struct ggml_fp8_e4m3_cpu {
|
| 12 |
+
uint8_t value;
|
| 13 |
+
@@ -7501,7 +7502,12 @@ static void ggml_compute_forward_conv_2d_impl(const ggml_compute_params * params
|
| 14 |
+
const int64_t patch_total = dst->ne[3] * dst_w * dst_h;
|
| 15 |
+
|
| 16 |
+
const int64_t space_per_patch = knl_n * traits->type_size + c_out * sizeof(float);
|
| 17 |
+
- const int64_t batch_size = params->wsize / space_per_patch;
|
| 18 |
+
+ int64_t conv_budget = params->wsize;
|
| 19 |
+
+ if (const char * env = getenv("GGML_CONV_PATCH_MB")) {
|
| 20 |
+
+ const int64_t cap = (int64_t) atoll(env) * 1024 * 1024;
|
| 21 |
+
+ if (cap > 0 && cap < conv_budget) conv_budget = cap;
|
| 22 |
+
+ }
|
| 23 |
+
+ const int64_t batch_size = conv_budget / space_per_patch;
|
| 24 |
+
const int64_t patches_per_batch = batch_size > 8 ? (batch_size / 8) * 8 : batch_size;
|
| 25 |
+
const int64_t batch_n = (patch_total + patches_per_batch - 1) / patches_per_batch;
|
| 26 |
+
|
| 27 |
+
@@ -7509,6 +7515,9 @@ static void ggml_compute_forward_conv_2d_impl(const ggml_compute_params * params
|
| 28 |
+
|
| 29 |
+
void * tmp = params->wdata;
|
| 30 |
+
|
| 31 |
+
+ const bool conv_prof = getenv("GGML_CONV_PROFILE") != nullptr;
|
| 32 |
+
+ int64_t acc_im2col = 0, acc_gemm = 0, acc_permute = 0;
|
| 33 |
+
+
|
| 34 |
+
for (int64_t batch_i = 0; batch_i < batch_n; ++batch_i) {
|
| 35 |
+
|
| 36 |
+
const int64_t patch_start_batch = batch_i * patches_per_batch;
|
| 37 |
+
@@ -7516,40 +7525,67 @@ static void ggml_compute_forward_conv_2d_impl(const ggml_compute_params * params
|
| 38 |
+
patch_total);
|
| 39 |
+
const int64_t patch_n = patch_end_batch - patch_start_batch;
|
| 40 |
+
|
| 41 |
+
+ int64_t t_phase = conv_prof ? ggml_time_us() : 0;
|
| 42 |
+
const int64_t patch_per_thread = (patch_n + params->nth - 1) / params->nth;
|
| 43 |
+
const int64_t patch_start = patch_start_batch + params->ith * patch_per_thread;
|
| 44 |
+
const int64_t patch_end = std::min(patch_start + patch_per_thread, patch_end_batch);
|
| 45 |
+
|
| 46 |
+
- //im2col for a patch
|
| 47 |
+
+ // im2col for a patch. Layout is [ic][ky][kx] (dst rows contiguous), and the
|
| 48 |
+
+ // common interior case reads knl_w contiguous floats per (ic, ky), so that
|
| 49 |
+
+ // run is a straight copy and no per-element bounds check is needed.
|
| 50 |
+
+ const int64_t khw = knl_h * knl_w;
|
| 51 |
+
+ const bool nb0_f32 = src->nb[0] == (int64_t) sizeof(float);
|
| 52 |
+
for (int64_t p = patch_start; p < patch_end; ++p) {
|
| 53 |
+
- const int64_t batch_n = p / (dst_w * dst_h);
|
| 54 |
+
- const int64_t src_x = (p / dst_w) % dst_h;
|
| 55 |
+
- const int64_t src_y = p % dst_w;
|
| 56 |
+
+ const int64_t batch_n = p / (dst_w * dst_h);
|
| 57 |
+
+ const int64_t src_x = (p / dst_w) % dst_h;
|
| 58 |
+
+ const int64_t src_y = p % dst_w;
|
| 59 |
+
|
| 60 |
+
- const float * src_base = (const float *)((const char *)src_data + batch_n * src->nb[3]);
|
| 61 |
+
- char * dst_row = (char *) tmp + (p % patches_per_batch) * knl_n * traits->type_size;
|
| 62 |
+
+ const char * src_base = (const char *)src_data + batch_n * src->nb[3];
|
| 63 |
+
+ char * dst_row = (char *) tmp + (p % patches_per_batch) * knl_n * traits->type_size;
|
| 64 |
+
+
|
| 65 |
+
+ const int64_t sy0 = src_x * stride_y - pad_y;
|
| 66 |
+
+ const int64_t sx0 = src_y * stride_x - pad_x;
|
| 67 |
+
+ const bool x_fast = nb0_f32 && dilation_x == 1 && sx0 >= 0 && sx0 + knl_w <= src_w;
|
| 68 |
+
|
| 69 |
+
for (int64_t ic = 0; ic < c_in; ++ic) {
|
| 70 |
+
+ const char * src_ic = src_base + ic * src->nb[2];
|
| 71 |
+
+ char * dst_ic = dst_row + ic * khw * traits->type_size;
|
| 72 |
+
for (int64_t ky = 0; ky < knl_h; ++ky) {
|
| 73 |
+
- for (int64_t kx = 0; kx < knl_w; ++kx) {
|
| 74 |
+
- const int64_t sy = src_x * stride_y + ky * dilation_y - pad_y;
|
| 75 |
+
- const int64_t sx = src_y * stride_x + kx * dilation_x - pad_x;
|
| 76 |
+
-
|
| 77 |
+
- int64_t dst_idx = ic * (knl_h * knl_w) + ky * knl_w + kx;
|
| 78 |
+
+ const int64_t sy = sy0 + ky * dilation_y;
|
| 79 |
+
+ char * drow = dst_ic + ky * knl_w * traits->type_size;
|
| 80 |
+
|
| 81 |
+
- float src_val;
|
| 82 |
+
- if (sy < 0 || sy >= src_h || sx < 0 || sx >= src_w) {
|
| 83 |
+
- src_val = 0.0f;
|
| 84 |
+
+ if (sy < 0 || sy >= src_h) {
|
| 85 |
+
+ if (kernel_type == GGML_TYPE_F32) {
|
| 86 |
+
+ memset(drow, 0, knl_w * sizeof(float));
|
| 87 |
+
} else {
|
| 88 |
+
- const float * src_ptr = (const float *)((const char *)src_base + sx * src->nb[0] + sy * src->nb[1] + ic * src->nb[2]);
|
| 89 |
+
- src_val = *src_ptr;
|
| 90 |
+
+ ggml_fp16_t * dp = (ggml_fp16_t *) drow;
|
| 91 |
+
+ for (int64_t kx = 0; kx < knl_w; ++kx) dp[kx] = 0;
|
| 92 |
+
}
|
| 93 |
+
+ continue;
|
| 94 |
+
+ }
|
| 95 |
+
+
|
| 96 |
+
+ const char * src_row = src_ic + sy * src->nb[1];
|
| 97 |
+
|
| 98 |
+
- char * element_ptr = dst_row + dst_idx * traits->type_size;
|
| 99 |
+
+ if (x_fast) {
|
| 100 |
+
+ const float * sp = (const float *) src_row + sx0;
|
| 101 |
+
if (kernel_type == GGML_TYPE_F32) {
|
| 102 |
+
- *(float *) element_ptr = src_val;
|
| 103 |
+
- } else if (kernel_type == GGML_TYPE_F16) {
|
| 104 |
+
- *(ggml_fp16_t *) element_ptr = GGML_CPU_FP32_TO_FP16(src_val);
|
| 105 |
+
+ memcpy(drow, sp, knl_w * sizeof(float));
|
| 106 |
+
+ } else {
|
| 107 |
+
+ ggml_fp16_t * dp = (ggml_fp16_t *) drow;
|
| 108 |
+
+ for (int64_t kx = 0; kx < knl_w; ++kx) dp[kx] = GGML_CPU_FP32_TO_FP16(sp[kx]);
|
| 109 |
+
+ }
|
| 110 |
+
+ } else {
|
| 111 |
+
+ for (int64_t kx = 0; kx < knl_w; ++kx) {
|
| 112 |
+
+ const int64_t sx = sx0 + kx * dilation_x;
|
| 113 |
+
+ float src_val = 0.0f;
|
| 114 |
+
+ if (sx >= 0 && sx < src_w) {
|
| 115 |
+
+ src_val = *(const float *)(src_row + sx * src->nb[0]);
|
| 116 |
+
+ }
|
| 117 |
+
+ if (kernel_type == GGML_TYPE_F32) {
|
| 118 |
+
+ ((float *) drow)[kx] = src_val;
|
| 119 |
+
+ } else {
|
| 120 |
+
+ ((ggml_fp16_t *) drow)[kx] = GGML_CPU_FP32_TO_FP16(src_val);
|
| 121 |
+
+ }
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
@@ -7558,6 +7594,8 @@ static void ggml_compute_forward_conv_2d_impl(const ggml_compute_params * params
|
| 126 |
+
|
| 127 |
+
ggml_barrier(params->threadpool);
|
| 128 |
+
|
| 129 |
+
+ if (conv_prof) { const int64_t t = ggml_time_us(); acc_im2col += t - t_phase; t_phase = t; }
|
| 130 |
+
+
|
| 131 |
+
float * gemm_output = (float *) ((char *) tmp + patches_per_batch * knl_n * traits->type_size);
|
| 132 |
+
|
| 133 |
+
GGML_ASSERT(gemm_output + patch_n * c_out <= (float*)tmp + params->wsize);
|
| 134 |
+
@@ -7567,6 +7605,8 @@ static void ggml_compute_forward_conv_2d_impl(const ggml_compute_params * params
|
| 135 |
+
|
| 136 |
+
ggml_barrier(params->threadpool);
|
| 137 |
+
|
| 138 |
+
+ if (conv_prof) { const int64_t t = ggml_time_us(); acc_gemm += t - t_phase; t_phase = t; }
|
| 139 |
+
+
|
| 140 |
+
|
| 141 |
+
//permute back [OC, N, OH, OW] to [N, OC, OH, OW]
|
| 142 |
+
const int64_t permute_per_thread = (patch_n + params->nth - 1) / params->nth;
|
| 143 |
+
@@ -7585,6 +7625,14 @@ static void ggml_compute_forward_conv_2d_impl(const ggml_compute_params * params
|
| 144 |
+
*dst_ptr = value;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
+ if (conv_prof) { const int64_t t = ggml_time_us(); acc_permute += t - t_phase; }
|
| 148 |
+
+ }
|
| 149 |
+
+
|
| 150 |
+
+ if (conv_prof && params->ith == 0) {
|
| 151 |
+
+ fprintf(stderr, "CONVPROF out=%lldx%lld in_c=%lld k=%lldx%lld im2col_us=%lld gemm_us=%lld permute_us=%lld\n",
|
| 152 |
+
+ (long long) dst_w, (long long) dst_h, (long long) c_in,
|
| 153 |
+
+ (long long) knl_w, (long long) knl_h,
|
| 154 |
+
+ (long long) acc_im2col, (long long) acc_gemm, (long long) acc_permute);
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
stages/stage3_vae_conv/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 3 — VAE conv2d im2col (sd.cpp direct conv)
|
| 2 |
+
|
| 3 |
+
Follows `stage1_vae`. After stage 1 the VAE decode (512px, `llamafile` + `conv-direct`)
|
| 4 |
+
is ~4.3 s. This stage profiles the direct conv internally and removes the scalar
|
| 5 |
+
im2col overhead.
|
| 6 |
+
|
| 7 |
+
Config: `llamafile` build, `SD_CPU_EXTRA_BUFT=1`, `--conv-direct auto,vae=on`,
|
| 8 |
+
Q4_K-ffn13-rq, 512x512, 8 steps.
|
| 9 |
+
|
| 10 |
+
## 1. Phase split inside `ggml_compute_forward_conv_2d_impl`
|
| 11 |
+
|
| 12 |
+
`GGML_CONV_PROFILE=1` (env-gated, added in `patches/ggml-0004-conv2d-im2col.patch`)
|
| 13 |
+
prints per-call `im2col` / `gemm` / `permute` microseconds. Summed over all VAE convs,
|
| 14 |
+
1-step run:
|
| 15 |
+
|
| 16 |
+
| phase | before | after |
|
| 17 |
+
|---|---|---|
|
| 18 |
+
| im2col (patch build) | 1716 ms | **875 ms** |
|
| 19 |
+
| GEMM (llamafile sgemm) | 2179 ms | 2238 ms |
|
| 20 |
+
| permute (OC-scattered store) | 202 ms | 216 ms |
|
| 21 |
+
| **VAE decode** | **~4.7 s** | **~3.9 s** |
|
| 22 |
+
|
| 23 |
+
## 2. What changed in im2col
|
| 24 |
+
|
| 25 |
+
The old inner loop recomputed `sy`/`sx` and the padding bounds check **per element**
|
| 26 |
+
(`ic × ky × kx`), with a `kernel_type` branch inside the innermost iteration. The
|
| 27 |
+
patch layout is `[ic][ky][kx]` (dst rows contiguous) and the source is channel-last,
|
| 28 |
+
so for the common interior case each `(ic, ky)` reads `knl_w` **contiguous** floats.
|
| 29 |
+
The rewrite:
|
| 30 |
+
|
| 31 |
+
- hoists `sy0 = src_x*stride_y - pad_y`, `sx0 = src_y*stride_x - pad_x` per patch;
|
| 32 |
+
- adds a fast path (dilation 1, whole `knl_w` run in-bounds, F32 source) that copies
|
| 33 |
+
the run directly, no per-element bounds check;
|
| 34 |
+
- keeps a general fallback for padding / dilation / non-F32 source;
|
| 35 |
+
- hoists the F32/F16 store branch out of the innermost loop.
|
| 36 |
+
|
| 37 |
+
Output is **pixel-identical** to the previous build (diff mean 0, max 0).
|
| 38 |
+
|
| 39 |
+
The same patch adds two diagnostic env knobs (no effect when unset):
|
| 40 |
+
`GGML_CONV_PROFILE=1` (phase timing) and `GGML_CONV_PATCH_MB=<n>` (cap the im2col
|
| 41 |
+
batch buffer; capping to L3 size made no difference, so it is left unlimited).
|
| 42 |
+
|
| 43 |
+
## 3. Remaining VAE cost
|
| 44 |
+
|
| 45 |
+
The GEMM is now the largest phase (~2.2 s, ~0.7 TFLOPS effective) and runs through the
|
| 46 |
+
F32 llamafile sgemm. im2col is ~0.9 s. OpenVINO still decodes this VAE in ~0.3 s
|
| 47 |
+
because it runs INT8 AMX. Closing that gap needs a quantized/bf16 conv GEMM, not more
|
| 48 |
+
im2col tuning:
|
| 49 |
+
|
| 50 |
+
- flatten the 3x3 conv weights to a 2-D `[K=KW*KH*IC, N=OC]` layout at load time so
|
| 51 |
+
they can live in the AMX weight buffer, then reuse the AMX int8/bf16 GEMM;
|
| 52 |
+
- or a Winograd F(2x2,3x3) transform to cut both the multiply count and the 9x im2col
|
| 53 |
+
expansion.
|
| 54 |
+
|
| 55 |
+
## Reproduce
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
./build.sh llamafile # includes patches/ggml-0004-conv2d-im2col.patch
|
| 59 |
+
GGML_CONV_PROFILE=1 SD_CPU_EXTRA_BUFT=1 work/bin/llamafile/sd-cli \
|
| 60 |
+
--diffusion-model work/models/z_image_turbo-Q4_K-ffn13-rq.gguf \
|
| 61 |
+
--vae work/models/ae.safetensors --llm work/models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf \
|
| 62 |
+
-p "a cat" -W 512 -H 512 --steps 1 --seed 42 -t 16 \
|
| 63 |
+
--conv-direct auto,vae=on -o out.png
|
| 64 |
+
```
|