Repository navigation
feat(cpu-moe): serve GGUF experts on the cpu and hybrid backends through ggml-cpu - #704
Conversation
110c069 to
1f3d743
Compare
cf7eed8 to
8bc94f5
Compare
|
The design looks good to me: only the row kernels are vendored, and the #700 executor keeps the threads and the work split, so GGUF is just one more format. I found no correctness bug. Before merge I would add tests and state two regressions in the PR. Checked (H100, Xeon 8480+, torch 2.9.0+cu128):
1. No tests. The PR deletes 2. Q4_0 is 6-13% slower than main, and there are no A/B numbers. Setup: Gemma 26B-A4B expert shape, 8 layers x 32 experts (~0.9 GB, well past the LLC), top-8, 12 threads, same banks. Median µs per layer over 2 runs:
So VNNI does help on this CPU. Options: an AVX-VNNI build dispatched at runtime (the cpuid check is already there), or accept the loss and state it in the PR. 3. aarch64 loses GGUF on the CPU. The removed Nit: |
7131bc6 to
dbeb7e4
Compare
…the best one the CPU supports
dbeb7e4 to
0ef6bca
Compare
Upstream 0bca88f..11c1adb: GGUF on vendored ggml (FlashML-org#690 and the per-arch loaders), MoE slot pools (FlashML-org#689, FlashML-org#700), GGUF experts on the cpu/hybrid executor (FlashML-org#704), the NVFP4 MoE prefill GEMM rewrite (FlashML-org#723), triton decode split-k by context length (FlashML-org#729), graph upload at capture (FlashML-org#697), transformers 5.18. Resolution: - GGUF: upstream's stack replaces ours (dense llama/qwen2/qwen3, qwen3moe, qwen35moe loaders, q4_0/gguf expert banks, our ggml kernels and their tests). Re-ported on top: rope-only --hf-overrides (YaRN) for the llama/qwen2/qwen3/qwen3moe GGUF parsers, and the scalar head_count_kv of Gemma-4 GGUFs (upstream PR FlashML-org#190). Gemma-4 E2B/E4B GGUF is not re-ported. - offload cache: upstream's pooled cache, with our disk tier (one pool only), fill_slots preload per pool, sync-free prefill invalidation for a contiguous staging range, hit-D2D-by-default wording and the HIP graph per-bank copy. - NVFP4 prefill: upstream's GEMM and per-GPU tuning supersede our sm_89 tile config and JOIN_B. - triton decode attention: upstream's split-k over our fp8/nvfp4 KV paths and HIP head padding; DFlash verify graphs are uploaded at capture too. - embedding: upstream's embedding quant method, with --embed-device cpu in front and the tied head's kernel choice kept for unquantized tables. - engine: DFlash, --kv-reserve-share, shrink-first rebuild, startup hybrid split and the WSL pin probe combined with the slot pools. GPU-checked on the RTX 4090: full suite green; Qwen3.6-35B-A3B NVFP4 prefill ~13.2k / decode ~131 tok/s (baseline 13.6k / 131), Qwen3-30B-A3B Q4_K_M GGUF 14.6k / 259 (was 8.3k / 225). Assisted-by: Claude Opus 5.5
Part of #671. Builds on #700.
Summary
GGUF experts of every ggml type, quantized or float, now run on the CPU MoE executor (
hybrid, andcpu/--moe-cpu-layerswhere the slot pools allow), not only Q4_0 and BF16. This includes layers whose gate_up and down mix a float and a quantized type (#702). The kernels are llama.cpp's ggml-cpu row kernels, vendored unchanged and built once per x86 variant of llama.cpp'sGGML_CPU_ALL_VARIANTS; at startup the executor uses the best variant this CPU runs. Theq4_0CPU format is removed.How it plugs into the CPU executor
GGUF is one more weight format. The executor (threads, work split, route sum) has nothing GGUF-specific.
Startup:
Decode (prefill stays on the GPU):
Rows are read in place from the host banks the GPU copies from. At startup
ggufis refused only when the build has no variant module (not x86-64), with an error pointing to--moe-strategy offload. The startup log names the variant, for exampleisa=zen4(ggml-cpu), andFREETOKEN_GGML_CPU_VARIANT=<name>forces one.CPU variants
freetoken.kernel.ggml_cpu._<variant>: the 14 x86 variants of upstreamGGML_CPU_ALL_VARIANTS(x64, sse42, sandybridge, ivybridge, piledriver, haswell, skylakex, cannonlake, cascadelake, icelake, cooperlake, zen4, alderlake, sapphirerapids), or the 9 of them upstream builds with MSVC. Each module holds the kernel files built with that variant's ISA flags and macros, as upstream's CMake sets them, and an entry (variant_module.cppwith upstream'scpu-feats.cpp) built without them.select()imports every module and keeps the highestscore(), upstream's rule: the score is 0 when the CPU lacks an ISA extension the variant is built for. Importing a module runs only the entry. x64 runs on any x86-64 CPU, so GGUF experts now also run on CPUs without AVX2.ggml-quants.cis built once without ISA flags, like upstream's ggml-base, and linked into every module._cpu_moekeep their per-functiontargetattributes and runtime dispatch._cpu_moeno longer links any ggml code.strip(5.6 MB in all; 25.5 MB with the-gdebug info setuptools adds).Vendored ggml-cpu
From llama.cpp 8c2134a8, the same commit as the ggml-cuda tree of #690. No file is modified;
VERSIONlists them.ggml-cpu/quants.candggml-cpu/arch/x86/quants.c(per-typevec_dotand input quantizers),ggml-cpu/vec.cpp(the f32 / f16 / bf16vec_dot) andggml-quants.c(reference quantizers and grids), with their headers, andggml-cpu/arch/x86/cpu-feats.cpp(the variant score) withggml-backend-impl.h. The ggml-cpu backend (ggml-cpu.c, ops, threadpool, graph) is not copied; the executor keeps its own threads and calls the kernels row by row.ggml/freetoken/cpu_kernels.c: the few ggml-base symbols the kernels need, the fp32 -> f16 / bf16 / f32 input converters, and a weight type ->vec_dot/ input type table whose 28 entries matchtype_traits_cpuin upstreamggml-cpu.c.VERSION, add new types to the table, and new upstream variants toGGML_CPU_VARIANTSinsetup.py.Validation
H100 80GB, driver 580.95.05, CUDA 13.0; Xeon Platinum 8480+, 12 executor threads; torch 2.14.1+cu130, gcc 11.4.
The two failures are not from this PR: one is a local HF cache entry with an unsupported
modelopt_mixedconfig, andtest_offload.py::test_graph_capture_reuses_warm_offload_cache_before_capture(FakeGraphhas noraw_cuda_graph_exec) also fails on main e8c6bf1.Kernel check:
CpuMoeExecutor.decodeagainst an fp32 reference built from the dequantized rows, bs 1, 3 and 8, top-8, with the variantselect()picks on the test CPU (zen4). Worst max error relative to the reference max:The GPU column runs the GPU path of the same layers: the ggml-cuda kernels for quantized rows, and the fused MoE Triton kernels for float rows (feat(gguf): run experts whose gate_up and down mix float and quantized types #702).
The same check with each of the other 12 variants the test CPU runs, forced with
FREETOKEN_GGML_CPU_VARIANT(sapphirerapids scores 0: the VM does not expose AMX): all pass. The worst CPU error is 2.4e-2, and 2.6e-2 for x64 and sse42, which run ggml's scalar code.Q4_0 decode on the CPU, Gemma 26B-A4B expert shape with real rows from the QAT Q4_0 GGUF, 8 layers x 32 experts, top-8, 12 threads. Median us per layer, two runs:
q4_0format (its own VNNI kernel)Q8_0 rows (unsloth Q8_K_XL GGUF) take 505 to 537 us at bs 1 on each of the five variants above.
A CUDA graph decoding a GGUF layer and a BF16 layer replays identically to eager decode.
Windows, compile only: clang-cl with the xwin MSVC SDK builds each of the 9 MSVC variants with the MSVC flags in
setup.py(plus the-mflags clang needs for VBMI, VNNI and AVX-VNNI, as upstream adds for Clang) andgguf.cpp, and lld-link links each variant with no unresolved symbols. The module entry was built against a stubPython.h, since no Windows Python headers were at hand.Not covered
cpu/--moe-cpu-layerson GGUF files whose layers use several ggml types: rejected at startup, since several slot pools do not combine with them.ft bench bwfor GGUF (hybrid uses a fetch cap of 1), one kernel call for several tokens of an expert, aarch64, running on Windows, and wheel installs of the variant modules (this was built withbuild_ext --inplace).