Critique du dev Kazeia juste : la comparaison RTF 0.18 (10s) vs 0.51 (1.6s) prod
était pommes/oranges (encoder fixe amorti sur audio long), et l'encoder +152 %
non investigué cachait un vrai problème de config QNN par défaut.
Investigation : options QNN ExecutionProvider explicites manquaient
- htp_performance_mode = "burst" (au lieu de default = balanced)
- htp_arch = "79" (au lieu de auto-detect)
- enable_htp_fp16_precision = "1" (NPU fp16 natif)
- SetGraphOptimizationLevel(ORT_ENABLE_ALL) + DisableMemPattern()
Impact mesuré (5 runs warm, audio FR continu, 12 tokens / 1.6s) :
- Encoder : 309 ms -> 62 ms (-80 %)
- Decoder/token : 48 ms -> 14 ms (-71 %)
- Total : 962 ms -> 320 ms (-67 %)
- RAM peak : 377 MB inchangée
Cas réel Kazeia (1.6s, extrapolé 22 tokens) :
- Prod Kotlin : 820 ms (mel 189 + enc 125 + 22*23)
- C++ unifié : ~450 ms (mel 80 + enc 62 + 22*14)
- Gain réel : -45 % sur le workload réel (pas amortissement)
Audio long (15s, 71 tokens) : decoder 10 ms/tok, RTF 0.06.
Mode warm bench (KZSTT_RUNS=N) : exerce N transcribes successifs sur le même
engine pour distinguer cold-start (1er run) vs warm path (suivants).
Recommandation prod Kotlin : ajouter les mêmes options QNN (encOpts.addQnn
multimap) pour gagner sans migration. Documenté dans STT_INTEGRATION.md §7bis.
A/B accuracy : 10 utterances FR transcrites sortent du texte FR cohérent
(avec erreurs typiques Whisper-Small sur mots rares : "Vagons" vs "Wagons",
"zéfière" vs "zéphyr"). La comparaison CER/WER vs WhisperHybridEngine sur
les mêmes fichiers reste à faire côté app (checklist §9.2) — non faisable
standalone sans la prod Kotlin en CLI.
Remerciements : critique du dev a permis de trouver une optim qui sert
TANT la version C++ unifiée QUE la prod Kotlin actuelle.
Optim decoder identifiée après bench sur audio FR continu (39 tokens) :
le `std::vector<>::emplace_back(data)` à chaque step recopiait 12 MB de
self KV inutilement = 2.4 GB de bande passante mémoire sur le loop entier.
Refactor zero-copy :
- Pré-allocation UNE FOIS des buffers persistants (self_k/v, mask, input_ids,
position_ids) au début de transcribe()
- Pré-création UNE FOIS des Ort::Value pointant sur ces buffers (les Ort::Value
sont des "borrowing views" — ORT lit les buffers au Run())
- Cache des indices k_self_out / v_self_out / logits_out (parsing 1 fois,
pas N fois dans le loop)
- Update self KV : memcpy direct depuis dec_outputs vers nos buffers persistants
(qui sont déjà les inputs du step suivant)
- mask_buf updaté à 1 fp16 par step (juste le slot R-to-L), pas 200
Bench zero-copy sur Snapdragon 8 Elite :
- Decoder/token : 98 ms -> 36 ms (-63%)
- Decoder total 39 tokens : 3831 ms -> 1390 ms (-64%)
- RTF audio 10s FR continu : 0.42 -> 0.18 (mieux que prod 0.51)
- RAM peak : 378 MB inchangée (zero-copy n'augmente pas la peak)
Sanity vérifié au runtime :
- Logits decoder shape = 51865 elements (= constante VOCAB_SIZE OK,
pas de buffer over-read sur argmax). Le vocab.json contient 50258 tokens
texte, les 1607 manquants sont les tokens langue/timestamp non décodés.
Doc STT_INTEGRATION.md :
- Chiffres perf à jour (RTF 0.18, decoder 36 ms/token)
- Nouvelle section "Checklist d'intégration" (6 points à vérifier en premier
côté app : System.loadLibrary, transcribe vs WhisperHybridEngine, null/edge,
VAD drop-in, threading IO, cycle de vie release)
- Roadmap optim future via Ort::IoBinding (gain estimé 30% sur decoder restant)