FastPath64 — Track 2, Cloud AI
Project Overview
The format you pick to fit an Arm instance was the one Arm could not accelerate
Every 4-bit GGUF makes the same two decisions: which 16 values a nibble may take, and how finely the scale that rescales them may vary. IQ4_XS is the only widely published format that takes the good answer to both.
| nibble values | scale structure | bits/weight | |
|---|---|---|---|
| Q4_0 | uniform grid | one fp16 per 32 weights | 4.50 |
| Q4_K | uniform grid | super-block + 6-bit scale and 6-bit min per 32 | 4.50 |
| IQ4_NL | non-uniform codebook | one fp16 per 32 weights | 4.50 |
| IQ4_XS | non-uniform codebook | super-block + 6-bit scale per 32, no min | 4.25 |
The codebook matters because weights are roughly normally distributed, and a uniform grid spends
half its levels on a tail that is nearly empty; kvalues_iq4nl places its 16 levels where the mass
actually is. The super-block matters because one fp16 per 32 weights is 0.5 bpw of pure metadata.
IQ4_XS takes the codebook from the IQ family and the scale hierarchy from the K family, and by
dropping the min term it lands cheaper than either. Each figure is one struct definition away from
being checked: 136/256, 144/256, 18/32.
That ranking is what actually gets published. In the standard GGUF release ladder — 18 files, IQ3_M through f16 — IQ4_XS is the only IQ4 variant shipped at all, and the smallest 4-bit rung:
IQ3_M · IQ4_XS · Q3_K_L · Q3_K_XL · Q4_0 · Q4_0_4_4 · Q4_0_4_8 · Q4_0_8_8
Q4_K_S · Q4_K_M · Q4_K_L · Q5_K_S · Q5_K_M · Q5_K_L · Q6_K · Q6_K_L · Q8_0 · f16
Arm's fast path accelerated IQ4_NL — the 4.5 bpw cousin that ladder does not contain — and not
IQ4_XS. The one IQ format on Arm's fast path is the one nobody downloads. Meanwhile the three
Q4_0_4_* files above are the tail of an earlier answer to this same problem: Arm-specific
pre-repacked GGUFs, which upstream removed in favour of doing the interleave at load time
(ggml/src/ggml.c:894 — "TYPE_Q4_0_4_4 REMOVED, use Q4_0 with runtime repacking") so nobody would have to publish an Arm-shaped file again. That runtime mechanism is exactly the one IQ4_XS was never
added to.
Two doors, both shut
llama.cpp reaches Arm's integer matrix-multiply unit through two mechanisms. The first is
KleidiAI, Arm's own microkernel library, which selects between DOTPROD, I8MM, SVE and SME2 kernels at runtime. The second is the in-tree repack path, which converts weight tensors into a
row-interleaved layout and issues smmla — the Armv8.6 8-bit integer matrix-multiply-accumulate
instruction — over 2×8 operand tiles.
Neither mechanism accepted IQ4_XS. KleidiAI dispatches only on GGML_OP_MUL_MAT and only for Q4_0
and Q8_0 (kleidiai.cpp:536,668), which also excludes GGML_OP_MUL_MAT_ID, the operation through
which every mixture-of-experts expert matmul is routed. The repack type table
(ggml_repack_get_optimal_repack_type, repack.cpp:4528) enumerates Q4_0, Q2_K, Q4_K, Q5_K, Q6_K,
Q8_0, IQ4_NL and MXFP4. IQ4_XS occurs zero times in the file.
On Neoverse hardware those weights fell through to the generic per-row vec_dot path: sdot at
best, smmla never issued, on a core whose lscpu advertises i8mm. The same format is enumerated
in qtype_has_amx_kernels() (amx/common.h:106), so Intel AMX carries a tiled path for it. The gap
was specific to Arm.
So the incentive closes on itself. Selecting an Arm instance for cost efficiency motivates fitting the largest model into the least memory, which motivates the most aggressive 4-bit format available — which was precisely the format with no Arm kernel. The optimisation chosen to fit the instance surrendered the throughput it was intended to buy.
The trade-off, priced
From the benchmark run's own metadata, on one 4 vCPU Neoverse N2 instance:
| Llama-3.2-3B | file on disk | prefill pp512, stock |
prefill, FastPath64 |
|---|---|---|---|
| Q4_K_M | 1.873 GiB | 42.70 t/s | 42.74 t/s (untouched control) |
| IQ4_XS | 1.696 GiB (−9.5%) | 24.95 t/s (0.58×) | 52.85 t/s (1.24×) |
Before this work, choosing IQ4_XS bought 9.5% less memory for 42% less prefill throughput. Prefill throughput is what sets how many agent turns an Arm instance absorbs per second, so the format chosen to make the model fit cost roughly 1.7× the instances to serve it.
After this work there is no trade at all. IQ4_XS is smaller and faster than Q4_K, on the same unmodified file, on the same hardware. (OLMoE-1B-7B: 3.458 vs 3.922 GiB, −11.8%.)

And the two properties are the same property. IQ4_XS is small because it has no min term; the
kernel is fast because it has no min term — a single 6-bit scale covers both nibble halves of a
sub-block, so no bsums/dmin correction is needed and the inner loop does strictly less
arithmetic per byte than Q4_K's. The format that was the slowest 4-bit option on Arm was
structurally the one that should have been the fastest. It was losing to a missing kernel, not to
its own design.
Result
Prefill throughput for Llama-3.2-3B in IQ4_XS on Neoverse N2 rises 2.12× at 512-token prompts and 1.60× at 2048, on an unmodified GGUF file, with Q4_K — a format the change does not touch — held as a control at 1.00×. A whole agent turn runs 1.30× faster end to end.

Functionality / Output
Why the kernel did not previously exist
IQ4_XS is structurally a hybrid. Its quantised values are indices into kvalues_iq4nl, the same
16-entry non-linear codebook used by IQ4_NL, while its scale structure is K-quant style: a
super-block d in fp16 plus eight 6-bit sub-block scales split across scales_h (2 bits) and
scales_l (4 bits), each covering 32 elements.
typedef struct {
ggml_half d; // super-block scale
uint16_t scales_h; // 2 high bits x 8 sub-blocks
uint8_t scales_l[QK_K/64]; // 4 low bits x 8 sub-blocks
uint8_t qs[QK_K/2]; // codebook indices
} block_iq4_xs;
Arm already had interleaved smmla kernels for each half separately — block_iq4_nlx8 handles the
codebook, block_q4_Kx8 handles super-block scales against q8_K activations. The missing kernel
was the intersection of two kernels already in the tree, which is a plausible explanation for why it
was never written: it belonged to neither the IQ-quant nor the K-quant effort.
Layout
block_iq4_xsx8 interleaves eight rows, taking eight bytes at a time round-robin, so row j's byte
(8c + i) is placed at qs[64c + 8j + i]. The interleaved block is exactly 8 × sizeof(block_iq4_xs)
= 1088 bytes, a hard constraint: the repack buffer type allocates ggml_nbytes(tensor), leaving no
room to pre-decode scales into a friendlier form.
The consequence is that eight consecutive bytes belong to a single column, so one 16-byte load
yields a column pair — precisely an smmla operand.
Kernels
vmmlaq_s32(acc, a, b) treats both operands as 2×8 int8 matrices and returns the 2×2 product. With
weights as one operand and activations as the other, a 16-byte weight load meets a 16-byte
activation load with no shuffle in the inner loop:
const int8x16_t w[4] = { // 4 chunks -> q8 groups 0..3
vqtbl1q_s8(kvalues, vandq_u8 (raw0, m4b)), // local elements 0-7
vqtbl1q_s8(kvalues, vandq_u8 (raw1, m4b)), // 8-15
vqtbl1q_s8(kvalues, vshrq_n_u8(raw0, 4)), // 16-23
vqtbl1q_s8(kvalues, vshrq_n_u8(raw1, 4)), // 24-31
};
for (int t = 0; t < 4; t++) {
sb01 = vmmlaq_s32(sb01, w[t], q8_01[t]); // activation rows 0,1
sb23 = vmmlaq_s32(sb23, w[t], q8_23[t]); // activation rows 2,3
}
Result lanes are [w0·a0, w0·a1, w1·a0, w1·a1], so lanes 0–1 carry the first column's scale and lanes 2–3 the second's, and the scale vector is built as [s0, s0, s1, s1].
Three kernels ship, covering the deployed Arm server population:
| path | instruction | hardware |
|---|---|---|
| GEMM | smmla (I8MM) |
Graviton3/4, Cobalt 100, Axion |
| GEMM | sdot (DOTPROD) |
Graviton2, Ampere Altra |
| GEMV | sdot (DOTPROD) |
all of the above |
GEMV remains on DOTPROD by design. At nr == 1 there is no second activation row with which to fill
an smmla operand, and the existing non-repacked vec_dot path is already well optimised;
displacing it would trade batch-1 decode for prefill. Activations are instead loaded once per
8-element run and duplicated so that a single vdotq_s32 serves both columns of a pair, keeping all
sixteen of its multiply-accumulates productive.
Dispatch is gated at DOTPROD. Below that threshold the portable reference is slower than not repacking at all, so IQ4_XS is deliberately left on the existing path.
Evaluation
Both arms are built from the same pinned upstream commit in the same CI job on the same physical runner, eliminating machine-to-machine variance. Timings are gated behind a numerical equivalence check and a GGUF provenance check that run first; the workflow refuses to report throughput if either fails.
| Llama-3.2-3B, IQ4_XS | stock | FastPath64 |
|---|---|---|
prefill pp512 |
24.95 ±0.02 t/s | 52.85 ±0.02 t/s — 2.12× |
prefill pp2048 |
17.72 ±0.01 t/s | 28.31 ±0.01 t/s — 1.60× |
| Q4_K control | 42.70 ±0.06 t/s | 42.74 ±0.04 t/s — 1.00× |
The gain is a property of the kernel rather than of one model size. On gemma-4-12b-it-IQ4_XS, four
times larger and benchmarked alone on its own runner, prefill rises 2.00× at pp512 and 1.61× at
pp2048 — tracking the 3B figures closely. Three independent runs on separate runner instances
returned these ratios to two decimal places.
Attribution rests on the control rather than on the delta alone. Q4_K is untouched by this change and lands at 1.00× to two decimal places across all four cases; had the patched build benefited from an incidental effect — compiler behaviour, cache state, a quieter neighbour — the control would have moved with it.
The mechanism was isolated before any kernel was written, by toggling GGML_CPU_REPACK across three
builds of the same commit:

Disabling Arm's fast path costs Q4_K 35% of its prefill throughput and costs IQ4_XS 0.6%, inside the run-to-run spread. The absence was demonstrated rather than inferred: a format cannot lose what it never had.
What it is worth on a real agent turn
llama-bench measures a kernel. An agent turn measures what the instance is rented for: a large
system prompt, tool schemas, history and retrieved context in; a short structured tool call out.
Measured end to end on the same runner — 5145 prompt tokens, 96 generated, median of 3:
| stock | FastPath64 | |
|---|---|---|
| time to first token | 463.48 s | 355.13 s — 1.31× |
| decode | 6.59 s | 6.55 s — 1.01× |
| whole turn | 470.09 s | 362.28 s — 1.30× |
99% of the stock turn is prefill, which is why a prefill kernel moves the number a user feels: 108 seconds of every 470-second turn, given back on an unmodified file. Absolute times are a property of a free 4 vCPU runner; the ratio is the result.
On the same prompt with greedy decode and a fixed seed, the two builds emit byte-identical output — the same tool call, token for token. Both checks run in the same workflow as the throughput numbers.
The speedup declines with prompt length, and the trend is the argument's own consistency check:
| prompt tokens | 512 | 2048 | 5145 |
|---|---|---|---|
| speedup | 2.12× | 1.60× | 1.31× |
This work touches the FFN matmuls and nothing else. Attention grows with sequence length, so the share of prefill available for a matmul kernel to improve shrinks. No headline figure here is quoted without its prompt length.
The MoE measurement is lower than the dense one (1.13× on OLMoE-1B-7B) for a reason visible in the same arithmetic: a MoE prefill reads every expert's weights while computing only the active fraction, so its arithmetic intensity is lower by roughly the sparsity ratio, moving it toward the bandwidth-bound regime in which a compute kernel cannot help.
Correctness
Repacked kernels are unreachable from test-backend-ops, which allocates tensors into the default
CPU buffer; the repack path activates only for tensors resident in the repack buffer type. This
submission therefore includes an equivalence harness that allocates identical weights into both
buffer types and diffs the results.
On Neoverse N2, before any timing is reported:
type=iq4_xs N=64 K=512 M=1 max_abs=3.815e-06 PASS (GEMV only)
type=iq4_xs N=64 K=512 M=5 max_abs=3.815e-06 PASS (both paths + remainder)
type=iq4_xs N=128 K=1024 M=9 max_abs=7.629e-06 PASS
type=iq4_xs N=8 K=256 M=16 max_abs=0.000e+00 PASS (GEMM only)
Worst-case deviation is below that of both the portable reference and upstream's own Q4_K repack
kernel, smmla accumulating exactly in int32 with fewer intermediate float roundings. The harness
also surfaced a latent fault upstream: init_tensor leaves tensor->extra null for any type no
kernel claims, and set_tensor dereferenced it unchecked, converting a configuration error into a
segmentation fault. A diagnostic assertion is included in the patch series.
Correctness is further verified across dispatch paths no free CI runner exposes, by cross-compiling
at native speed and executing under QEMU with a selected core model — neoverse-n1 reproduces the
DOTPROD-only behaviour of Ampere Altra and Graviton2. Twenty-four combinations pass: six shapes ×
{IQ4_XS, Q4_K} × {N2, N1}. Emulation is used for correctness only; no timing taken under it is
reported.
Artifacts
- A three-patch series against pinned upstream
llama.cpp, on a branch prepared for review with a written PR description. test_repack_equiv.cpp, an equivalence gate for repacked kernels covering a case upstream's test suite structurally cannot reach.- A QEMU cross-build harness giving access to DOTPROD-only and I8MM dispatch paths without Arm hardware or a cloud account.
agentbench.pyanddiff_generation.sh, measuring a whole tool-calling turn and checking that the output text does not change.gguf_types.py, a dependency-free GGUF tensor-type parser — because a file named IQ4_XS need not contain any, and benchmarking one that does not returns a clean 1.00× that looks like a result.docs/the-gap.md, a source-level audit with file:line citations of what each Arm fast path accepts.- Benchmark workflows any reader can execute on free hardware.
Limitations
Decode is not improved: batch-1 decode is memory-bandwidth-bound and no matmul kernel alters that.
It carries a small regression (0.97× on the 3B, 0.92× at 12B) from per-sub-block scale decoding the
non-repacked path does not perform. One fix was written, measured, and rejected — decoding the scales with vld4_u8 cost ~10% of prefill because the vector result was consumed as eight scalars and
stalled on store-to-load forwarding; the mechanism is recorded in the repository. Output is
numerically equivalent rather than guaranteed bit-identical, smmla accumulating in a different
order than the reference path. The AMX comparison is read from upstream source rather than measured.
KleidiAI is untouched: extending Arm's own library to MUL_MAT_ID and the IQ formats is the larger
fix, and it belongs upstream of llama.cpp rather than in a patch against it.
Setup Instructions
Three routes, in increasing order of what you need to have. None requires paying for Arm hardware.
1. Benchmarks on Arm, without an account or spend. In the repository, open
Actions → A/B - stock vs FastPath64 on Neoverse N2 → Run workflow. The job clones the pinned
upstream twice, patches one copy, builds both on the same free Neoverse N2 runner, runs the
equivalence and provenance gates, and reports throughput only if they pass. Runtime is 1h30–2h30,
most of it building llama.cpp twice on 4 vCPUs; for a ~40 minute sanity run set repeats: 2 and
prompt_sizes: 512 in the dispatch form. Expected output is a step-summary table in which the
IQ4_XS rows move ~2.12× and the Q4_K control rows read 1.00×.
2. Correctness locally, on x86, without Arm hardware. Cross-compiles at native speed and executes under emulation across both dispatch paths. Needs Docker, 10–20 minutes:
git clone https://github.com/Marc-Dvci/fastpath64 && cd fastpath64
docker build -t fastpath64-cross tools/qemu/
git clone --filter=blob:none https://github.com/ggml-org/llama.cpp.git ../upstream-llama.cpp
git -C ../upstream-llama.cpp checkout "$(cat UPSTREAM_SHA)"
for p in patches/*.patch; do git -C ../upstream-llama.cpp apply "$p"; done
docker run --rm -v "$PWD/..:/src" fastpath64-cross bash /src/fastpath64/tools/qemu/run-equiv-test.sh
Expected: 24 PASS lines, each preceded by took repack fast path: yes, then ALL CHECKS PASSED.
3. On your own Arm64 server — Graviton, Axion, Cobalt, Ampere Altra. No flags are required;
kernel selection follows runtime CPU feature detection, so one binary picks smmla on an I8MM core
and sdot on one without. Dependencies: cmake, a C++17 compiler, curl, python3.
git clone https://github.com/Marc-Dvci/fastpath64 && cd fastpath64
git clone --filter=blob:none https://github.com/ggml-org/llama.cpp.git upstream
git -C upstream checkout "$(cat UPSTREAM_SHA)"
for p in patches/*.patch; do git -C upstream apply "$p"; done
cmake -S upstream -B upstream/build -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF
cmake --build upstream/build --target llama-bench -j"$(nproc)"
curl -fL -o m.gguf https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/Llama-3.2-3B-Instruct-IQ4_XS.gguf
upstream/build/bin/llama-bench -m m.gguf -p 512,2048 -n 128 -t "$(nproc)" -r 5
Build a second tree from the same commit without applying the patches to see the delta;
bench/compare_ab.py formats the comparison and bench/gguf_types.py confirms the file really is
IQ4_XS before you trust either. If both builds give the same pp512 number on a core whose
/proc/cpuinfo lists i8mm, the patches did not apply.
Log in or sign up for Devpost to join the conversation.