Aller au contenu principal

Module 9 — Mesure de la latence et de la consommation énergétique

Le module 1 a posé les budgets, les modules 3 à 5 ont donné des chiffres indicatifs, le module 6 a suggéré des délégués. Rien de tout cela ne remplace une mesure sérieuse sur le matériel visé. Ce module apprend à produire des chiffres reproductibles, à lire les percentiles au lieu des moyennes, et à quantifier la consommation d'énergie que l'inférence coûte à la batterie de l'utilisateur.

L'outil officiel : benchmark_model

TensorFlow Lite fournit un binaire de mesure qui s'installe soit en téléchargeant l'APK Android dédié, soit en le compilant depuis le dépôt source. Il exécute le modèle avec des entrées aléatoires, mesure la latence, la consommation mémoire, et permet d'activer chaque délégué pour comparer.

Sur Android, une commande typique :

adb push modeles/plantvillage_qat_elague.tflite /data/local/tmp/
adb push benchmark_model /data/local/tmp/
adb shell chmod +x /data/local/tmp/benchmark_model
adb shell /data/local/tmp/benchmark_model \
--graph=/data/local/tmp/plantvillage_qat_elague.tflite \
--num_threads=4 \
--num_runs=100 \
--warmup_runs=10 \
--use_nnapi=true \
--report_peak_memory_footprint=true

Cinq drapeaux comptent vraiment :

  • --num_runs=100 : nombre d'inférences mesurées. Cent est un minimum pour extraire un p95 stable ; mille est mieux pour un p99.
  • --warmup_runs=10 : nombre d'inférences ignorées avant la mesure. Le premier appel alloue les tenseurs, initialise les délégués, lance les kernels GPU si nécessaire. Sans échauffement, la première inférence peut peser 400 ms sur un total de 100 mesures et fausser la médiane.
  • --use_nnapi / --use_gpu / --use_xnnpack : pour comparer les délégués sur exactement le même modèle et la même série de tirages.
  • --report_peak_memory_footprint=true : la mémoire au pic, qui est la vraie contrainte (le module 1 fixait < 60 Mo).

Pourquoi les moyennes mentent, et pourquoi les percentiles éclairent

Une latence moyenne de 120 ms cache trois réalités possibles :

  • Cas idéal : toutes les inférences autour de 120 ms, p95 à 135 ms. La moyenne est représentative.
  • Cas moyen : la majorité à 90 ms, quelques-unes à 250 ms à cause du contrôle thermique. La moyenne monte à 120 ms mais aucune inférence n'y tombe vraiment.
  • Cas mauvais : 60 % à 60 ms, 40 % à 210 ms à cause d'un repli sur CPU intermittent. La moyenne dit 120 ms, l'utilisateur voit un « clignotement » de performance.

Le p50 (médiane), p95 et p99 racontent des histoires différentes et rendent visible la variance. La règle de terrain :

  • p50 est la latence typique.
  • p95 est la latence perçue par l'utilisateur (une image sur 20 est aussi lente ou plus).
  • p99 est la latence qui provoquera les mauvais avis (« ça bloque »).

Un modèle avec p50 = 100 ms et p99 = 800 ms est pire, du point de vue utilisateur, qu'un modèle avec p50 = 150 ms et p99 = 200 ms, malgré une médiane plus élevée. La stabilité vaut mieux que la vitesse crête.

Un script Python de mesure fine

Quand benchmark_model ne suffit pas — pour intégrer les mesures dans un tableau de bord, ou pour tester des scénarios précis (rafales, pauses, images de résolutions différentes) — un script maison en Python garde le contrôle :

import tensorflow as tf, numpy as np, time

interprete = tf.lite.Interpreter(
model_path="modeles/plantvillage_qat_elague.tflite",
num_threads=4,
)
interprete.allocate_tensors()
entree = interprete.get_input_details()[0]

# Echauffement : 10 appels ignores
image = np.random.randint(0, 256, size=(1, 224, 224, 3), dtype=np.uint8)
for _ in range(10):
interprete.set_tensor(entree["index"], image)
interprete.invoke()

# Mesure : 200 appels chronometres
temps = []
for _ in range(200):
t0 = time.perf_counter()
interprete.set_tensor(entree["index"], image)
interprete.invoke()
temps.append((time.perf_counter() - t0) * 1000) # ms

temps.sort()
print(f"p50 : {temps[100]:.1f} ms")
print(f"p95 : {temps[190]:.1f} ms")
print(f"p99 : {temps[198]:.1f} ms")
print(f"moyenne : {sum(temps)/len(temps):.1f} ms")

Ce script se lance sur un ordinateur pour un premier tri des variantes, puis se traduit en Kotlin ou Swift pour mesurer sur les appareils cibles avec les vraies images du domaine.

Mesurer l'énergie n'est plus optionnel

La latence dit ce que l'utilisateur perçoit ; l'énergie dit ce que la batterie consomme et à quelle vitesse le téléphone chauffe. Deux méthodes existent.

Méthode Android (Battery Historian). L'outil dumpsys batterystats d'Android capture les consommations par processus. Un script court :

# Reinitialiser les stats, lancer l'application, faire N inferences, exporter
adb shell dumpsys batterystats --reset
# ... l'application est lancee et execute 1000 inferences ...
adb shell dumpsys batterystats > stats.txt
python -m historian.py stats.txt > energie.html

Battery Historian donne la consommation en milliampères-heures par processus, qu'on divise par le nombre d'inférences pour obtenir un coût unitaire. Pour le fil rouge sur Galaxy A15, une inférence NNAPI coûte environ 0,003 mAh, soit environ 7 000 inférences par 1 % de batterie sur une batterie de 5 000 mAh — largement au-dessus du budget du module 1.

Méthode iOS (Instruments Energy Log). Xcode intègre un profileur d'énergie qui donne une note qualitative (« Low / Medium / High ») et un graphique des composants (CPU, GPU, réseau) responsables. Moins précis que Battery Historian, mais suffisant pour comparer deux variantes du modèle.

Le contrôle thermique change les mesures

Une série de 1 000 inférences consécutives, comme celles d'un benchmark, n'est pas représentative de l'usage réel où l'utilisateur prend une photo toutes les cinq secondes. Le contrôle thermique du processeur intervient après trente à soixante secondes de charge soutenue et peut faire chuter la fréquence de 20 à 40 %.

Pour une mesure représentative :

  • Cas usage réel : introduire une pause entre les inférences (par exemple time.sleep(3) dans le script Python). Le p95 mesuré est alors celui que l'utilisateur verra.
  • Cas usage soutenu (traitement vidéo temps réel) : mesurer sans pause sur au moins deux minutes. Le p95 après une minute inclut la baisse thermique.

Ne pas mélanger les deux : un modèle qui tient 30 ms en pointe et 60 ms sous contrôle thermique est parfait pour un cas ponctuel et catastrophique pour un cas continu. Le module 10 tranche selon le scénario du fil rouge.

Le tableau final de mesures pour le fil rouge

Après application des modules 2 à 8, sur la variante retenue (2,1 Mo, int8 QAT + élagage 60 %) et avec le meilleur délégué disponible par appareil :

AppareilDéléguép50p95p99Énergie/inf.
Nokia G21NNAPI65 ms82 ms128 ms0,005 mAh
Galaxy A15NNAPI30 ms41 ms68 ms0,003 mAh
iPhone 12Core ML12 ms18 ms24 ms0,001 mAh

Les trois appareils passent le budget de 150 ms au p95. Le Nokia G21 est le plus proche de la limite : son p99 à 128 ms laisse peu de marge, et une photo prise pendant que le téléphone chauffe (traitement d'une deuxième image dans les cinq secondes) fait grimper la latence à 200-250 ms. C'est l'usage limite acceptable, et cela colore les choix d'ergonomie du module 10 (pas de rafale, retour visuel immédiat).

En résumé

  • benchmark_model est l'outil canonique, avec --warmup_runs=10 et --num_runs >= 100 pour extraire des percentiles utiles.
  • Raisonner en p50, p95, p99 ; la moyenne cache la variance qui fait la différence entre une expérience fluide et une expérience clignotante.
  • L'échauffement est obligatoire : la première inférence peut être 10 fois plus lente que les suivantes.
  • L'énergie se mesure avec Battery Historian (Android) ou Instruments (iOS) ; l'inférence n'est presque jamais le premier consommateur, sauf en vidéo temps réel.
  • Le contrôle thermique change les mesures selon le scénario : simuler l'usage réel (pauses) ou l'usage soutenu (continu), jamais les mélanger.

Module suivant : l'application complète du fil rouge, du tableau des variantes au choix final et à la mise à jour du modèle.