chantier B STT #4 : audit dev pris en compte — options QNN explicites + bench apples-to-apples
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.
This commit is contained in:
parent
970f731e01
commit
c9e198332d
|
|
@ -5,22 +5,37 @@
|
|||
|
||||
**Runtime** : ONNX Runtime 1.24.3 + QNN ExecutionProvider HTP V79 — **inchangé**. Pas de régression NPU (les contextes QAIRT Qualcomm 545 MB sont conservés tels quels).
|
||||
|
||||
**Perf de référence** (mesures sur Snapdragon 8 Elite / SM8750, Whisper-Small, FR continu, decoder zero-copy) :
|
||||
**Perf de référence** (Snapdragon 8 Elite / SM8750, Whisper-Small FR, warm path) :
|
||||
|
||||
| Métrique | Prod actuelle (Kotlin) | Lib unifiée (C++ zero-copy) | Gain |
|
||||
Cas réel Kazeia (audios ~1.6 s, parole dense, **apples-to-apples par étape** vs prod) :
|
||||
|
||||
| Étape | Prod Kotlin (22 tok) | C++ (warm, 12 tok mesuré) | Extrapol 22 tok C++ |
|
||||
|---|---:|---:|---:|
|
||||
| Mel extraction | 189 ms (DFT brute force) | **57 ms** (FFT radix-2) | -70 % |
|
||||
| Encoder NPU | 125 ms | 315 ms* | +152 % |
|
||||
| Decoder/token NPU | 23 ms | **36 ms** | +56 % |
|
||||
| **RTF (10 s audio, 39 tokens FR continu)** | 0.51 (sur 1.6 s, 22 tokens) | **0.18** | **2.8× mieux** |
|
||||
| **RAM peak** | 545 MB | **379 MB** | **-30 %** |
|
||||
| Mel | 189 ms | 80 ms | 80 ms |
|
||||
| Encoder | 125 ms | **62 ms** | 62 ms |
|
||||
| Decoder/token | 23 ms | **14 ms** | 14 ms |
|
||||
| Decoder total | 506 ms | 164 ms (12 tok) | ~308 ms |
|
||||
| **Total** | **820 ms** | **320 ms (12 tok)** | **~450 ms (-45 %)** |
|
||||
| **RAM peak** | 545 MB | **377 MB** | **-31 %** |
|
||||
|
||||
\* L'encoder C++ reste localement plus lent qu'ORT Java (overhead de l'initial Ort::Value bound — différence d'optim interne ORT). Sur le RTF global et la RAM, **la version C++ bat la prod**. Le decoder a été optimisé en zero-copy (Ort::Value persistants pointant sur les buffers self_k/v, memcpy in-place des outputs) — gain x2.5 vs version initiale.
|
||||
Cas audio long (10-15 s, parole continue, montre l'amortissement encoder) :
|
||||
|
||||
**Vérifs sanity faites au runtime** :
|
||||
- Logits decoder shape = 51865 (= constante `VOCAB_SIZE`), pas de buffer overread sur argmax
|
||||
- vocab.json (50258 tokens) + tokens spéciaux Whisper (1607 langue/timestamp) couvrent les 51865 logits
|
||||
- Transcription FR cohérente sur 5 audios différents (damien, richard 1.6/3/5/10 s)
|
||||
| Audio | Tokens | Total | RTF | Decoder/tok |
|
||||
|---|---:|---:|---:|---:|
|
||||
| 10 s richard FR | 39 | 1390 ms | 0.14 | 36 ms |
|
||||
| 15 s richard FR | 71 | 896 ms (warm) | 0.06 | 10 ms |
|
||||
|
||||
**Clé du gain : options QNN explicites** (cf §X plus bas). Sans ces options, l'encoder est ~5× plus lent (par défaut QNN tourne en mode `default` au lieu de `burst`, n'identifie pas l'arch V79, n'active pas le fp16 natif).
|
||||
|
||||
**Decoder/token 14 ms** (-39 % vs prod 23 ms) grâce à :
|
||||
1. **Options QNN** : burst + v79 + enable_htp_fp16_precision = NPU à fréquence max
|
||||
2. **Zero-copy** : `Ort::Value` persistants pointant sur les buffers self_k/v (pas de `vector::emplace_back` qui copiait 12 MB/step)
|
||||
|
||||
**Vérifs sanity au runtime** :
|
||||
- Logits shape = 51865 (= constante `VOCAB_SIZE`), pas de buffer overread
|
||||
- 5 runs warm successifs : encoder 60-63 ms stable, decoder 160-162 ms stable, RAM 377 MB inchangée
|
||||
- Transcription FR cohérente sur 10+ audios différents (damien, richard 1.6/3/5/10/15 s)
|
||||
- Texte produit lisible et plausible. **A/B comparison vs `WhisperHybridEngine` Kotlin sur les mêmes fichiers reste à faire côté app** — c'est le seul moyen de quantifier le drift FP16 NPU éventuel (cf checklist §9 point 2).
|
||||
|
||||
\* Decoder et encoder C++ légèrement plus lents qu'en ORT Java à cause d'overhead memcpy fp16 ; optim future via IO bindings ORT. Le RTF global reste sous la cible.
|
||||
|
||||
|
|
@ -244,6 +259,38 @@ total 684 ms for 2.50 s audio => RTF 0.274
|
|||
|
||||
---
|
||||
|
||||
## 7bis. Options QNN HTP V79 — critique pour la perf
|
||||
|
||||
Le code C++ active explicitement ces options dans `stt_engine_load` (différence avec la prod Kotlin qui les laisse par défaut) :
|
||||
|
||||
```cpp
|
||||
opts.AppendExecutionProvider("QNN", {
|
||||
{"backend_path", "libQnnHtp.so"},
|
||||
{"htp_performance_mode", "burst"}, // NPU freq max (vs default = balanced)
|
||||
{"htp_arch", "79"}, // SM8750 explicite
|
||||
{"enable_htp_fp16_precision", "1"}, // NPU fp16 natif
|
||||
{"profiling_level", "off"},
|
||||
{"rpc_control_latency", "100"},
|
||||
});
|
||||
opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
|
||||
opts.DisableMemPattern();
|
||||
```
|
||||
|
||||
**Impact mesuré** : sans ces options, encoder à 309 ms et decoder à 48 ms/token. Avec, encoder à 62 ms et decoder à 14 ms/token (-80 % et -71 %).
|
||||
|
||||
Recommandation pour le prod Kotlin actuel : ajouter ces options dans `WhisperHybridEngine.kt` aussi pour gain immédiat même sans migration. Syntaxe Java :
|
||||
|
||||
```kotlin
|
||||
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.optimizationLevel = OrtSession.SessionOptions.OptLevel.ALL_OPT
|
||||
```
|
||||
|
||||
## 8. Notes pour optim future (non bloquant, opt-in)
|
||||
|
||||
Le decoder C++ est ~1.5× plus lent par token que la prod Kotlin (36 ms vs 23 ms). Reste de l'écart probablement dans :
|
||||
|
|
|
|||
|
|
@ -82,17 +82,24 @@ int main(int argc, char** argv) {
|
|||
tc.language = lang;
|
||||
tc.force_transcribe = true;
|
||||
|
||||
auto R = stt_engine_transcribe(eng, tc);
|
||||
if (R.err) { fprintf(stderr, "transcribe FAIL err=%d\n", R.err); stt_engine_free(eng); return 5; }
|
||||
// Mode warm-bench : si KZSTT_RUNS=N défini, fait N transcribes successifs sur le
|
||||
// même engine et reporte chacun. Utile pour distinguer cold-start (1er run lent)
|
||||
// vs warm path (runs suivants).
|
||||
int n_runs = 1;
|
||||
if (const char* p = std::getenv("KZSTT_RUNS")) { int v = atoi(p); if (v > 0) n_runs = v; }
|
||||
|
||||
printf("\n=== TRANSCRIPTION (%s, %s) ===\n", use_htp ? "NPU" : "CPU", lang);
|
||||
for (int run = 0; run < n_runs; ++run) {
|
||||
auto R = stt_engine_transcribe(eng, tc);
|
||||
if (R.err) { fprintf(stderr, "transcribe[%d] FAIL err=%d\n", run, R.err); stt_engine_free(eng); return 5; }
|
||||
const char * tag = (n_runs > 1) ? (run == 0 ? "[cold]" : "[warm]") : "";
|
||||
if (run == 0 || n_runs == 1) {
|
||||
printf("\n=== TRANSCRIPTION (%s, %s) %s ===\n", use_htp ? "NPU" : "CPU", lang, tag);
|
||||
printf("%s\n", R.text.c_str());
|
||||
printf("\n=== TIMING ===\n");
|
||||
printf("mel %4d ms\n", R.mel_ms);
|
||||
printf("encoder %4d ms\n", R.encoder_ms);
|
||||
printf("decoder %4d ms (%d tokens)\n", R.decoder_ms, R.n_tokens);
|
||||
printf("total %4d ms for %.2f s audio => RTF %.3f\n",
|
||||
R.total_ms, (double)pcm.size() / sr, R.rtf);
|
||||
}
|
||||
printf("run %d %-6s : mel %3d enc %3d dec %4d (%2d tok) total %4d ms RTF %.3f\n",
|
||||
run, tag, R.mel_ms, R.encoder_ms, R.decoder_ms, R.n_tokens,
|
||||
R.total_ms, R.rtf);
|
||||
}
|
||||
|
||||
stt_engine_free(eng);
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -442,15 +442,28 @@ SttEngine * stt_engine_load(const SttEngineLoadCfg & cfg) {
|
|||
}
|
||||
|
||||
// 3) Sessions encoder + decoder. QNN EP si use_htp.
|
||||
// Options HTP V79 importantes (cf QNN ExecutionProvider doc) :
|
||||
// backend_path : libQnnHtp.so (CPU stub vers HTP)
|
||||
// htp_performance_mode : "burst" = max NPU freq (vs "default" = balanced)
|
||||
// htp_arch : "v79" = SM8750 (Snapdragon 8 Elite)
|
||||
// enable_htp_fp16_precision: "1" = NPU fp16 native (Whisper a fp16 inputs/outputs)
|
||||
// profiling_level : "off" en prod (sinon overhead)
|
||||
// rpc_control_latency : "100" us pour les transferts CPU↔NPU (défaut peut être plus lent)
|
||||
auto make_opts = [&](Ort::SessionOptions & opts) {
|
||||
if (cfg.use_htp) {
|
||||
// QNN ExecutionProvider via AppendExecutionProvider("QNN", {...})
|
||||
std::vector<const char *> keys = { "backend_path" };
|
||||
std::vector<const char *> vals = { "libQnnHtp.so" };
|
||||
opts.AppendExecutionProvider("QNN",
|
||||
{{"backend_path", "libQnnHtp.so"}});
|
||||
opts.AppendExecutionProvider("QNN", {
|
||||
{"backend_path", "libQnnHtp.so"},
|
||||
{"htp_performance_mode", "burst"},
|
||||
{"htp_arch", "79"},
|
||||
{"enable_htp_fp16_precision", "1"},
|
||||
{"profiling_level", "off"},
|
||||
{"rpc_control_latency", "100"},
|
||||
});
|
||||
}
|
||||
opts.SetIntraOpNumThreads(cfg.n_threads);
|
||||
// Optim générale : graph opt level max + disable mem pattern (HTP gère sa propre mémoire)
|
||||
opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
|
||||
opts.DisableMemPattern();
|
||||
};
|
||||
|
||||
std::string enc_path = D + "/HfWhisperEncoder.onnx";
|
||||
|
|
|
|||
Loading…
Reference in New Issue