Kazeia-engine/dist/PATCH_QNN_PROD.md

4.5 KiB
Raw Blame History

Patch QNN options pour la prod gelée — sans migration

Action 1 isolée de la décision migration libkazeia_stt. Ce patch ne touche pas le chemin code, juste les options ORT passées à QNN. Réversible en 4 lignes.

Constat

Dans WhisperHybridEngine.kt:101-102 actuel :

val encOpts = OrtSession.SessionOptions()
encOpts.addQnn(mapOf("backend_path" to htpPath))

Ces options QNN par défaut donnent :

  • htp_performance_mode = balanced (scheduler équilibré, pas freq max NPU)
  • pas de htp_arch explicite → ORT auto-détecte, fallback générique sur certains paths
  • pas de enable_htp_fp16_precision → conversion implicite parfois sub-optimale
  • pas de SetGraphOptimizationLevel(ALL_OPT) activé explicitement

Coût mesuré sur SD8 Elite (SM8750, Whisper-Small) : encoder 309 ms au lieu de 62 ms, decoder 48 ms/tok au lieu de 14 ms/tok.

Patch (4 lignes Kotlin)

Dans WhisperHybridEngine.kt, remplacer les blocs encOpts/decOpts :

val encOpts = OrtSession.SessionOptions()
encOpts.addQnn(mapOf(
    "backend_path"              to htpPath,
    "htp_performance_mode"      to "burst",
    "htp_arch"                  to "79",
    "enable_htp_fp16_precision" to "1",
))
encOpts.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT)

Idem pour decOpts. Et symmetric pour le fallback encoder_npu path si présent (ligne 134 environ).

Gain mesuré attendu

Sur le port C++ avec exactement ces options, mesures stables 5 runs warm (audio FR continu, 12 tokens / 1.6 s) :

Étape Sans options Avec burst+v79+fp16
Encoder 309 ms 62 ms
Decoder/token 48 ms 14 ms
Total (12 tok) 962 ms 320 ms

Hypothèse cohérente : sur la prod Kotlin actuelle (820 ms / 22 tok), avec patch :

  • Encoder 125 → ~62 ms
  • Decoder 22 × 23 → 22 × ~14 ms = 308 ms
  • Total ≈ 560 ms (-32 %)

À valider sur audio Pierre réel. L'extrapolation est mathématique, pas un bench prod direct.

Script de bench audio Pierre

Pour valider le gain sur les audios prod typiques (phrases courtes thérapeutiques) :

// Dans un mode debug de l'app, ou un test Android instrumenté :
val engine = WhisperHybridEngine(nativeLibDir)
engine.load(modelPath = WHISPER_DIR)

// Charge audio Pierre déjà capturé en prod (ShortArray PCM16 16kHz mono)
val samples: List<Pair<String, ShortArray>> = loadPierrAudioSamples()  // 10-20 samples

samples.forEach { (name, pcm) ->
    val t0 = System.currentTimeMillis()
    val r = engine.transcribe(pcm, "fr")
    val dt = System.currentTimeMillis() - t0
    Log.i("BENCH", "$name : ${pcm.size / 16000.0}s audio, $dt ms total, text='${r.text}'")
}

Compare le dt total avec et sans patch sur les mêmes audios :

  • Si gain ≥ 25 % en moyenne : patch validé, à merger en l'état
  • Si gain < 25 % : enquêter (probable que l'app n'utilise pas les bonnes libs QNN, vérifier jniLibs/arm64-v8a/libQnnHtp*.so et version qnn-runtime)

Pourquoi c'est sans risque

  1. Code path identique : c'est de la config ORT, pas un changement de modèle ni de pipeline.
  2. Réversible : 4 lignes à retirer si jamais ça régresse.
  3. Compatibilité : htp_arch=79 est explicite mais valide sur SD8 Elite. Sur autre device (Gen3 = V75), il faudrait conditionner sur Build.SOC_MANUFACTURER/SOC_MODEL. Pour Kazeia mono-tablette (Pad3 = SM8750 = V79), c'est sûr.
  4. Pas de dégel STT : le contexte QAIRT est inchangé, le modèle est inchangé, la classe Kotlin est inchangée hors les 4 lignes.

Après ce patch, la décision migration libkazeia_stt

Le delta restant entre prod patchée et lib unifiée :

Prod + patch QNN Lib unifiée
Total (22 tok / 1.6 s) ~560 ms ~450 ms
Delta -110 ms
RAM 545 MB 377 MB (-168 MB)
Code îlot STT (527 l Kotlin + 203 l C++ mel + 197 l VAD) unifié dans libkazeia_stt.so + SttEngine.kt

Décision migration conditionnée à :

  1. Patch QNN posé et mesuré sur audio Pierre ✓
  2. A/B accuracy CER/WER lib unifiée vs prod patchée sur les mêmes audios — à mesurer in-app
  3. Si #2 passe (CER ≤ +5 % relatif vs prod), la migration vaut le coup pour les -168 MB et la cohérence code
  4. Si #2 dégrade, on garde la prod patchée et la lib unifiée reste un POC

L'A/B accuracy reste le seul point dur non mesurable en standalone. Le dev offre un patch derrière l'interface existante (WhisperHybridEngineSttEngine swap derrière SttStage) — c'est la bonne façon de mesurer sans s'engager.