Skip to main content
Jonathan Andrei
Retour aux billets
Août 202612 min de lecture

coldpath : le build Ollama Windows-on-Arm est expédié avec l'unité matricielle éteinte. Correctif d'une ligne déposé en amont, 5,75x sur le traitement de prompt.

Je ne pouvais pas répondre à une question basique sur ma propre machine Arm : quand un LLM local tourne, utilise-t-il réellement l'unité matricielle de la puce, ou retombe-t-il silencieusement sur du code scalaire ? Alors j'ai écrit l'outil qui répond, l'ai pointé sur l'écosystème, et j'ai trouvé le runner LLM Windows-on-Arm le plus populaire froid. coldpath est un désassembleur AArch64 basé sur Capstone qui prouve qu'un binaire contient des instructions SME, i8mm et dotprod ; la trouvaille est que le build officiel win-arm64 d'Ollama en a zéro. Correctif d'une ligne déposé en amont (PR #17654), 5,75x récupéré sur le traitement de prompt mesuré en direct sur Azure Cobalt 100, gaté en CI comme GitHub Action réutilisable.

ArmCloudAIAArch64Neoverse N2Azure Cobalt 100Ollamallama.cppggmlKleidiAICapstoneGitHub ActionsPython
J'ai créé ce billet et le projet coldpath pour le défi Arm Cloud AI sur Devpost. Le correctif amont a été déposé comme Ollama PR #17654 pendant la fenêtre du défi.
La revendication en une phrase : le build officiel Windows-on-Arm d'Ollama (la façon la plus populaire d'exécuter un LLM local) exécute zéro instruction matricielle et zéro dot-product, et tout build Arm portable qui rate un flag de compilation fait pareil. Sur Azure Cobalt 100, ce flag vaut 5,75x sur le traitement de prompt et environ 0,45 $ contre 0,08 $ par million de tokens de prompt, mesuré en direct sur le runner Neoverse N2 gratuit de GitHub. coldpath le trouve dans n'importe quel binaire expédié sans matériel Arm, le correctif que j'ai déposé est une ligne, et la gate est une GitHub Action réutilisable pour que ça ne revienne pas.
Sortie terminal de `coldpath ollama-windows-arm64/lib/ollama/ggml-cpu.dll` montrant 154 880 instructions décodées à 100 % de couverture avec le verdict COLD et zéros pour SME (tuile ZA), i8mm (smmla) et dotprod (sdot). L'unité matricielle est éteinte.
Ollama v0.31.2, release officielle Windows-on-Arm, exactement telle qu'expédiée. Zéro instruction matricielle, zéro dot-product, à 100 % de couverture de décodage. L'absence est étanche : aucun coeur ne peut jamais exécuter une matmul i8mm sur ce binaire, quelle que soit la façon dont la lib dispatche à l'exécution.

La question à laquelle je ne pouvais pas répondre

Je voulais savoir quelque chose de basique sur ma propre installation : quand je fais tourner un LLM quantifié sur une machine Arm, utilise-t-il réellement les instructions de multiplication matricielle de la puce, ou retombe-t-il silencieusement sur du code scalaire ? Je ne pouvais pas trouver comment y répondre. Les kernels matriciels Arm (i8mm, SME2, dot-product) sont sélectionnés à la compilation, donc un mauvais flag de build ne les rend pas lents, il les retire entièrement du binaire, et rien ne vous prévient. Le seul conseil disponible était d'attacher un profileur à du vrai silicium Arm et de lire des noms de symboles assembleur à la main. Cet écart est tout le projet : une question qu'un ingénieur compétent ne peut pas répondre aujourd'hui, rendue répondable, sans matériel Arm requis.

Pourquoi un scan statique est suffisant (et marche sur Arm mais pas sur x86)

ggml, XNNPACK et la plupart des bibliothèques de kernels Arm cuisent leurs chemins rapides dans chaque binaire à la compilation. Après la PR #10457 de llama.cpp (qui a retiré la détection ISA à l'exécution pour corriger une régression de ~15x), les intrinsèques i8mm/dotprod sont `#if`-gardés sur `__ARM_FEATURE_MATMUL_INT8` et `__ARM_FEATURE_DOTPROD`. Donc si le build a utilisé le mauvais `-march`, le chemin rapide n'est pas simplement sauté : il est compilé dehors, physiquement absent du binaire. Désassembler `.text` et regarder est donc décisif. Deux propriétés rendent ça solide. D'abord, AArch64 est à largeur fixe, instructions de 4 octets sur une grille de 4 octets, donc un mot de données incrusté dans `.text` (un pool littéral, une table de sauts) est local à ses 4 octets et ne peut jamais cascader dans le code environnant. Un balayage linéaire x86 se désynchroniserait et corromprait tout en aval ; sur Arm ce n'est pas possible. Ensuite, la détection lit les opérandes de registre (tuiles ZA pour SME, Z/P pour SVE) parce que Capstone laisse `insn.groups` vide pour SVE comme pour SME, donc la détection par groupe rapporte silencieusement zéro, exactement la classe de faux négatifs que cet outil existe pour attraper.

Sortie coldpath côte à côte sur le même OS avec la même source ggml : la release win-arm64 officielle de llama.cpp est WARM avec i8mm 244 et dotprod 1 052, tandis que la release win-arm64 d'Ollama est COLD avec i8mm 0 et dotprod 0. Un flag de build d'écart.
La comparaison qui fait du flag la cause. Même OS, même source ggml, deux binaires expédiés séparés par un flag de build. Ollama ne forke pas ggml : il construit llama.cpp amont épinglé avec un flag manquant. C'est un risque de distribution, pas Ollama qui serait incapable.

Ce que le chemin froid coûte, en dollars, mesuré en direct

La partie honnête. Chaque ligne est construite `GGML_NATIVE=OFF` avec un `-march` épinglé pour isoler l'effet ISA ; seul `-march` change. Cold armv8-a (la base sur laquelle un build portable retombe, le build Windows d'Ollama) exécute la préconstruction Qwen2.5-0.5B Q4_0 à ~95 tok/s. Le correctif d'une ligne que j'ai déposé (armv8.2-a+dotprod, sûr sur chaque appareil Arm expédié) tourne à ~545, une accélération 5,75x, environ 0,08 $ par million de tokens de prompt contre une base de 0,45 $. armv8.6-a+i8mm+bf16 tourne à ~660, 6,9x, environ 0,065 $. La génération s'améliore d'environ 2,3x. Les dollars utilisent un tarif on-demand Arm-cloud d'exemple (Graviton4 c8g, 0,0385 $/vCPU-heure), donc seule cette colonne suppose un prix ; les tok/s et le 5,75x sont mesurés sur le runner Neoverse N2 4-vCPU et se régénèrent depuis CI à chaque push.

Graphique de coût intitulé « Ce que le chemin froid coûte », mesuré en direct sur Azure Cobalt 100 (Neoverse N2) : cold armv8-a tourne à ~95 tok/s à 0,45 $ par 1M tokens, le correctif +dotprod à ~545 tok/s à 0,08 $ par 1M tokens, une accélération 5,75x. Seul le flag -march change entre les lignes.
0,45 $ à 0,08 $ par million de tokens de prompt, sur le runner Arm64 gratuit de GitHub. Le correctif dot-product est sûr sur chaque appareil Arm expédié, donc cette récupération est universelle, pas conditionnelle à du silicium plus récent.
Résumé GitHub Actions du workflow « What the cold path costs » : trois builds ISA (COLD armv8-a, TEPID armv8.2-a+dotprod, WARM armv8.6-a+i8mm+bf16) plus un job compare, tous réussis sur ubuntu-24.04-arm.
Chaque chiffre se régénère depuis CI sur du matériel Arm gratuit, donc un juge peut le rejouer plutôt que faire confiance à une capture. Forkez le dépôt et lancez le workflow.

Le correctif amont : une ligne, déposée comme Ollama PR #17654

La cause est une seule variable CMake manquante dans le `llama/server/CMakePresets.json` d'Ollama. `GGML_CPU_ALL_VARIANTS` est éteint (donc le build ne peut pas dlopen le meilleur de plusieurs variantes ISA à l'exécution), et `GGML_CPU_ARM_ARCH` n'est pas défini, donc le build retombe sur armv8-a de base et chaque intrinsèque matriciel est `#if`-é dehors. Ajouter `"GGML_CPU_ARM_ARCH": "armv8.2-a+dotprod"` récupère le dot-product sur chaque appareil Arm expédié, y compris les puces classe Cortex-A76 des laptops Windows-on-Arm qui n'ont pas i8mm. C'est pourquoi la PR expédie intentionnellement dot-product seulement, pas la ligne i8mm 6,9x : le dot-product est sûr sur chaque coeur, i8mm ne l'est pas.

Vue diff de llama/server/CMakePresets.json montrant l'ajout d'une ligne : `"GGML_CPU_ARM_ARCH": "armv8.2-a+dotprod"`, ajouté à côté des clés existantes GGML_CPU_ALL_VARIANTS OFF et OLLAMA_WINDOWS_RUNTIME_ARCH arm64.
Tout le correctif. Une ligne, déposée comme Ollama PR #17654 pendant la fenêtre du défi. Confirmé COLD sur les 8 releases stables d'Ollama de v0.31.2 à v0.32.7 actuelle, donc cette revendication ne peut pas pourrir silencieusement : `test.yml` re-télécharge la dernière release et la re-vérifie à chaque push.

Comme gate CI : un build froid ne rejoint jamais votre flotte

Pointez la Action réutilisable sur votre propre sortie de build, où chaque binaire devrait être hot. `coldpath --require i8mm ./build/` sort non-zéro si un binaire expédié manque les instructions matricielles, donc la pull request échoue avant que le build froid n'atteigne une flotte Arm. Quand à la place vous scannez toute une archive de release tierce, les kernels matmul vivent généralement dans une lib backend (ex. `ggml-cpu.dll`) tandis que les exécutables `llama-*`/`ollama` à côté sont des shims lanceurs minces qui ne portent pas de kernels, donc une gate stricte échouerait sur les shims. Pour ce cas, jugez le set par son meilleur membre avec `--any` (CLI) ou `any: true` (Action). Le même flag couvre un build multi-variantes qui dlopen le meilleur de plusieurs libs single-ISA à l'exécution, donc le set est noté sur la variante qui charge réellement, pas sur son fallback armv8.0.

Sortie CI simulée de `coldpath --require i8mm ./build/` rapportant « 1 sur 1 binaires manquent i8mm : - lib/ollama/ggml-cpu.dll » avec un verdict rouge « PR failed » et la note « un build froid ne rejoint jamais votre flotte ».
La gate est une GitHub Action réutilisable publiée depuis ce dépôt : `uses: JonathanSolvesProblems/coldpath@v1` avec `require: i8mm`. C'est tout ce qu'il faut pour bloquer un build froid.

Vérité terrain, et limites honnêtes

L'outil est validé contre une vérité terrain qu'il n'a pas écrite. La release linux-arm64 officielle de llama.cpp expédie huit backends `ggml-cpu` avec le niveau ISA dans le nom de fichier, et `scripts/verify_ladder.py` reproduit cet escalier de huit lignes exactement (armv8.0 a zéro de tout, armv8.2 obtient 1 184 dotprod, armv8.2_3 ajoute 10 735 SVE, armv8.6 ajoute 402 i8mm, et ainsi de suite). Le wheel aarch64 d'ONNX Runtime est un contrôle positif prouvant qu'un zéro signifie une vraie absence, avec 469 SME, 800 i8mm, et 1 642 dotprod. La portée est déclarée d'entrée : sur Apple Silicon, la matmul peut passer par l'unité Accelerate/AMX, qu'un désassembleur ne peut pas voir, donc le titre est cadré à Linux/Neoverse, Windows-on-Arm, et Android où le chemin ISA est le seul chemin. coldpath rapporte ce qui est présent dans `.text` ; la présence égale ce qui s'exécute pour ggml (dispatch à la compilation) et est un indicateur d'expédition prouvée pour les libs à dispatch runtime.

Carte de marque coldpath : le wordmark « coldpath » en violet avec le slogan « does your Arm AI binary use the matrix hardware? » et la commande d'installation `$ pip install coldpath`. En dessous : « MIT · on PyPI · found a real one on day one ».
`pip install coldpath`, MIT, sur PyPI. L'outil a trouvé un vrai défaut dans un logiciel que des millions de personnes utilisent, dès son premier jour. Ensuite : distinguer SME de SME2, détecter le dispatch gaté à l'exécution comme troisième état, et faire grandir le scan d'écosystème en un tableau de bord public.
Projet associé

coldpath: The Arm AI Ecosystem Ships with the Matrix Unit Off. I Wrote the Tool that Finds It, Filed the One-Line Fix Upstream (Ollama PR #17654), and Gated It in CI.

Voir le projet