Méthodologie de benchmark
asiai suit les standards de benchmarking établis (MLPerf, SPEC CPU 2017, NVIDIA GenAI-Perf) pour produire des résultats fiables, reproductibles et comparables.
Protocole
- Vérification pré-vol : Refuser de démarrer si la pression mémoire est critique ou si le système est fortement throttlé (<80%)
- Warmup : 1 génération non chronométrée par moteur pour amorcer les compilateurs JIT et les caches
- Exécutions mesurées : Par défaut 3 exécutions par prompt par moteur (configurable via
--runs) - Échantillonnage :
temperature=0(greedy) pour une sortie déterministe - Déchargement du modèle : Après le benchmark de chaque moteur, le modèle est déchargé pour libérer la mémoire unifiée avant le prochain moteur. Cela empêche l'accumulation mémoire et le swapping lors de la comparaison de plusieurs moteurs sur de grands modèles
- Refroidissement adaptatif : Après le déchargement, asiai attend que la pression mémoire macOS revienne à « normal » (max 30s), puis ajoute un minimum de 5s de refroidissement thermique
- Contrôles de cohérence : Les résultats avec tok/s ≤ 0 sont rejetés. Un TTFT > 60s ou tok/s > 500 déclenche des avertissements (swapping probable ou erreurs de mesure)
- Reporting : Médiane tok/s comme métrique principale (standard SPEC), moyenne ± stddev en secondaire
- Throttling : Avertissement émis si
thermal_speed_limit < 100%pendant une exécution. La dérive thermique (diminution monotone des tok/s entre les exécutions, baisse ≥ 5%) est détectée et signalée - Métadonnées : Version du moteur, format du modèle, quantification, puce matérielle, version macOS stockés par résultat
Métriques
tok/s — Vitesse de génération
Tokens par seconde du temps de génération uniquement, hors traitement du prompt (TTFT).
Ollama (API native, /api/generate) :
tok_per_sec = eval_count / (eval_duration_ns / 1e9)
Moteurs compatibles OpenAI (LM Studio, llama.cpp, mlx-lm, vllm-mlx) :
generation_s = wall_clock_s - ttft_s
tok_per_sec = completion_tokens / generation_s
Comptage de tokens : depuis usage.completion_tokens dans la réponse du serveur. Si le serveur ne rapporte pas ce champ, asiai revient à len(text) // 4 et enregistre un avertissement. Ce repli peut être décalé d'environ 25%.
Validation croisée (avril 2026, Qwen3.5-35B NVFP4, M4 Pro 64GB) :
| Méthode | tok/s | Écart vs référence |
|---|---|---|
| Ollama native (GPU interne) | 66.6 | référence |
| OpenAI streaming (client) | 66.1 | -0.8% |
Pour les grandes tailles de contexte (ex. 64k tokens), le TTFT peut dominer la durée totale. L'exclure du tok/s empêche les générateurs rapides de paraître lents.
TTFT — Time to First Token
Temps entre l'envoi de la requête et la réception du premier token de sortie, en millisecondes.
Depuis la v1.6.0, asiai mesure deux valeurs TTFT pour Ollama, et une seule pour tous les autres moteurs :
Ollama (double mesure) :
- TTFT côté serveur (
ttft_ms) : extrait deprompt_eval_durationdans la réponse Ollama. C'est le temps pur de traitement GPU du prompt, sans aucune surcharge réseau — la mesure la plus précise possible. Rapporté commettft_source: server. - TTFT côté client (
ttft_client_ms) : mesuré à l'arrivée du premier chunk SSE de contenu. Inclut la configuration HTTP, la transmission de la requête et le traitement serveur. C'est la même méthode utilisée pour tous les autres moteurs.
Moteurs compatibles OpenAI (LM Studio, llama.cpp, mlx-lm, vllm-mlx) :
- TTFT côté client (
ttft_client_ms) : mesuré au premier chunk SSE de contenu. C'est la seule mesure disponible car ces moteurs n'exposent pas le timing interne de traitement du prompt.ttft_msetttft_client_mscontiennent la même valeur.
Métrique comparable : ttft_client_ms est la métrique comparable entre moteurs — elle utilise la même méthode de mesure quel que soit le moteur. Utilisez-la pour comparer le TTFT entre différents moteurs. Le ttft_ms côté serveur d'Ollama est plus précis pour le temps absolu de traitement du prompt, mais n'est pas directement comparable avec les autres moteurs.
Validation croisée (avril 2026, Qwen3.5-35B NVFP4, M4 Pro 64GB) :
| Méthode | TTFT | Écart |
|---|---|---|
Ollama côté serveur (ttft_ms) |
27 ms | référence |
Ollama côté client (ttft_client_ms) |
51 ms | +24 ms |
L'écart de 24ms représente la surcharge HTTP sur localhost. Cette surcharge est constante et prévisible, mais suffisamment significative pour compter lors de la comparaison entre moteurs.
Puissance — Watts GPU
Puissance GPU moyenne pendant l'exécution, mesurée via le framework Apple IOReport Energy Model (sans sudo requis). Une mesure par moteur — pas de moyenne sur la session entière.
tok/s/W — Efficacité énergétique
tok_per_sec_per_watt = tok_per_sec / power_watts
Variance — Stddev poolée
Écart-type intra-prompt poolé qui capture le bruit inter-exécutions sans mélanger la variance inter-prompts. Utilise la correction de Bessel (dénominateur N-1) pour une variance d'échantillon non biaisée.
Classification de stabilité :
- CV < 5% →
stable - CV < 10% →
variable - CV >= 10% →
unstable
Où CV = (std_dev / mean) * 100.
VRAM — Utilisation mémoire
Primaire : API native du moteur (Ollama /api/ps, LM Studio /v1/models).
Repli : ri_phys_footprint via ctypes (identique au Moniteur d'activité). Marqué « (est.) » dans l'interface.
Mode Agentique — Benchmark de réutilisation du préfixe en cache
Les benchmarks single-shot classiques mesurent la vitesse de génération en isolation. Ils passent à côté du pattern de coût dominant des workloads agentiques multi-tour : un long prompt système partagé (outils, règles, persona — souvent 6K+ tokens) plus un message utilisateur court qui change à chaque tour. Un moteur qui ne réutilise pas le préfixe en cache re-traite ces 6K tokens à chaque appel, et le TTFT explose.
asiai bench --agentic-mode exécute un protocole en 8 phases conçu
pour exposer ce comportement de manière explicite.
Protocole
| Phase | Système | Utilisateur | max_tokens | Objectif |
|---|---|---|---|---|
cold |
SYS_A | USER_X | 400 | Premier run, cache vide |
warm |
SYS_A | USER_X | 400 | Même requête — full cache hit |
prefix-test-1 |
SYS_A | USER_Y | 400 | Sys identique, user différent — le vrai test |
prefix-test-2 |
SYS_A | USER_X | 400 | Retour à USER_X — cache hit attendu |
prefix-test-3 |
SYS_A | USER_Y | 400 | Répète le pattern cross-user |
cold-prefix |
SYS_B | USER_X | 400 | Le sys change — cache miss attendu |
long-context |
SYS_A | USER_L (~50K tok) | 200 | Saturer le decode sur contexte long |
long-prefix |
SYS_A | USER_L | 200 | Même contexte long — cache hit |
Les prompts sont générés de façon déterministe avec un pattern sentinelle qui casse les caches substring naïfs ; les tailles sont calibrées pour les tokenizers de la famille Qwen (~5.3 char/token sur prose anglaise).
Verdict
prefix_cache_reuse est calculé sur les phases prefix-test :
- Signal primaire — si le moteur expose
usage.prompt_tokens_details.cached_tokensdans sa réponse streaming (llama.cpp, mlx-lm), le ratiocached / promptest moyenné sur les phases prefix-test : ≥ 0.5→yes≥ 0.1→partial- sinon →
no - Signal de repli — si le moteur n'expose pas
cached_tokens(LM Studio, vllm-mlx, oMLX), le ratio TTFT est utilisé : TTFT prefix-test rapporté au TTFT cold. < cold/5→yes< cold/2→partial- sinon →
no
Quality gates
Trois gates tournent en parallèle du bench et apparaissent dans
result["quality_gates"] :
early_stop— flagge les phases oùcompletion_tokenschute sous 50 % dumax_tokensdemandé sur au moins deux runs. Détecte les bugs moteur où un token EOS spéculé est accepté à tort sous réutilisation du préfixe — la réponse reste valide en OpenAI-compat mais le moteur retourne silencieusement des réponses tronquées.memory_pressure— un thread d'arrière-plan pollvm_statetvm.swapusagetoutes les 15 s avec une baseline prise au démarrage. Alerte quand le swap monte de plus de 500 MB ou les swapouts de plus de 1000 depuis la baseline. Les deux indiquent que l'OS pagine le modèle ou le KV cache sur disque, donc letok/smesuré ne représente plus le moteur lui-même.duplicate_processes— un snapshotpsunique avant le bench rejette les runs où deux instances du même moteur tournent en parallèle, l'une risquant de se battre avec le bench pour le GPU.
Quand une gate déclenche, la CLI affiche un warning rouge sous la ligne de verdict et la sortie JSON garde tous les samples détaillés pour qu'un leaderboard ou un tracker de régression puisse refuser de publier le résultat.
Cold start reproductible (intégration aisctl opt-in)
asiai bench --agentic-mode --agentic-auto-restart appelle
aisctl restart <engine> avant la première phase et poll /health
jusqu'à disponibilité. Utile pour les moteurs sans API d'unload
(llama.cpp, oMLX, TurboQuant) où un restart du daemon est le seul
moyen fiable de vider le KV cache. Ajouter
--agentic-auto-restart-required pour abandonner au lieu de continuer
si aisctl n'est pas disponible.
Cette intégration nécessite
asiai-inference-server
installé ; sinon le bench affiche un warning et continue contre l'état
courant du moteur.
Pourquoi c'est important
Un chiffre tok/s single-shot est peu informatif pour les workflows
agentiques quand le moteur ne réutilise pas le préfixe système. Deux
moteurs avec un throughput single-shot identique peuvent différer de
5 à 10× sur la latence d'un tick agentique selon que le préfixe
en cache tient ou non.
agentic-mode expose cet écart explicitement pour que le leaderboard
et les choix de moteur reflètent le workload dominant, pas un
microbenchmark.
Sécurité de l'environnement
asiai effectue des vérifications pré-benchmark :
- Pression mémoire : refuse de démarrer si critique
- Throttling thermique : avertit si la limite de vitesse < 80%
- Processus dupliqués : avertit si plusieurs instances du même moteur sont en cours (ex. deux processus
ollama servesur le même port) - Type de runner du moteur : pour Ollama, détecte si le runner
--mlx-engineou--ollama-engineest actif
Ces vérifications préviennent les erreurs de mesure causées par la contention de ressources ou le routage incorrect.
Conformité
| Pratique | Statut |
|---|---|
| Vérification pré-vol (pression mémoire + thermique) | Implémenté |
| Détection de processus dupliqués | Implémenté (v1.5.0) |
| Détection du type de runner Ollama (MLX vs llama.cpp) | Implémenté (v1.5.0) |
| TTFT séparé du tok/s | Implémenté |
| Étiquetage de la source TTFT (server vs client) | Implémenté (v1.5.0) |
| Double mesure TTFT (server + client) | Implémenté (v1.6.0) |
| Échantillonnage déterministe (temperature=0) | Implémenté |
| Comptage de tokens via l'API serveur (pas les chunks SSE) | Implémenté (avertissement en repli) |
| Monitoring de puissance par moteur (IOReport, sans sudo) | Implémenté |
| 1 génération de warmup par moteur | Implémenté |
| 3 exécutions par défaut (minimum SPEC) | Implémenté |
| Médiane comme métrique principale (standard SPEC) | Implémenté |
| Stddev intra-prompt poolée (Bessel N-1) | Implémenté (corrigé v1.5.0) |
| Déchargement du modèle entre les moteurs | Implémenté |
| Refroidissement adaptatif (sensible à la pression mémoire) | Implémenté |
| Contrôles de cohérence (tok/s, bornes TTFT) | Implémenté |
| Détection du throttling thermique + avertissement | Implémenté |
| Détection de la dérive thermique (diminution monotone) | Implémenté |
| Version du moteur + type de runner stockés par résultat | Implémenté (v1.5.0) |
| VRAM universelle via ri_phys_footprint | Implémenté |
| Détection de régression historique | Implémenté |
| Script de validation croisée (3 méthodes comparées) | Disponible (scripts/cross-validate-bench.py) |
Considérations Apple Silicon
Mémoire unifiée
Apple Silicon partage la mémoire entre CPU et GPU. asiai exécute les moteurs séquentiellement et décharge les modèles entre les moteurs pour éviter la contention mémoire et le swapping. La VRAM est rapportée nativement par Ollama et LM Studio ; pour les autres moteurs, asiai estime l'utilisation mémoire via ri_phys_footprint (la métrique d'empreinte physique macOS, identique au Moniteur d'activité). Les valeurs estimées sont étiquetées « (est.) » dans l'interface.
Throttling thermique
- MacBook Air (sans ventilateur) : throttling sévère sous charge soutenue
- MacBook Pro (ventilateur) : throttling léger
- Mac Mini/Studio/Pro : refroidissement actif, throttling minimal
asiai enregistre thermal_speed_limit par résultat et avertit si un throttling est détecté.
KV Cache
Les grandes tailles de contexte (32k+) peuvent causer de l'instabilité sur les moteurs qui pré-allouent le KV cache. Réglez la longueur de contexte du moteur pour correspondre à la taille réelle du test pour des résultats équitables.
Mesure de puissance
asiai mesure la consommation GPU, CPU, ANE et DRAM via le framework Apple IOReport Energy Model — sans sudo requis. La puissance est mesurée automatiquement dans chaque benchmark et chaque snapshot de monitoring.
IOReport lit les mêmes compteurs d'énergie matériels que sudo powermetrics, mais via une API en espace utilisateur (libIOReport.dylib via ctypes). Cela élimine le besoin de configuration sudo sans mot de passe.
Validation
Nous avons validé IOReport par rapport à sudo powermetrics sous charge d'inférence LLM sur M4 Pro 64 Go, avec 10 échantillons appariés par moteur à intervalles de 2 secondes :
| Moteur | Moy. IOReport | Moy. powermetrics | Écart moyen | Écart max |
|---|---|---|---|---|
| LM Studio (MLX) | 12.6 W | 12.6 W | 0.9% | 2.1% |
| Ollama (llama.cpp) | 15.6 W | 15.4 W | 1.3% | 4.1% |
Les deux moteurs confirment un écart moyen <1,5% avec 10/10 échantillons appariés. La puissance ANE était de 0.000W sur les 20 échantillons, confirmant qu'aucun moteur LLM n'utilise actuellement le Neural Engine.
Le flag --power active une validation croisée supplémentaire en exécutant simultanément IOReport et sudo powermetrics, stockant les deux mesures pour comparaison.
Efficacité énergétique
L'efficacité énergétique (tok/s par watt) est calculée comme tok_per_sec / gpu_watts pour chaque résultat de benchmark. Cette métrique permet de comparer le coût d'inférence entre moteurs et matériels.
Métadonnées
Chaque résultat de benchmark stocke : engine, engine_version, model, model_format, model_quantization, hw_chip, os_version, thermal_level, thermal_speed_limit, power_watts, power_source, metrics_version. Cela permet une comparaison de régression équitable et des benchmarks inter-machines.