HelloSun commited on
Commit
26151cf
·
1 Parent(s): 111541a

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 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
+ ```