4.5 KiB
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_archexplicite → 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*.soet versionqnn-runtime)
Pourquoi c'est sans risque
- Code path identique : c'est de la config ORT, pas un changement de modèle ni de pipeline.
- Réversible : 4 lignes à retirer si jamais ça régresse.
- Compatibilité :
htp_arch=79est explicite mais valide sur SD8 Elite. Sur autre device (Gen3 = V75), il faudrait conditionner surBuild.SOC_MANUFACTURER/SOC_MODEL. Pour Kazeia mono-tablette (Pad3 = SM8750 = V79), c'est sûr. - 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 à :
- Patch QNN posé et mesuré sur audio Pierre ✓
- A/B accuracy CER/WER lib unifiée vs prod patchée sur les mêmes audios — à mesurer in-app
- Si #2 passe (CER ≤ +5 % relatif vs prod), la migration vaut le coup pour les -168 MB et la cohérence code
- 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 (WhisperHybridEngine → SttEngine swap derrière SttStage) — c'est la bonne façon de mesurer sans s'engager.