coldpath found this. This directory is the fix and the measured recovery.
$ coldpath ollama-windows-arm64/lib/ollama/ggml-cpu.dll
lib/ollama/ggml-cpu.dll
154,880 instructions 100.0% decoded COLD -- 0 matrix instructions
sha256:536ada0d3dd642f4...
-- SME (ZA tile) 0
-- i8mm (smmla) 0
-- dotprod (sdot) 0
Ollama's official Windows-on-Arm build (v0.31.2) contains zero matrix and zero dot-product instructions. Every Snapdragon X laptop running it does LLM matmul in scalar/NEON only. For contrast, coldpath on llama.cpp's own Windows-on-Arm build, same OS, same ggml source:
llama.cpp win-arm64 ggml-cpu.dll WARM i8mm 244 dotprod 1,052
Ollama does not fork ggml's kernels; it fetches upstream llama.cpp pinned by LLAMA_CPP_VERSION and
builds it. The difference is one build flag.
llama/server/CMakePresets.json has two CPU presets:
| preset | used for | GGML_CPU_ALL_VARIANTS |
GGML_CPU_ARM_ARCH |
|---|---|---|---|
cpu |
Linux / x86 | ON (builds 8 ISA variants, dlopens the best at runtime) | unset |
cpu_arm64 |
Windows-on-Arm | OFF | unset |
ggml's GGML_CPU_ALL_VARIANTS is not supported on Windows-on-Arm (it hits a FATAL_ERROR in ggml's
CMake), so Ollama correctly turns it OFF for that target. But nothing then sets a specific -march.
In ggml/src/ggml-cpu/CMakeLists.txt (current main), the non-native ARM branch is:
if (GGML_CPU_ARM_ARCH)
list(APPEND ARCH_FLAGS -march=${GGML_CPU_ARM_ARCH})
elseif(GGML_CPU_ALL_VARIANTS)
... # per-variant -march
endif()
# <-- neither set: NOTHING is appendedWith both unset and GGML_NATIVE=OFF, no -march is appended at all, so the compiler defaults to
plain armv8-a. ggml's fast paths are #if-guarded on __ARM_FEATURE_DOTPROD / __ARM_FEATURE_MATMUL_INT8,
so under armv8-a they are compiled out. The instructions are not slow, they are absent, exactly
what coldpath reports.
cpu_arm64.patch adds one line to the cpu_arm64 preset:
"GGML_CPU_ARM_ARCH": "armv8.2-a+dotprod",armv8.2-a+dotprod is the safe universal floor: every Windows-on-Arm device supports it (Cortex-A76
based Surface SQ1/SQ2 and Snapdragon X / Oryon all have FEAT_DotProd), so there is no SIGILL risk on any
shipped hardware. It restores the dot-product kernels that carry most of the win.
For a Snapdragon-X-targeted build, armv8.6-a+i8mm+bf16 (the WARM row below) additionally enables i8mm
(smmla), matching the i8mm path llama.cpp's own Windows-on-Arm release ships, at the cost of dropping
pre-Snapdragon-X SQ-series compatibility (Cortex-A76 predates i8mm). That is the aggressive option; the
patch here takes the conservative, zero-regression one.
Same model (Qwen2.5-0.5B, Q4_0), same Arm hardware (Neoverse N2), changing only -march. Produced by
../../.github/workflows/benchmark.yml, re-runnable from the
Actions tab. coldpath scans each build to prove what landed in it; llama-bench measures the cost.
| build | what coldpath sees | prompt processing (pp512) | vs COLD |
|---|---|---|---|
COLD armv8-a (Ollama's current WoA build) |
i8mm 0, dotprod 0 | ~95 tok/s | 1.0x |
TEPID armv8.2-a+dotprod (this patch) |
dotprod 1,044 | ~545 tok/s | ~5.75x |
WARM armv8.6-a+i8mm+bf16 (Snapdragon X option) |
i8mm 268, dotprod 1,044 | ~660 tok/s | ~6.9x |
Representative figures from the CI benchmark on the shared Neoverse N2 runner; they vary a few percent per run and reproduce at ~5.75x from the filed dot-product patch, up to ~6.9x with i8mm.
Token generation (decode) is memory-bandwidth-bound and rides on dot-product, not the matrix unit:
roughly 2.3x from the dot-product fix, and flat-to-slightly-lower once i8mm is added on top (expected:
smmla is an outer product that pays off in the compute-bound prefill GEMM, not in batch-1 decode).
Prompt processing is where the cold path costs the most: ~5.75x with the filed dot-product fix,
up to ~6.9x with i8mm.
This likely also explains ollama#8246: 5-10 seconds per token on one Arm chip, fine on another, closed with no root cause. That is the fingerprint of a build that fell back to scalar because the fast-path instructions were compiled out.
I do not have to rebuild Ollama to know the flag is the whole story, because llama.cpp already ships
the counterfactual. Its official Windows-on-Arm release is built from the same pinned ggml source as
Ollama's, on the same OS, and differs only in that it sets a target -march. coldpath scores it WARM
(i8mm 244, dotprod 1,052) where Ollama's is COLD (0, 0). Two real, shipped binaries, identical
source, one build flag apart, opposite verdicts. The CI benchmark then rebuilds that same ggml three
times changing only -march and measures what the flag is worth in tokens/sec, so the COLD -> WARM
recovery is demonstrated on real binaries, not asserted.
The patch is proposed upstream to Ollama as PR #17654 (open, not yet merged). The measurement above is reproducible in CI on free Arm64 hardware; the root cause is confirmed against Ollama's and ggml's current source.