Repository navigation
CPU: ik_llama.cpp GEMM kernels for K-quant and I-quant weights on ARM (Q2_K/Q3_K DiTs 1.5x, IQ text encoders up to 2.6x) - #48
danielhanchen wants to merge 8 commits into
Conversation
…tches/0018) Ports the classic iqk GEMM kernels of ik_llama.cpp (commit 009dce19, MIT, Iwan Kawrakow) into ggml-cpu as src/ggml-cpu/iqk/: Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, IQ4_XS, IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S, IQ1_S and IQ1_M weights times Q8_K activations, for AVX2, AVX512 (F/BW/DQ/VL/VNNI) and NEON with dotprod. They read the upstream block layouts and the Q8_K rows ggml already makes, form the same integer products as vec_dot and only accumulate the floats in another order; ik's re-quantizing Q8_K_R8 conversion path is not ported. Several activation rows share each weight block decode (up to 8), instead of one row by one column per vec_dot call. Hooked in ggml_compute_forward_mul_mat after src1 is converted to Q8_K, in place of the vec_dot loop. On x86 the tiled K-quant path keeps taking ne11 >= 8 first; on ARM, which has no tiled path, these kernels take every ne11. Variants without AVX2 (or NEON dotprod) and MSVC builds compile a stub and keep vec_dot. Repacked weights do not reach this code. GGML_CPU_IQK=0 disables, GGML_CPU_IQK=2 runs them ahead of the tiled path, GGML_CPU_IQK_TYPES=q2_K,iq2_xs,... restricts the types, GGML_CPU_IQK_DEBUG=1 logs the first use per type.
…64 kernels (no lax vector conversions)
…path measured faster), on for ARM
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Image accuracy, ARM (GitHub ubuntu-24.04-arm, Neoverse N2), where this PR is on by default. LPIPS (alex), mean of 2 seeds, 256 px, Studio step counts (Z-Image 8, klein 4). Reference = the same build with BF16 weights for the part under test.
|
test-backend-ops MUL_MAT perf (m 4096, k 14336) on Zen 3, Zen 4, Zen 5, Xeon 8370C / 8573C and Core i7-8700B: iqk is 1.07-3.2x faster than vec_dot for 2-7 src1 rows and 1.15-2.9x faster than the tiled path at 8 rows (not iq2_xxs / iq3_s, 0.84x / 0.91x on Zen 5), and 1.01-2.3x for K-quants up to 12 rows; tiled wins from 16 rows up and vec_dot at 1 row. x86 now takes iqk exactly there when GGML_CPU_IQK is unset; GGML_CPU_IQK=0 / 1 / 2 keep their meaning.
Stacked on #47 (
cpu-sgemm-f16-bf16, itself on #42). Adds one ggml patch,scripts/unsloth/ggml-patches/0018-ggml-cpu-iqk-gemm.patch. No sd.cpp source change and no ggml submodule bump.Problem
On ARM CPUs (Linux aarch64, Apple Silicon when a matmul stays on the CPU backend), every K-quant and I-quant weight without a repack runs ggml's
vec_dot, one row times one column per call: Q2_K, Q3_K, IQ4_XS, IQ3_S / IQ3_XXS, IQ2_S / IQ2_XS / IQ2_XXS, IQ1_S / IQ1_M, plus Q4_K / Q5_K / Q6_K whenever they are not repacked. x86 does not have this problem: the pinned ggml already has an exact tiled K-quant / I-quant GEMM there (src/ggml-cpu/tiled/) for ne11 >= 8.Fix
Port the GEMM kernels of ik_llama.cpp (commit 009dce19, MIT, Iwan Kawrakow) into
src/ggml-cpu/iqk/, for Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, IQ4_XS, IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S, IQ1_S and IQ1_M weights times the Q8_K activations ggml already makes:vec_dotare ported. Only the float accumulation order differs. ik's on-the-fly re-quantization of weights to Q8_K_R8 is not ported (it rounds the weights a second time).ggml_compute_forward_mul_matafter src1 is converted to Q8_K, next to the llamafile hook. Repacked and BLAS-routed weights never reach it.Defaults and switches (read once in
ggml_cpu_init):vec_dot(1 row); see belowGGML_CPU_IQK=0GGML_CPU_IQK=1GGML_CPU_IQK=2GGML_CPU_IQK_TYPES=q2_K,iq2_xxs,...narrows the types,GGML_CPU_IQK_DEBUG=1prints the enabled types and the first call per type.Evidence
Speed, ARM: GitHub ubuntu-24.04-arm (Neoverse N2, 4 cores,
armv8.6_2variant), 2 runnersSame runner, same binary for
headandGGML_CPU_IQK=0, 3 reps, median. The kill switch renders the base pixels exactly and matches base time, so it is the A/A.Kernel micro-benchmark (mul_mat, K x N x M = 3840 x 3840 x 1024 for K-quants, 2560 x 9728 x 512 for I-quants, plain buffers, same runners): q2_K 2.18x, q3_K 1.80x, q4_K 1.21x, q5_K 2.29x, q6_K 1.24x, iq4_xs 1.46x, iq3_s 3.24x, iq3_xxs 2.95x, iq2_s 2.18x, iq2_xs 2.14x, iq2_xxs 2.80x, iq1_s 2.82x, iq1_m 2.67x. Both runners agree to within 1%.
Apple M1 (GitHub macos-15 VM, 3 threads):
test-backend-ops perfiqk on vs off 1.10x (q4_K) to 2.93x (iq3_xxs). In the default Mac setup Accelerate BLAS still takes the large non-repacked matmuls, so Mac render times are unchanged; withSD_CPU_BLAS=0the IQ text encoders run 1.3-1.8x faster than through Accelerate.Speed, x86: small batches only
GitHub runners (Zen 3, 2x Zen 4, Zen 5, Xeon 8573C) and Colab (Broadwell, Zen 2), same-machine A/B. The tiled path already runs these types 1.5-25x faster than
vec_doton large batches. Running the iqk kernels ahead of it for every batch (GGML_CPU_IQK=2) was 5-10% slower on Zen 3, 2-4x slower on the AVX512 parts, and a tie within the A/A spread on Broadwell / Zen 2. Example, Zen 4: Z-Image Q5_K_M sampling 26.0 s (tiled) vs 44.3 s (iqk first).Small batches are different.
test-backend-ops perf -o MUL_MAT(m 4096, k 14336, all 13 types) on Zen 3 (EPYC 7763, three runners), Zen 4 (EPYC 9V74), Zen 5 (EPYC 9V45), Xeon 8370C / 8573C and Core i7-8700B, iqk vs the path that runs otherwise:vec_dot(iq1_m / iq2_s / iq2_xxs slower everywhere)vec_dotvec_dot(one 0.96x: q6_K at 2 rows on Zen 4)These are the modulation / embedding matmuls (one or two rows with batched CFG) and short prompts; sampling of SD1.5 Q3_K at 256 px is unchanged by it (20.95 vs 20.88 s on Zen 3). The pixels move only by accumulation order, as on ARM.
Correctness
test-backend-ops -o MUL_MAT/-o GET_ROWSon CPU, with a test-only harness change that loads the scalarx64variant as the reference: haswell and sapphirerapids variants 2510/2510 MUL_MAT, 240/240 GET_ROWS (GGML_CPU_IQK=2, so the iqk kernels are the ones under test).vec_dotdifference is 3.6e-5 of max|ref| on x86 and 1.8e-6 on the M1, and the error against fp64 is the same asvec_dot's. An engagement counter confirms the kernels ran; withGGML_CPU_IQK=0it stays at 0.GGML_BACKEND_DL, all CPU variants) builds on x86 (all 14 variants) and with GCC 14 on aarch64. No static initializers in the ported code (dlopen runs them for every variant while ggml scores CPUs).Image accuracy on ARM (base vs this PR vs a BF16 reference, every shipped K quant and the I-quant text encoders): see the comment below.
Not covered
vec_dot(ARM64 and x64).