Skip to content

Optimize EmbeddingGemma 2 CPU inference with AVX-VNNI, faster attention, and multimodal acceleration - #6

Closed
Yazeedx0 wants to merge 32 commits into
QuixiAI:mainfrom
Yazeedx0:main
Closed

Yazeedx0 wants to merge 32 commits into
QuixiAI:mainfrom
Yazeedx0:main

Conversation

@Yazeedx0

Copy link
Copy Markdown
Contributor

Summary

This PR introduces CPU inference optimizations for EmbeddingGemma 2, building on the existing embeddinggemma.c implementation.

The goal is to improve inference latency, throughput, and memory efficiency across text, image, audio, video, and mixed inputs without retraining or changing the model weights.

What's changed

  • AVX-VNNI: Optimized 256-bit Q8_0 GEMM kernels.
  • Attention: Tiled attention with improved memory layouts.
  • Multimodal: Faster CPU execution paths for vision and audio.
  • Memory: Weight repacking, buffer reuse, and reduced intermediate allocations.
  • Parallelism: Improved CPU thread scheduling and cold-start performance.
  • Concurrency: Reduced head-of-line blocking for text requests during media inference.

Benchmarks

Measured on an Intel Core i7-14700HX:

  • 2.46× geometric mean speedup over the fastest tested competing CPU backend.
  • Faster than PyTorch FP32 and ONNX Runtime INT8 across all 16 tested workloads.
  • 16–54× faster than unmodified upstream for image, audio, and video workloads on 8 P-cores.
  • Near-FP32 embedding quality, with minimum cosine similarity of 0.99914.

Results are specific to the tested hardware and configurations.

Validation

  • Existing upstream tests pass.
  • GEMM and attention kernels validated against numerical references.
  • Multimodal inference and concurrency tests pass.
  • Retrieval quality evaluated against the official FP32 implementation.

Notes

The optimizations preserve the original model weights and include portable fallbacks. Official model semantics are enabled by default, with an option to restore upstream behavior. The optimized engine may use more peak memory than upstream.

Full benchmarks, methodology, and implementation details are available in the repository.

https://github.com/Yazeedx0/embeddinggemma-cpu-max

…io towers

Wall-clock accounting (act_quant, mm_q8, mm_f32, attention, norm, rope,
activation, elementwise, conv) printed per tower call on stderr. Off by
default; computation is unchanged. Used to size the CPU media bottlenecks
on hosts without perf access.
Upstream runs the CPU vision tower in the scalar reference graph. Profile
on an i7-14700HX (8 P-core threads, 280-token image, 2520 patches):
attention 67.9%, Q8 GEMM 26.3%, serial activation quantization 3.3%;
31.5 s per image.

* gemma2_vision_fast.c: same math as the reference (Q8_0 activations for
  Q8_0 weights, fp32 attention/norms, fp16-rounded patch input, the GGUF's
  GELU variant), executed with threaded fused row passes (norm+quant,
  q/k/v norms + axial RoPE from tables, GELU+quant, post-norm+residual),
  the FFN in 256-token chunks (O(chunk) instead of O(patches) buffers) and
  a persistent workspace. EI_GEMMA2_CPU_MEDIA_FAST=0 selects the reference.
* gemma2_media_x86.c: g2m_attn64, tiled online-softmax attention in a
  transposed form (S^T/P^T/O^T) so softmax is all vertical vector ops and
  K/V are read in place; vector exp/QuickGELU/SiLU. Scalar fallbacks.
* gemma2_vnni256.c: Q8_0 GEMM for AVX-VNNI CPUs without AVX-512. Weights
  repacked once into 8-row groups (one 32-byte load = 4 k-bytes of 8 rows),
  u8 activations with a -128*sum(w) seeded accumulator, per-block scaling of
  8 rows with one convert/multiply/FMA, two int32 chains per token, loops
  unrolled explicitly (GCC keeps them rolled at -O2). Used by every Q8 GEMM
  of waves over 8 tokens (text, vision, audio); EI_GEMMA2_CPU_VNNI256=0
  restores the upstream tile.
* test_gemma2_vnni256: route exactness vs a double scalar Q8_0 dot and
  GMAC/s; test_gemma2_media: fast vs reference tower parity and timing.

Results (same process and pixels, 8 P-core threads): GEMM 1.07-1.25
TMAC/s vs 196-213 for the upstream tile (5.2-6.3x), max rel err 4.8e-8;
vision tower 125 ms vs 3565 ms (i70, 28x) and 977 ms vs 31459 ms (i280,
32x). Tower cosine vs the HF FP32 tower with the same activation:
reference 0.99827/0.99907, fast 0.99863/0.99894 (i70/i280).
Upstream runs the CPU audio tower in the scalar reference graph (15 s clip,
1499 mel frames, i7-14700HX 8 P-core threads: Q8 GEMM 68.6%, subsampling
convolutions 11.8%, serial activation quantization 8.2%; 3.3 s).

gemma2_audio_fast.c keeps the reference math (Q8_0 activations, the
clipped-linear input/output clamps, fp32 convolutions, norms and local
relative-position attention, the reference loader's conv_norm/norm_conv
mapping) and runs it with channel-vectorized AVX2 subsampling convolutions
with fused LayerNorm+ReLU, Q8 GEMMs on the fast-graph routes (VNNI256
here), per-token fused clamp/norm/SiLU/GLU/depthwise-conv/quantize passes,
relative-position keys computed once per layer, and a persistent
workspace. EI_GEMMA2_CPU_MEDIA_FAST=0 selects the reference.

test_gemma2_media gains audio (raw [frames][128] float32 mel) and
audio-synthetic modes; its gate is now flattened cosine >= 0.999 and every
token >= 0.95 (two Q8-activation graphs differ by rounding noise).

Real Clotho clip, HF-extractor mel, tower vs the HF FP32 tower:
reference 0.99893 (worst token 0.944), fast 0.99907 (0.961).
…t on x86

g2m_attn generalizes the vision kernel: head widths 64..512 (multiple of
16), separate q/kv/out strides for GQA/MQA, optional per-query key bounds
(packed sequences, +-512 sliding window), exact zeros for masked keys
(exp8 returns 0 below its range; fully masked lanes cannot produce NaN).
The query block adapts to the width so Q^T/O^T stay 32 KB each.

The fast text graph's attention (upstream: 8-query blocks, two-pass
softmax, 4x4 dot tiles with horizontal sums) routes through it on x86
AVX2; EI_GEMMA2_CPU_FLASH=0 restores it, the NEON path is unchanged.
bench_gemma2_cpu on 12 E-cores: 600x2 1298 -> 867 ms, 1100x1 1498 -> 908
ms; min cosine vs the reference graph unchanged (0.99966 / 0.99976).

test_gemma2_attn: tiled vs scalar fp32 vs a float64 ground truth for
d=64/128/256/512, GQA 4:2 and MQA, windows and packed bounds; the tiled
kernel is as accurate as plain fp32 (e.g. d=512: 9.8e-5 vs 1.0e-4 of row
RMS, dominated by fp32 score rounding).
Upstream's gemm_f converts four weight rows to fp32 per task on every call
and computes 4x4 dot tiles ending in 16 horizontal sums. It serves the
BF16 per-layer-input projection (512 -> 12288, every text/media token),
the F32 vision patch embedding and the F32 audio input projection.

gemma2_sgemm_x86.c expands the weight once (exact for BF16/F16) into
16-column panels and runs a 6-token x 16-output register tile (2 loads +
6 broadcasts per 12 FMAs, no reductions); fp32 throughout. One-time cost
~31 MB resident (fp32 copies). EI_GEMMA2_CPU_SGEMM=0 restores gemm_f.

test_gemma2_vnni256 fNxMxK/FNxMxK: 12 E-cores, 2048x12288x512 BF16 250 vs
53 GMAC/s (4.7x), 2520x768x768 F32 269 vs 51; max rel err 1.8e-7.
bench_gemma2_cpu (12 E-cores): 600x2 867 -> 735 ms, min cosine vs the
reference graph 0.999655 (unchanged).
* EI_GEMMA2_AUDIO_LOGMEL=hf: log(mel + 0.001) as the Transformers
  Gemma4AudioFeatureExtractor computes it (upstream: log(max(mel, 0.001)),
  after llama.cpp mtmd-audio).
* EI_GEMMA2_VIDEO_TIMESTAMPS=0: no MM:SS text before each frame, the
  official processor's default (add_timestamps=False). Upstream always
  inserts it (the add_timestamps=True layout).
* (existing) EI_GEMMA2_VISION_GELU=tanh: gelu_pytorch_tanh in the vision
  MLP as in the official config; the GGUF lacks clip.use_gelu so upstream
  selects QuickGELU.

Defaults are unchanged (upstream semantics) until measured.
…tention bench

* gemm_q8: waves of <= 8 tokens also use the repacked VNNI256 GEMM (one task
  per 8-row group, upstream's short-wave task width). P8: 1-token text
  forward 4.65 -> 3.37 ms, 8 tokens 11.25 -> 5.37 ms; exact (rel err 2e-8).
  EI_GEMMA2_CPU_VNNI256_SHORT=0 restores upstream's tile.
* gemma2_release.h: once a weight is repacked (VNNI256, SGEMM), its interior
  pages in the read-only GGUF mapping are dropped with MADV_DONTNEED so the
  process does not keep two resident copies. gguf.c keeps a registry of open
  mappings; anonymous memory is never released (it would be zeroed).
  EI_GEMMA2_CPU_RELEASE_WEIGHTS=0 disables. (Memory effect still to be
  measured end to end.)
* gemma2_vision_fast.c sizes attention tasks with g2m_attn_qblock(); the
  fixed G2M_ATTN_QB constant is gone.
* test_gemma2_attn bench N D HEADS: attention GMAC/s.

Rejected (not kept): a 48x48-block attention variant with 6x16 / 4x24
register tiles: +1-3.5% at 2,520 patches, -6.5% at 10,080 (P8).
…upstream

Upstream deviates from the official Transformers implementation in three
places: the vision MLP uses QuickGELU (the GGUF lacks clip.use_gelu; the
official config says gelu_pytorch_tanh), the log-mel floor is
log(max(mel, 0.001)) instead of log(mel + 0.001), and every video frame is
preceded by MM:SS text (the processor's add_timestamps=True layout; its
default is False).

Measured against the official Sentence Transformers FP32 embeddings on the
16 benchmark workloads (P8): upstream semantics min cosine 0.8746 (video),
mean 0.983; official semantics min 0.99914, mean 0.99970 (images
0.9992-0.9997 -> 0.9996-0.9999, audio 0.9988-0.9993 -> 0.9991-0.9995, video
0.87-0.96 -> 0.99987+). The reference graph honors the same switch, so
fast-vs-reference parity tests compare like with like.

Adds CPU_MAX.md: what this branch changes relative to upstream d8f9e33,
flags, tests.
On Intel hybrid CPUs, a single unpinned pool over every core made short
requests slower than using the P-cores alone: over HTTP on an i7-14700HX,
16-token text took 13.6-13.8 ms on 28 threads vs 8.7 ms on 8 P-core
threads, while large media gained 12-22% from the E-cores. The calling
thread (an HTTP worker or the wave collector) could run on an E-core and
sits on the critical path of every parallel step.

* parallel.c: ei_cpu_hybrid_order() lists the allowed CPUs (affinity mask)
  as P-core primary threads, E-cores, P-core SMT siblings from
  /sys/devices/cpu_core|cpu_atom; a participant cap per pool
  (ei_thread_pool_set_cap) and the count of leading P-core threads.
* engine.c: on hybrid CPUs the gemma2 CPU pool is pinned in that order
  (EI_CPU_HYBRID=0, non-hybrid CPUs, or more threads than allowed CPUs
  keep upstream's unpinned pool; thread count rules unchanged).
* gemma2_fast.c / vision / audio: the caller is pinned to the pool's first
  P-core for each wave; text waves under EI_GEMMA2_CPU_HYBRID_TOKENS (512)
  run on the P-core threads only.

HTTP, 28 threads, interleaved twice: t16 13.8/13.6 -> 8.7/8.3 ms, t512
130.8/128.8 -> 130.1/130.5 ms, i280 799/858 -> 810/808 ms, a15 241/251 ->
235/234 ms. In-process (bench_gemma2_cpu) the layouts are within noise,
so the gain is the caller's placement.
The goldens are llama.cpp outputs (QuickGELU vision MLP, log(max) mel floor,
timestamped video). Under the fork's official default the vision tower golden
reads 0.9974 by construction; with EI_GEMMA2_SEMANTICS=upstream the whole
suite passes (text 0.999672, vision tower 0.999257 / embedding 0.999772,
audio 0.999929 / 0.999635, HTTP stage). Official-semantics accuracy is
validated against the HF FP32 model.
Block-size sweep, test_gemma2_attn bench, 8 P-core threads, vision shapes
(12 heads x 64): KB 32/64/128 x QB 16/32/48/64/96/128. Smaller query blocks
lose (n=10080: QB=16 180-219 GMAC/s vs QB=64 264-284), so K/V re-streaming
matters more than L1 fit; from QB=48 up every point is within +-5% of the
default KB=64/QB=64 (305/272 GMAC/s at n=2520/10080), the same spread as
repeating one configuration. No default changed.
…ling

- VNNI256 repacked weights keep the Q8_0 block scales as fp16 (exact; the
  GGUF stores fp16) and the per-row block sums as int16 (|sum| <= 4064),
  expanded to fp32 / -128*sum in registers: 36 instead of 40 bytes of
  metadata+codes per 32 weights of a row (-10% of the repacked weights).
- The audio subsampling runs conv0 per task into a thread-local buffer
  covering only the rows its conv1 rows read (2*grain+1), instead of
  materializing conv0 for the whole clip (t0 x 64 x 128 floats, 49 MB at
  30 s); boundary conv0 rows are recomputed (+6% of conv0, ~11% of the
  subsampling MACs). The conv1 output buffer is sized for conv1.

Outputs are bit-identical to 2cd784b (fast and reference towers, synthetic
audio and vision dumps compared with cmp). Fresh-server peak RSS, 12 E-core
threads: t512 367->352, i280 582->548, i1120 810->776, a30 845->753,
v16 798->764, m_tia 1036->948 MB. Latency: subsample stage 10.2 vs 10.2 ms
(1499 mel frames, 3 runs each, interleaved); HTTP p50 within noise.
Tests: test_gemma2_vnni256, test_gemma2_attn, test_gemma2_media
{audio,vision}-synthetic, make test-gemma2 (incl. HTTP stage) pass.
K and V were read in place from the token-major [n][768] buffers: each
key of a head sat 3 KB from the next, on its own 4 KB page, and the
per-head K/V stream of a query block (5.2 MB at 10080 patches) spilled
L2 with the prefetchers unable to follow it. The QKV post pass now writes
normed+roped K as [12][n][64] into h (free until the output projection)
and a second pass writes the normed V the same way into the k buffer
(free once K has moved), so no memory is added. Q and the output stay
token-major (Q is packed once per task).

Bit-identical (fast tower dumps for i280 and i1120 compared with cmp).
test_gemma2_attn bench, 12 E-core threads, token-major -> head-major:
n=2520 143/143 -> 157/153 GMAC/s, n=10080 128/126 -> 159/145 (THP via
EI_ATTN_BENCH_THP alone: within noise). Vision tower, 12 E-cores, two
interleaved runs: attention i1120 20.3-20.6 -> 17.4-17.8 s, i280
1.18 -> 1.07 s, i70 74 -> 70 ms; rows +45 ms at i1120 for the V pass;
tower total i1120 -11%, i280 -6%, i70 -2.5%.

test_gemma2_attn bench gains EI_ATTN_BENCH_HEADMAJOR / EI_ATTN_BENCH_THP.
Tests: test_gemma2_attn, test_gemma2_media vision-synthetic, make
test-gemma2 pass.
Runs a one-token text, a 48x48 image and a 16-frame silent clip through the
CPU graphs before the server listens, so the one-time weight repack is not
paid by the first request of each modality. Fresh server, first request,
12 E-core threads, two rounds: t512 647/639 -> 400/394 ms, i280 2857/3032
-> 2407/2441 ms, a30 2091/2179 -> 1530/1448 ms (warm: ~370, ~2540, ~1470).
Startup +708 ms. Off by default because every tower's repacked weights
then stay resident from startup (peak RSS of a t512-only server 352 ->
852 MB).
The token-major V buffer is free once ROW_V_HEADS has moved V into the
head-major planes, so attention writes its output there and the separate
[n][768] output buffer is gone (31 MB at 10080 patches). Bit-identical
(i1120 dump compared with cmp). Fresh-server peak RSS, 12 E-cores: i280
548 -> 541, i1120 776 -> 747, v16 764 -> 761, m_tia 948 -> 941 MB.
The log-mel ran single-threaded in the HTTP worker and evaluated the
320-point Hann window with cosf for every frame. The window is now a
table (same expression, computed once with the FFT twiddles), and on the
CPU tower path the frames run on the engine pool; the engine lock is
taken before the mel instead of before the tower (it covers the pool).
GPU paths keep the serial mel outside the lock. Embeddings bit-identical
(a5, a15, a30, m_ta over HTTP, compared with numpy array_equal).

12 E-core threads, server stage medians: logmel a5 5.9 -> 0.7 ms, a15
17.4 -> 1.6, a30 35.2 -> 3.0. End to end, 4 rounds with alternating
order: a5 274 -> 261 ms, a15 712 -> 698 ms.

Also: the streamed subsampling uses 4-row tasks (was 8): a 5 s clip has
125 output rows and 16 tasks for 12 threads. Subsample stage, 499 frames,
12 E-cores, 9 runs each: 2cd784b (unstreamed) median 3.9 ms, 8-row tasks
4.5, 4-row tasks 3.9; 1499 frames: 10-12 ms for all three.
On the CPU backend every tower call holds inference_mutex, so a text wave
queued behind a media request waited for the whole tower (a 280-token image
is ~1 s on 8 P-cores, a 1120-token one ~8-11 s). Media engine sections now
take cpu_media_mutex before inference_mutex (one media request at a time in
the towers), and the vision and audio towers call ei_gemma2_cpu_yield()
between layers, after vision attention and between vision FFN chunks. The
server's hook releases inference_mutex only when text waves are waiting and
blocks on a condition variable until one has acquired it (no barging, no
spinning), then re-locks. Text waves never take cpu_media_mutex or a tower
lock, so the handoff cannot let a second media request in and deadlock on
the tower workspace lock. EI_GEMMA2_CPU_YIELD=0 restores the plain lock;
GPU media slots are unchanged.

Head-of-line probe (parent repo bench/hol_blocking.py), 12 E-cores, one
closed-loop t16 client while one media request runs (t16 alone 22 ms):
  i280: t16 p50 1133 -> 24.7 ms, max 2082 -> 183 ms, 2 -> 177 completed;
        the image takes 2.29 -> 5.72 s, i.e. by the ~3.5 s of text work done
        meanwhile (the engine is shared, total work unchanged)
  a30:  t16 p50 659 -> 89 ms, max 766 -> 555 ms (the request's own final
        text pass, one wave), 2 -> 13 completed; 1.34 -> 1.62 s
Concurrency stress (bench/stress_mixed.py: 6 clients, 90 s, text/image/
audio/video/mixed): no deadlock, all 67 responses bit-identical to the same
request alone. make test-gemma2 passes.
The first use of every Q8_0 tensor (VNNI256 8-row repack) and every
float-weight projection (SGEMM 16-column panels) was packed by the calling
thread alone while the pool idled: a cold server paid it inside the first
request of each modality. Both packers now split the work (8-row groups,
16-column panels) over the caller's pool; the bytes written are unchanged
(i280 tower dump compared with cmp).

First request on a fresh server, 12 E-cores, two interleaved rounds:
t512 581/591 -> 415/479 ms, i280 2552/2602 -> 2318/2292 ms, a30
1868/1860 -> 1484/1509 ms (warm: ~370, ~2300-2500, ~1470).
test_gemma2_vnni256 and make test-gemma2 pass.
The tooling and results behind the CPU-Max measurements, next to the
engine they measure:

- bench/: latency, throughput and memory harness for the native engines
  (HTTP), Sentence Transformers FP32 and ONNX Runtime int8; quiet-host
  guard and per-measurement foreign CPU load; head-of-line probe and a
  concurrency stress that checks responses against solo requests.
- scripts/: pinned asset downloads, the upstream baseline as a git
  worktree of d8f9e33, fixtures, measurement queues, embedding
  validation, retrieval evaluation, the quantization study, profiles,
  summaries, tables and plots.
- reports/: final results (JSON/CSV), optimization log, bottleneck
  analysis, plots and raw per-run data; runs not used by any report are
  kept in reports/raw/excluded/ with the reason.

Models, datasets, fixtures, cached embeddings and the virtual
environment are not included (cpu-max/.gitignore).
The repository README will describe this fork; upstream's README (engine
and HTTP API documentation) moves unchanged so its history stays
traceable (git log --follow README.upstream.md).
The repository README now describes this fork: what was optimized, how
it differs from upstream, build and run, results against PyTorch FP32,
ONNX Runtime int8 and the unmodified upstream engine, correctness and
retrieval quality, limitations, reproduction and attribution. Upstream's
README (engine and HTTP API documentation) is unchanged under
README.upstream.md. CPU_MAX.md: commit IDs of the published history and
the new layout.
make_fixtures.py and prepare_retrieval_data.py read the photos from
cpu-max/data/flickr1k/images_flickr_1k_test; the archive is part of the
pinned dataset snapshot and is now extracted there. Found by building the
fixtures from a fresh clone.
An installed binary could not tell CPU-Max from upstream's embeddinggemma.
The server now prints its identity with --version and as the first log
line, e.g.

  EmbeddingGemma-2-CPU-Max 0.1.0-dev (commit 1a2b3c4; fork of
  QuixiAI/embeddinggemma.c v0.3.1-8-gd8f9e33)

from CPU_MAX_VERSION (0.1.0-dev until a release is approved), the git
commit (-dirty for uncommitted builds) and the upstream base, a literal
in the Makefile because upstream's VERSION still reads v0.3.1 eight
commits later and shallow clones have no tags. No inference code changes.
Found by building in a clean ubuntu:24.04 container with the toolchains
the new CI uses:

- clang 18 rejects __builtin_cpu_supports("avxvnni") (accepted from
  clang 19 and by GCC), in upstream's gemma2_fast_x86.c and in
  gemma2_vnni256.c. AVX-VNNI is now read from CPUID.(EAX=7,ECX=1):EAX[4];
  the AVX2 check already covers OS support for the YMM state. Same
  dispatch on the benchmark machine; g2v_enabled() drops the redundant
  builtin (its AVX-VNNI level already implies the feature).
- On non-x86 targets (aarch64 cross build, macOS arm64) the repack caches
  of gemma2_vnni256.c and gemma2_sgemm_x86.c were unused statics, an error
  under -Werror; they now live inside the x86 sections.

Embeddings unchanged (t16, t128, i70, a5, m_tia over HTTP compared byte
for byte with the 5dd7dbf benchmark run).
…binaries

The inherited installer downloaded QuixiAI/embeddinggemma.c release
binaries and installed them as ~/.local/bin/embeddinggemma, so a user
expecting CPU-Max got upstream. CPU-Max has no release binaries yet, so
install.sh now:

- builds the checkout it belongs to, or clones this repository at --ref
  (default main), with the system C compiler;
- refuses to install unless the binary's --version names
  EmbeddingGemma-2-CPU-Max;
- installs it as embeddinggemma-cpu-max, never replacing an upstream
  embeddinggemma;
- rejects the GPU variants (unchanged upstream code, not built or tested
  here) with the command for upstream's installer, and upstream's
  --version option with an explanation;
- warns on untested platforms (Linux/macOS arm64) and refuses others;
- --with-models downloads the model files via the new
  cpu-max/scripts/download_models.sh (curl/wget + SHA-256, no Python;
  defaults to the server's own model directory). download_assets.sh uses
  it for the GGUF files.
…, model tests

The inherited workflow ran upstream's model-free checks only. Now:

- portable: upstream's make check and build on ubuntu-24.04 and macos-14,
  plus the build identity;
- cpu-max-linux: CPU-Max kernel tests at the runner's dispatch level and
  forced AVX2 and scalar (EI_GEMMA2_CPU_X86), so nothing assumes AVX-VNNI;
  clang build; aarch64 cross build of the portable fallbacks;
- model-tests: the two GGUF files (1.0 GB, pinned revision, SHA-256
  checked) cached by checksum, upstream's integration suite
  (make test-gemma2: tokenizer, towers vs reference graphs, HTTP), and an
  HTTP smoke test (cpu-max/examples/quickstart.py: text, image, audio,
  mixed; standard library only, synthetic media).

Read-only token, no secrets, actions pinned to commit SHAs,
persist-credentials off.
…provenance

README (claims checked against cpu-max/reports):
- headline scoped to the tested backends on one i7-14700HX; same-budget
  geomeans (P8 2.20x, min 1.35x) and per-baseline averages alongside the
  best-budget 2.46x; near-FP32 quality (min 0.99914, mean 0.99970); the
  precision of each backend (CPU-Max Q8_0, PyTorch FP32, ONNX Runtime
  INT8); hardware, thread budgets, quiet-host guard, run policy (CPU-Max
  one run, baselines best of two) and tail latency at 28 threads;
- retrieval limits: small subsets, Clotho top-1 agreement 80.2%;
- vector-index compatibility between official and upstream semantics;
- quick start verified end to end (build, checksum-verified models,
  localhost server, text/image/audio/mixed smoke test), source installer;
- status: pre-release, no binaries, v0.1.0-v0.3.1 tags are upstream's;
  fork base d8f9e33 is v0.3.1 plus 8 commits.
Tables now show p50 (p95) and the backends' precision in their names;
plots regenerated with the same names.

RELEASE.md, CONTRIBUTING.md: CPU-Max sections above upstream's text.
README.upstream.md: note that its installer installs upstream.
SECURITY.md: private reporting, default 0.0.0.0 listen, model checksums.
cpu-max/reports/README.md: which raw runs feed which report, policies,
known anomalies (scaling runs record a later HEAD; 28-thread tails).
CPU_MAX.md: the two new engine commits.
@coderabbitai

coderabbitai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 192 files, which is 92 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5e842516-94e2-45d3-9700-6e705fd0ac65
📥 Commits

Reviewing files that changed from the base of the PR and between d8f9e33 and 197f8fb.

⛔ Files ignored due to path filters: 21
  • cpu-max/reports/benchmarks.csv is excluded by !**/*.csv
  • cpu-max/reports/memory.csv is excluded by !**/*.csv
  • cpu-max/reports/plots/latency_ALL.png is excluded by !**/*.png
  • cpu-max/reports/plots/latency_P8.png is excluded by !**/*.png
  • cpu-max/reports/plots/memory.png is excluded by !**/*.png
  • cpu-max/reports/plots/readme/chart_data.csv is excluded by !**/*.csv
  • cpu-max/reports/plots/readme/retrieval-dark.svg is excluded by !**/*.svg
  • cpu-max/reports/plots/readme/retrieval-light.svg is excluded by !**/*.svg
  • cpu-max/reports/plots/readme/speedup-vs-fastest-dark.svg is excluded by !**/*.svg
  • cpu-max/reports/plots/readme/speedup-vs-fastest-light.svg is excluded by !**/*.svg
  • cpu-max/reports/plots/readme/speedup-vs-pytorch-onnx-dark.svg is excluded by !**/*.svg
  • cpu-max/reports/plots/readme/speedup-vs-pytorch-onnx-light.svg is excluded by !**/*.svg
  • cpu-max/reports/plots/scaling.png is excluded by !**/*.png
  • cpu-max/reports/plots/speedup_ALL.png is excluded by !**/*.png
  • cpu-max/reports/plots/speedup_P8.png is excluded by !**/*.png
  • cpu-max/reports/quality_embeddings.csv is excluded by !**/*.csv
  • cpu-max/reports/raw/excluded/prelim_summary.csv is excluded by !**/*.csv
  • cpu-max/reports/retrieval.csv is excluded by !**/*.csv
  • cpu-max/reports/scaling.csv is excluded by !**/*.csv
  • cpu-max/reports/throughput.csv is excluded by !**/*.csv
  • cpu-max/uv.lock is excluded by !**/*.lock
📒 Files selected for processing: 192
  • .github/workflows/ci.yml
  • CONTRIBUTING.md
  • CPU_MAX.md
  • CPU_MAX_VERSION
  • Makefile
  • README.md
  • README.upstream.md
  • RELEASE.md
  • SECURITY.md
  • cpu-max/.gitignore
  • cpu-max/README.md
  • cpu-max/bench/backends.py
  • cpu-max/bench/hol_blocking.py
  • cpu-max/bench/profile_native.py
  • cpu-max/bench/run_bench.py
  • cpu-max/bench/stress_mixed.py
  • cpu-max/examples/client.py
  • cpu-max/examples/quickstart.py
  • cpu-max/models/SHA256SUMS
  • cpu-max/pyproject.toml
  • cpu-max/reports/BENCHMARKS.md
  • cpu-max/reports/README.md
  • cpu-max/reports/benchmarks.json
  • cpu-max/reports/bottlenecks.md
  • cpu-max/reports/hardware.json
  • cpu-max/reports/memory.json
  • cpu-max/reports/optimization_log.md
  • cpu-max/reports/quality_embeddings.json
  • cpu-max/reports/quant_study.json
  • cpu-max/reports/raw/ab_compact_E12_build-next_r1.json
  • cpu-max/reports/raw/ab_compact_E12_build-next_r2.json
  • cpu-max/reports/raw/ab_compact_E12_build-next_r3.json
  • cpu-max/reports/raw/ab_compact_E12_build_r1.json
  • cpu-max/reports/raw/ab_compact_E12_build_r2.json
  • cpu-max/reports/raw/ab_compact_E12_build_r3.json
  • cpu-max/reports/raw/ab_compact_build-next_r1.json
  • cpu-max/reports/raw/ab_compact_build-next_r2.json
  • cpu-max/reports/raw/ab_compact_build_r1.json
  • cpu-max/reports/raw/ab_compact_build_r2.json
  • cpu-max/reports/raw/ab_hybrid_P8_h0_r1.json
  • cpu-max/reports/raw/ab_hybrid_P8_h0_r2.json
  • cpu-max/reports/raw/ab_hybrid_P8_h1_r1.json
  • cpu-max/reports/raw/ab_hybrid_P8_h1_r2.json
  • cpu-max/reports/raw/attn_layout_P8.txt
  • cpu-max/reports/raw/cold_prepack0_E12_r1.json
  • cpu-max/reports/raw/cold_prepack0_E12_r2.json
  • cpu-max/reports/raw/cold_prepack1_E12_r1.json
  • cpu-max/reports/raw/cold_prepack1_E12_r2.json
  • cpu-max/reports/raw/excluded/README.md
  • cpu-max/reports/raw/excluded/baseline_onnx_q8_P8_t8.json
  • cpu-max/reports/raw/excluded/baseline_st_fp32_P8_t8.json
  • cpu-max/reports/raw/excluded/final_fork_5dd7dbf_ALL_t28_contaminated.json
  • cpu-max/reports/raw/excluded/final_fork_5dd7dbf_P8_t8_contaminated.json
  • cpu-max/reports/raw/excluded/final_tput_media_fork_5dd7dbf_ALL_t28_contaminated.json
  • cpu-max/reports/raw/excluded/final_tput_media_fork_5dd7dbf_P8_t8_contaminated.json
  • cpu-max/reports/raw/excluded/final_tput_media_quixi_upstream_P8_t8_contaminated.json
  • cpu-max/reports/raw/excluded/final_tput_text_fork_5dd7dbf_ALL_t28_contaminated.json
  • cpu-max/reports/raw/excluded/final_tput_text_fork_5dd7dbf_P8_t8_contaminated.json
  • cpu-max/reports/raw/excluded/final_tput_text_quixi_upstream_P8_t8_contaminated.json
  • cpu-max/reports/raw/excluded/prelim_summary.json
  • cpu-max/reports/raw/excluded/quant_study_invalid_empty_match.json
  • cpu-max/reports/raw/excluded/smoke_native.json
  • cpu-max/reports/raw/excluded/smoke_onnx.json
  • cpu-max/reports/raw/excluded/smoke_st.json
  • cpu-max/reports/raw/final_fork_1a3e206_ALL_t28.json
  • cpu-max/reports/raw/final_fork_1a3e206_P16_t16.json
  • cpu-max/reports/raw/final_fork_1a3e206_P8_t8.json
  • cpu-max/reports/raw/final_fork_1a3e206_PE20_t20.json
  • cpu-max/reports/raw/final_fork_1a3e206_fidelity_P8_t8.json
  • cpu-max/reports/raw/final_fork_5dd7dbf_ALL_t28.json
  • cpu-max/reports/raw/final_fork_5dd7dbf_P8_t8.json
  • cpu-max/reports/raw/final_fork_97fdcc7_ALL_t28.json
  • cpu-max/reports/raw/final_fork_97fdcc7_P8_t8.json
  • cpu-max/reports/raw/final_mem_fork_1a3e206_P8_t8.json
  • cpu-max/reports/raw/final_mem_fork_1a3e206_norelease_P8_t8.json
  • cpu-max/reports/raw/final_mem_fork_5dd7dbf_P8_t8.json
  • cpu-max/reports/raw/final_mem_fork_5dd7dbf_norelease_P8_t8.json
  • cpu-max/reports/raw/final_mem_fork_5dd7dbf_prepack_P8_t8.json
  • cpu-max/reports/raw/final_mem_fork_97fdcc7_P8_t8.json
  • cpu-max/reports/raw/final_mem_fork_97fdcc7_norelease_P8_t8.json
  • cpu-max/reports/raw/final_mem_upstream_P8_t8.json
  • cpu-max/reports/raw/final_onnx_q8_ALL_t28.json
  • cpu-max/reports/raw/final_onnx_q8_ALL_t28_media.json
  • cpu-max/reports/raw/final_onnx_q8_P8_t8.json
  • cpu-max/reports/raw/final_onnx_q8_P8_t8_run1.json
  • cpu-max/reports/raw/final_st_fp32_ALL_t28.json
  • cpu-max/reports/raw/final_st_fp32_P8_t8.json
  • cpu-max/reports/raw/final_st_fp32_P8_t8_run1.json
  • cpu-max/reports/raw/final_tput_media_fork_1a3e206_P8_t8.json
  • cpu-max/reports/raw/final_tput_media_fork_5dd7dbf_ALL_t28.json
  • cpu-max/reports/raw/final_tput_media_fork_5dd7dbf_ALL_t28_g1.json
  • cpu-max/reports/raw/final_tput_media_fork_5dd7dbf_P8_t8.json
  • cpu-max/reports/raw/final_tput_media_fork_5dd7dbf_P8_t8_g1.json
  • cpu-max/reports/raw/final_tput_media_fork_97fdcc7_ALL_t28.json
  • cpu-max/reports/raw/final_tput_media_fork_97fdcc7_P8_t8.json
  • cpu-max/reports/raw/final_tput_media_quixi_upstream_P8_t8.json
  • cpu-max/reports/raw/final_tput_media_quixi_upstream_P8_t8_g1.json
  • cpu-max/reports/raw/final_tput_text_fork_1a3e206_P8_t8.json
  • cpu-max/reports/raw/final_tput_text_fork_5dd7dbf_ALL_t28.json
  • cpu-max/reports/raw/final_tput_text_fork_5dd7dbf_ALL_t28_g1.json
  • cpu-max/reports/raw/final_tput_text_fork_5dd7dbf_P8_t8.json
  • cpu-max/reports/raw/final_tput_text_fork_5dd7dbf_P8_t8_g1.json
  • cpu-max/reports/raw/final_tput_text_fork_97fdcc7_ALL_t28.json
  • cpu-max/reports/raw/final_tput_text_fork_97fdcc7_P8_t8.json
  • cpu-max/reports/raw/final_tput_text_quixi_upstream_P8_t8.json
  • cpu-max/reports/raw/final_tput_text_quixi_upstream_P8_t8_g1.json
  • cpu-max/reports/raw/final_upstream_ALL_t28.json
  • cpu-max/reports/raw/final_upstream_P8_t8_fast.json
  • cpu-max/reports/raw/final_upstream_hflayout_P8_t8.json
  • cpu-max/reports/raw/hol_a30_yield0_E12.json
  • cpu-max/reports/raw/hol_a30_yield0_P8.json
  • cpu-max/reports/raw/hol_a30_yield1_E12.json
  • cpu-max/reports/raw/hol_a30_yield1_P8.json
  • cpu-max/reports/raw/hol_i1120_yield0_P8.json
  • cpu-max/reports/raw/hol_i1120_yield1_P8.json
  • cpu-max/reports/raw/hol_i280_E12.json
  • cpu-max/reports/raw/hol_i280_c8_yield0_E12.json
  • cpu-max/reports/raw/hol_i280_c8_yield1_E12.json
  • cpu-max/reports/raw/hol_i280_yield0_E12.json
  • cpu-max/reports/raw/hol_i280_yield0_P8.json
  • cpu-max/reports/raw/hol_i280_yield1_E12.json
  • cpu-max/reports/raw/hol_i280_yield1_P8.json
  • cpu-max/reports/raw/hybrid_hybrid0_1792128037.json
  • cpu-max/reports/raw/hybrid_hybrid0_1792128067.json
  • cpu-max/reports/raw/hybrid_hybrid_1792128051.json
  • cpu-max/reports/raw/hybrid_hybrid_1792128082.json
  • cpu-max/reports/raw/m1_fork_3ec5d16_P8_t8.json
  • cpu-max/reports/raw/m1_upstream_P8_t8.json
  • cpu-max/reports/raw/m1_upstream_P8_t8_slow.json
  • cpu-max/reports/raw/mel_build-next_E12_r1.json
  • cpu-max/reports/raw/mel_build-next_E12_r2.json
  • cpu-max/reports/raw/mel_build_E12_r1.json
  • cpu-max/reports/raw/mel_build_E12_r2.json
  • cpu-max/reports/raw/mem_compact_build-next.json
  • cpu-max/reports/raw/mem_compact_build.json
  • cpu-max/reports/raw/mem_noa_E12.json
  • cpu-max/reports/raw/mem_stream_build-next.json
  • cpu-max/reports/raw/mem_stream_build.json
  • cpu-max/reports/raw/profile_ALL.txt
  • cpu-max/reports/raw/profile_P8.txt
  • cpu-max/reports/results.md
  • cpu-max/reports/retrieval.json
  • cpu-max/reports/scaling.json
  • cpu-max/reports/throughput.json
  • cpu-max/scripts/compare_audio_towers.py
  • cpu-max/scripts/compare_vision_towers.py
  • cpu-max/scripts/download_assets.sh
  • cpu-max/scripts/download_models.sh
  • cpu-max/scripts/eval_retrieval.py
  • cpu-max/scripts/gguf_inspect.py
  • cpu-max/scripts/hw_detect.py
  • cpu-max/scripts/make_fixtures.py
  • cpu-max/scripts/plot_bench.py
  • cpu-max/scripts/prepare_retrieval_data.py
  • cpu-max/scripts/profile_final.sh
  • cpu-max/scripts/quant_study.py
  • cpu-max/scripts/render_results.py
  • cpu-max/scripts/run_baselines.sh
  • cpu-max/scripts/run_final_head.sh
  • cpu-max/scripts/run_final_matrix.sh
  • cpu-max/scripts/run_retrieval.sh
  • cpu-max/scripts/run_upstream_P8.sh
  • cpu-max/scripts/setup_upstream.sh
  • cpu-max/scripts/summarize_bench.py
  • cpu-max/scripts/validate_embeddings.py
  • install.sh
  • src/cpu_max_version.h
  • src/engine.c
  • src/gemma2.c
  • src/gemma2.h
  • src/gemma2_audio_fast.c
  • src/gemma2_fast.c
  • src/gemma2_fast_x86.c
  • src/gemma2_media_x86.c
  • src/gemma2_media_x86.h
  • src/gemma2_release.h
  • src/gemma2_sgemm_x86.c
  • src/gemma2_sgemm_x86.h
  • src/gemma2_vision_fast.c
  • src/gemma2_vnni256.c
  • src/gemma2_vnni256.h
  • src/gguf.c
  • src/gguf.h
  • src/http_media.h
  • src/media.c
  • src/media.h
  • src/parallel.c
  • src/parallel.h
  • src/server.c
  • src/test_gemma2_attn.c
  • src/test_gemma2_media.c
  • src/test_gemma2_vnni256.c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ehartford

Copy link
Copy Markdown
Contributor

amazing contribution.
I am going to be careful with this, since I don't want to take the rebrand.
I'll definitely be bringing the features in.

@Yazeedx0

Copy link
Copy Markdown
Contributor Author

amazing contribution. I am going to be careful with this, since I don't want to take the rebrand. I'll definitely be bringing the features in.

Thanks, really appreciate it!

Totally understand about the rebrand. The goal was always to build on your great work and contribute back to the original project.

Happy to help bring the optimizations upstream and make the integration easier!

@ehartford

Copy link
Copy Markdown
Contributor

Thank you for this work. We wanted the engine but not the rebrand, so rather than merge the fork's main branch (README, installer, --version, the 127k-line cpu-max/ tree, and the default semantics change) I re-applied the CPU engine commits on top of current main as a curated series in #7, with you as the author of each engine commit:

  • repacked 256-bit AVX-VNNI Q8_0 GEMM with source-page release
  • packed fp32 GEMM for the float-weight projections
  • tiled online-softmax attention for the text graph
  • the SIMD/threaded vision and audio towers
  • the media-tower yield and the prepack option
  • the pool cap / hybrid P/E order / mapping registry plumbing

Differences from this PR, on purpose: model semantics stay llama.cpp's by default (your "official" mode is opt-in via EI_GEMMA2_SEMANTICS=official; Metal and CUDA vision would also need it before a default flip), and a few fixes found in review (weight caches keyed by the weight data rather than a stack copy of the tensor, the weight_cache_lock arch guard for Linux aarch64, aligned_alloc on the UCRT, g2x_ prefixes for the AVX isolation check, per-tensor audio clamps instead of ei_die). Validation and numbers are in #7 and in perf/optimization_status.md.

The 9950X and EPYC boxes here are AVX-512, so your 256-bit kernel ran under a new EI_GEMMA2_CPU_X86=avxvnni force value rather than natively; if you can run #7 on the i7-14700HX, the hybrid-pool and VNNI256 defaults would get their first native check. Closing this one in favour of #7.

@ehartford ehartford closed this Oct 18, 2026
ehartford added a commit that referenced this pull request Oct 18, 2026
Port the CPU-Max fork's EmbeddingGemma 2 CPU engine work (PR #6) without the rebrand
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants