Repository navigation
CPU bundles: runtime-selected ggml CPU variants and repacked quantized weights (1.3x to 3x faster on CPU) - #28
Conversation
The Linux x64, Linux arm64, macOS x86_64 and Windows CPU bundles were static -DGGML_NATIVE=OFF builds: AVX2/FMA on x86 (no AVX512, VNNI or AMX, and SIGILL on pre-AVX2 CPUs) and plain Armv8.0 on Linux arm64 (no dotprod or i8mm). Build them with GGML_BACKEND_DL + GGML_CPU_ALL_VARIANTS so ggml loads the best libggml-cpu-<variant> for the host at startup (Linux arm64 with GCC 14, which accepts +sme). Linux libraries get DT_RPATH $ORIGIN like the CUDA bundles, macOS ones @loader_path only. A gate checks every finished zip ships the floor variant, keeps no build-tree paths, and loads a variant from itself.
ggml's CPU backend converts Q4_0 / Q4_K / Q2_K / Q5_K / Q6_K / IQ4_NL weights to interleaved layouts (and Q4_0 / Q4_K / Q8_0 ... to AMX tiles on AMX hosts) when they are uploaded into its extra buffer types, so every matmul reading them runs a wider GEMM kernel. sd.cpp put all weights in the plain CPU buffer, so these kernels never ran. Linear tags its quantized weight as matmul-only, and the model manager places tagged weights in the first extra buffer type whose supports_op accepts the matmul. LoRA on such a weight is added to the output instead of folded into the weight; 'immediately' LoRA mode turns repacking off. SD_CPU_REPACK=0 disables it.
The auto LoRA mode looked only at the file's weight types, so an f16 file converted with --type q4_0 chose 'immediately', which turned CPU repacking off for every such run even with no LoRA. On CPU, count the conversion target as quantized and apply LoRAs at runtime, which works on repacked weights.
Measured on GitHub's Azure EPYC 9V74 (Zen 4: AVX512 without AVX-VNNI), the AVX512 variants ran sd.cpp 15 to 50% slower than the AVX2 one (MSVC and clang alike), while on Zen 5 (Strix Halo) and Emerald Rapids (AVX512 + AMX), both with AVX-VNNI, they were the fastest. Score an AVX512 variant 0 without AVX-VNNI so those hosts load the AVX2 variant, as the static build ran.
|
@codex review |
|
@codex security 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: Something went wrong. Try again later by commenting “@codex review”. ℹ️ 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". |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 89efda16df
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
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".
|
|
||
| void sd_tag_matmul_only_weight(ggml_tensor* weight) { | ||
| if (weight != nullptr && weight->op == GGML_OP_NONE) { | ||
| weight->op_params[SD_MATMUL_ONLY_TAG_SLOT] = SD_MATMUL_ONLY_TAG; |
There was a problem hiding this comment.
Write the tag as a 32-bit op parameter
ggml_tensor::op_params is a byte array, so this stores only the low byte (0x4d) at byte offset 15, while sd_is_matmul_only_weight() compares that byte with the full 0x53444d4d value. The predicate therefore never recognizes tagged weights, sd_cpu_repack_buffer_type_for() always rejects them, and the CPU repacking optimization introduced by this commit is never used. Store and retrieve the tag with the 32-bit op-parameter helpers or an equivalent memcpy at the intended slot.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
op_params is declared as int32_t op_params[GGML_MAX_OP_PARAMS / sizeof(int32_t)] in this ggml (include/ggml.h:707), so SD_MATMUL_ONLY_TAG_SLOT indexes the last 32-bit slot and the whole tag is stored and compared. Engagement confirmed on d8008ba: SD1.5 with --type q4_0 on an AMX host logs 'cpu weight repack: quantized matmul weights use AMX' and ggml reports 'amx repack tensor ...' for each tagged weight.
…wide repack switch
… every CPU quantize-on-load
|
Codex Review: Didn't find any major issues. Can't wait for the next one! 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". |
AMX steps the shared 2D weight per activation batch (wrong batches, out-of-bounds writes) and both AMX and CPU_REPACK read strided views as contiguous rows. Make the activation contiguous and flatten every batch dim into rows before the matmul on a possibly repacked weight.
…uffer A repack buffer has no read path and holds the repacked layout, so a destination sharing its source with a repacked first destination is read from the file in its own batch.
ggml_backend_load_best opens every matching libggml-<name>-*.so before scoring it, so the current directory search ran any such library placed where sd-cli or sd-server is launched. Keep GGML_BACKEND_DIR, the executable directory and explicit search paths.
…SD libs in the rpath loops
|
Codex Review: Didn't find any major issues. Swish! 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". |
…indows path buffer On Linux/FreeBSD get_executable_path() fell back to "." when readlink failed, which put the current directory back on the backend search list. Return an empty path instead and only search the executable directory when it is known. On Windows, GetModuleFileNameW truncates at MAX_PATH and the helper then returned an ancestor directory; retry with a larger buffer.
AMX and CPU_REPACK extra buffers only implement MUL_MAT with F32 activations (the type the buffer selection probes with). A non-F32 activation produced NaN on AMX and aborted on CPU_REPACK.
…the CPU A weight's buffer is not allocated until its first segment loads, so the first graph asked the process-wide repack switch instead of the weight's placement, and quantized GPU linears took the repacked-CPU matmul path (2D flatten) on that graph. The model manager now keeps the tag only while the weight's compute backend is the CPU, at registration and when the compute backend changes.
…iate LoRA opt out)
Problem
Every CPU path in the prebuilt bundles ran a fraction of the CPU it was on.
-DGGML_NATIVE=OFFbuilds. On x86 that is ggml's fixed AVX/AVX2/FMA/F16C/BMI2 baseline: no AVX512, AVX512-VNNI or AMX on the CPUs that have them, andSIGILLon any CPU without AVX2. On Linux arm64 no-marchis passed at all, so the build is plain Armv8.0 (System Info: NEON = 1 | ARM_FMA = 1): no dotprod, no fp16 arithmetic, no i8mm, which are the instructions every quantized and f16 matmul uses on Arm.Change
Four commits.
unsloth-sd-prebuilt.yml). The Linux x64, Linux arm64, macOS x86_64 andwin-cpu-x64legs build withGGML_BACKEND_DL=ON GGML_CPU_ALL_VARIANTS=ON(shared ggml), so ggml scores eachlibggml-cpu-<variant>against the host at startup and loads the best: x86-64 baseline, SSE4.2, AVX, AVX2 (haswell), AVX-VNNI, AVX512 / VNNI / BF16 and AMX on x86 (MSVC builds the 9 variants it can compile), Armv8.0 up to Armv8.6 i8mm, SVE2 and SME on Linux arm64 (built with GCC 14, since GCC 13 rejects+sme). sd.cpp already creates its CPU backend through the ggml registry, so no source change was needed for this.DT_RPATH=$ORIGINexactly like the CUDA bundles; macOS libraries get@loader_pathonly.scripts/unsloth/cpu_variants_gate.shruns on every finished CPU zip (a PowerShell twin on Windows): the floor variant (x64/armv8.0_1) must be present, no file may point back at the build tree, andsd-clistarted from the unzipped copy in an empty cwd must load a variant from that copy.Lineartags its quantized weight as matmul-only (a slot in the leaf tensor's unusedop_params, so the tag lives and dies with the tensor). When the compute backend is the CPU, the model manager places a tagged weight in the first CPU extra buffer type whosesupports_opaccepts that weight'sMUL_MAT(AMX first, then the generic repack), and logs it once:cpu weight repack: quantized matmul weights use CPU_REPACK. Everything else, and every GPU path, is untouched.x W^T + x dW^T,add_lora_to_output) instead of being folded into a copy ofW, since a repacked weight can only feedMUL_MAT. Weights that are not repacked keep the existing path bit for bit.--lora-apply-mode immediately(which rewrites weights in place) turns repacking off.SD_CPU_REPACK=0.--mmapkeeps weights in the file mapping, which the repacked layout cannot use, so mmapped weights are not repacked; the log says so once (cpu weight repack: skipped for mmapped weights). Studio does not pass--mmap.immediately) opts out of repacking for that context only; on CPU, AUTO picksat_runtimewhenever--typeor--tensor-type-rulesquantizes while loading and the diffusion model, text encoder or VAE runs on the CPU.at_runtimeLoRA when the weights end up quantized.autolooked only at the file's types, so an f16 file loaded with--type q4_0choseimmediately. That turned repacking off for every such run, LoRA or not. The conversion target now counts as quantized when the diffusion model runs on the CPU. (On the base, that sameimmediatelypath aborts withGGML_ASSERT(*cur_backend_id != -1)for an f16 SD1.5 file +--type q4_0+ a LoRA on CPU; with this change the run completes.)ggml-patches/0008). On GitHub's Azure EPYC 9V74 (Zen 4: AVX512 without AVX-VNNI) the AVX512 variants ran 15 to 50% slower than the AVX2 one, with MSVC and with clang. On Zen 5 (Strix Halo) and Emerald Rapids, both with AVX-VNNI, they are the fastest by a wide margin. Without AVX-VNNI an AVX512 variant now scores 0, so those hosts load the AVX2 variant, as the static build ran.Results
Base = fork master
afac7a7cbuilt with the workflow's flags; head = this branch built with the new flags. Same host, same job, arms interleaved ABBA, one freshsd-cliprocess per measurement, 3 repeats, median [min, max]. Seconds. Prompt "a photo of a red fox sitting in fresh snow, golden hour, detailed fur", seed 42, cfg 1. "Sampling" is sd-cli'ssampling completed, "generate" itsgenerate_image completed(text encoder + sampling + VAE).AMD Ryzen AI MAX+ 395 (Strix Halo, Zen 5, 16C/32T, AVX512 + AVX-VNNI), Linux, 16 threads, exclusive box
Head loads
libggml-cpu-zen4.so.Where it comes from (same box, sampling):
SD_CPU_REPACK=0Threads: head at 32 threads (SMT) instead of 16 gave the same sampling time (44.13 vs 43.83 s, Z-Image) and a slower VAE decode (9.12 vs 6.24 s), so Studio's physical-core default stays.
GitHub
ubuntu-24.04-arm(Neoverse-N2, 4 cores, SVE2 + i8mm), 4 threadsBase
System Info: NEON = 1 | ARM_FMA = 1; head loadslibggml-cpu-armv8.6_2.so(FP16_VA | MATMUL_INT8 | SVE | DOTPROD).Z-Image with
SD_CPU_REPACK=0: 116.16 s sampling.GitHub
ubuntu-latest(AMD EPYC 7763, Zen 3, AVX2 only, 4 vCPU), 4 threadsHead loads
libggml-cpu-haswell.so: the same instruction set as the static build, and withSD_CPU_REPACK=0its images are bit-identical to base and its times equal (SD-Turbo q8_0 / q4_K / q4_0 / f16 sampling 23.89 / 23.79 / 25.35 / 24.33 s base, 23.86 / 23.69 / 25.27 / 24.31 s head). The gain is the repack:Intel Xeon Platinum 8559C (Emerald Rapids, AVX512 + AVX-VNNI + AMX), Linux, 16 threads pinned to 16 physical cores
A shared host (load average 57 to 73 on 192 threads during the run), so ratios only. Head loads
libggml-cpu-sapphirerapids.soand puts Q4_K / Q4_0 / Q8_0 weights in AMX tiles.Z-Image with the AVX2 variant forced and repack on: 68.56 s; AMX variant with
SD_CPU_REPACK=0: 65.88 s.GitHub
windows-latest(MSVC, as the release leg builds it), 4 threadsThis branch, with the fork's full ggml patch set applied to both arms, on an AMD EPYC 7763 (Zen 3, AVX2 only). Head loads
ggml-cpu-haswell.dll.SD-Turbo f16 head images are bit-identical to base. Z-Image's repack gain on this MSVC build is inside the noise (the same arm under another name ran 227.09 s).
Why commit 4 exists, from two earlier Windows runs without it (SD-Turbo, 512, 2 steps, sampling,
SD_CPU_REPACK=0):cascadelakeloadedzen4loadedhaswellforcedcascadelakeloadedWith commit 4 both CPUs load
haswell, which matches base.GitHub
macos-15-intel(Core i7-8700B, AVX2 only, 4 vCPU)Head loads
libggml-cpu-haswell.so; SD-Turbo images are bit-identical to base at all four types. This runner's timings swung up to 2.5x within one arm (SD-Turbo q8_0 base sampling 23.13 to 66.35 s), so it gives no speed verdict either way; every head run completed.Accuracy
An instruction set change legitimately moves low bits; the check is that head is no further from a high-precision reference than base.
SD_CPU_REPACK=0(Zen 3 above; AVX2 variant forced on this host with the full ggml patch set), images are bit-identical to base.SD_CPU_REPACK=039.9 dB (AVX2 repack) and 42.6 dB (AMX); the same seed without the LoRA is 13.3 dB away, so the LoRA is applied. With the AVX2 variant and Q8_0 (no repack for that type on x86) head is bit-identical to base with the LoRA.Tests
win-cpu-x64legs all pass the new CPU variants gate. Each lists its variants (14 on Linux / macOS x64, 9 with MSVC, 8 on arm64) andsd-clifrom the unzipped bundle loadshaswell/armv8.6_2/haswell/ggml-cpu-haswell.dllfrom it (an earlier Linux x64 runner loadedsapphirerapids). The gate caught two real problems on the way, both fixed in commit 1: build-treeLC_RPATHs in the macOS libraries, and the Windows step ending onsd-cli's own exit code.win-cpu-x64(and the unchangedwin-vulkan-x64) fail the existing "every PE is signed" check, since a fork has no signing secrets; the signing step itself signs every PE inbuild/bin, which now includes theggml-cpu-*.dllfiles.ubuntu-latest,ubuntu-24.04-arm,macos-15,macos-15-intel,windows-latestand on the gfx1151 CI box: every head run exits 0.SD_CPU_REPACK=0bit-identical (SD-Turbo q4_0 / q8_0 / q4_K / f16, SD1.5 + LoRA); repack engagement in every DiT run (cpu weight repack: ... use CPU_REPACK/AMX, 324 to 449 tensors).Not covered
GGML_CPU_ALL_VARIANTS'apple_m1variant drops the fp16 arithmetic Apple clang's default target already has, and was 40% slower than base on GitHub's M1 runner, so it is not used there.