chantier B STT #6 : 2 bugs critiques fixés grâce à l'A/B in-app du dev
L'A/B in-app du dev a révélé un bug grave que mon harness standalone ratait : 3/6 audios FR (elodie, richard, zelda) sortaient vides (0 tokens, EOT immédiat). Investigation : reproduit en standalone (chaque audio dans son propre process) - DONC PAS un bug d'enchaînement KV (mon code reset déjà self_k/v à chaque transcribe). Le dev avait une fausse piste qui m'a poussé à investiguer en profondeur côté algo decoder, ce qui a révélé les vraies causes. Bug #1 : FFT zero-pad 512 vs DFT exact 400 - kazeia_mel utilisait next_pow2(N_FFT=400)=512 + FFT radix-2 zero-padded - Conséquence : bins 0..200 du FFT 512 sont à fs/512=31.25 Hz/bin, alors que mel_filters.json est calibré pour fs/400=40 Hz/bin du DFT exact - Spectre désaligné en fréquences -> mel cohérent sur voix graves (damien, jerome, sid) mais cassé sur voix avec hautes harmoniques (elodie, richard, zelda) - Fix : DFT directe N=n_fft quand n_fft pas puissance de 2, FFT radix-2 quand pow2 (TTS speaker encoder N_FFT=1024 reste rapide) - Coût : mel 60 ms -> 220-320 ms (incontournable pour N_FFT=400) - Fixe elodie après ce bug, mais richard/zelda restaient vides Bug #2 : Pas de forced decoder prompt - Sur audios borderline (énergie spectrale ambiguë), Whisper saute la prédiction de langue (top-1 step 0 = 50362 NOTIMESTAMPS direct au lieu de 50265 FR) et atteint EOT au step 1 - Trace step-0 logits richard : [50362:+25.1] [50265:+24.7] (0.4 diff) - Solution standard HF whisper : forcer le prompt <SOT, |lang|, |task|> au lieu de laisser le modèle prédire les 3 tokens spéciaux - IMPORTANT : NE PAS forcer <|notimestamps|> ; le decoder QAIRT pred timestamp_begin (50363) après <|transcribe|> et veut le mode timestamps. Forcer NOTIMESTAMPS cause un step 3 = EOT immédiat sur TOUS les audios. - Fix : forced_prompt = [SOT, lang_tok, TRANSCRIBE_TOK], modèle décide timestamps vs notimestamps après. lang="auto" garde juste SOT. Résultat après les 2 fixes : 6/6 audios FR transcrivent en texte cohérent (damien/elodie/jerome/richard/sid/zelda). Chiffres honnêtes (warm path, après fixes) : - Mel : 220-320 ms (vs prod 189) — DFT plus coûteux mais correct - Encoder : 60-86 ms (vs prod 125) — gain options QNN burst/v79/fp16 - Decoder/token : 12-13 ms (vs prod 23) — gain zero-copy + options QNN - Total 21 tokens / 3s : ~620 ms (mesuré) - Total estimé 22 tokens / 1.6s : ~546 ms (vs prod ~820, -33 %) Les chiffres précédents (-45 %, mel 60 ms) étaient FAUX à cause du bug #1. Le -33 % réel reste meilleur que prod mais l'écart s'est réduit. A/B accuracy CER/WER vs WhisperHybridEngine reste à mesurer in-app par le dev sur SES audios fixtures. Modifs : - kazeia_mel.cpp : ajout DFT directe pour n_fft non pow2 - stt_engine.cpp : forced decoder prompt + lang_to_token + flags KZSTT_QNN_* réglables runtime pour debug - stt_cli.cpp : multi-audio sequence + KZSTT_DEBUG_STEPS pour trace logits Crédit : sans le retour précis du dev sur les vides observés, ces bugs étaient passé en checklist §9 et fait crash en prod.
This commit is contained in:
parent
dbd4f3d944
commit
4ad3b3716d
|
|
@ -5,18 +5,23 @@
|
||||||
|
|
||||||
**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).
|
**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** (Snapdragon 8 Elite / SM8750, Whisper-Small FR, warm path) :
|
**Perf de référence** (Snapdragon 8 Elite / SM8750, Whisper-Small FR, warm path, **après fix DFT 400 + forced prompt SOT/lang/task**) :
|
||||||
|
|
||||||
Cas réel Kazeia (audios ~1.6 s, parole dense, **apples-to-apples par étape** vs prod) :
|
Cas réel Kazeia (audios ~3 s parole dense, mesuré directement sur la tablette) :
|
||||||
|
|
||||||
| Étape | Prod Kotlin (22 tok) | C++ (warm, 12 tok mesuré) | Extrapol 22 tok C++ |
|
| Étape | Prod Kotlin (22 tok / 1.6 s) | C++ unifié (21 tok mesuré sur 3 s) |
|
||||||
|---|---:|---:|---:|
|
|---|---:|---:|
|
||||||
| Mel | 189 ms | 80 ms | 80 ms |
|
| Mel | 189 ms | **220-320 ms** (DFT directe N_FFT=400, obligatoire) |
|
||||||
| Encoder | 125 ms | **62 ms** | 62 ms |
|
| Encoder | 125 ms | **60-86 ms** |
|
||||||
| Decoder/token | 23 ms | **14 ms** | 14 ms |
|
| Decoder/token | 23 ms | **12-13 ms** |
|
||||||
| Decoder total | 506 ms | 164 ms (12 tok) | ~308 ms |
|
| Decoder total | 506 ms (22 tok) | 252 ms (21 tok) |
|
||||||
| **Total** | **820 ms** | **320 ms (12 tok)** | **~450 ms (-45 %)** |
|
| **Total** | **~820 ms (estimé)** | **~620 ms (mesuré 21 tok / 3 s)** |
|
||||||
| **RAM peak** | 545 MB | **377 MB** | **-31 %** |
|
| **RAM peak** | 545 MB | **377 MB (-31 %)** |
|
||||||
|
| **A/B accuracy in-app** | référence | **à mesurer** par dev (cf §9.2) |
|
||||||
|
|
||||||
|
**Honnêteté sur les chiffres** : les annonces précédentes (-45 %, mel 60 ms) cumulaient un bug. Le mel devait utiliser DFT directe N=400 (pas FFT zero-pad 512) sinon les bins du spectre sont **désalignés en fréquence** par rapport à mel_filters.json (40 Hz/bin attendu vs 31.25 Hz/bin produit). Sur audios à hautes harmoniques (voix féminines, certaines voix), cela causait l'encoder à produire un cross-attention "no speech" → decoder sortait `<EOT>` immédiat → transcription vide. **Bug fix v2** dans la session 01/06.
|
||||||
|
|
||||||
|
**Bug fix v3 — forced decoder prompt** : sur certains audios borderline, Whisper saute l'étape « prédire la langue » et choisit `<|notimestamps|>` direct → EOT au step 1. Solution standard HF : forcer le prompt `<SOT, |lang|, |transcribe|>` puis laisser le modèle décider du mode timestamps. **PAS** forcer `<|notimestamps|>` (le decoder QAIRT préfère le mode timestamps).
|
||||||
|
|
||||||
Cas audio long (10-15 s, parole continue, montre l'amortissement encoder) :
|
Cas audio long (10-15 s, parole continue, montre l'amortissement encoder) :
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,27 +160,50 @@ std::vector<float> compute(const std::vector<float> & wav_in,
|
||||||
else T = (len_pad - cfg.n_fft) / cfg.hop_size + 1;
|
else T = (len_pad - cfg.n_fft) / cfg.hop_size + 1;
|
||||||
out_T = T;
|
out_T = T;
|
||||||
|
|
||||||
// FFT en puissance de 2 >= n_fft (Whisper N_FFT=400 -> FFT 512).
|
// IMPORTANT : si n_fft n'est pas une puissance de 2, on DOIT faire une DFT directe
|
||||||
|
// (pas un FFT zero-padded), sinon les bins du FFT sont à des fréquences décalées
|
||||||
|
// par rapport à celles attendues par mel_basis (cf bug "empty output" sur certains
|
||||||
|
// audios — Whisper N_FFT=400 -> DFT exigée, mel_filters calculé pour fs/400=40Hz/bin).
|
||||||
const int fftN = next_pow2(cfg.n_fft);
|
const int fftN = next_pow2(cfg.n_fft);
|
||||||
|
const bool is_pow2 = (fftN == cfg.n_fft);
|
||||||
|
|
||||||
std::vector<float> spec((size_t)fft_bins * T, 0.0f);
|
std::vector<float> spec((size_t)fft_bins * T, 0.0f);
|
||||||
std::vector<float> re(fftN), im(fftN);
|
|
||||||
|
|
||||||
|
if (is_pow2) {
|
||||||
|
// FAST PATH : FFT radix-2 (TTS speaker encoder N_FFT=1024)
|
||||||
|
std::vector<float> re(fftN), im(fftN);
|
||||||
for (int t = 0; t < T; t++) {
|
for (int t = 0; t < T; t++) {
|
||||||
const int off = t * cfg.hop_size;
|
const int off = t * cfg.hop_size;
|
||||||
// Window sur win_size samples, zero-pad jusqu'à fftN
|
for (int i = 0; i < cfg.win_size; i++) { re[i] = pad[off + i] * hann[i]; im[i] = 0.0f; }
|
||||||
for (int i = 0; i < cfg.win_size; i++) {
|
|
||||||
re[i] = pad[off + i] * hann[i];
|
|
||||||
im[i] = 0.0f;
|
|
||||||
}
|
|
||||||
for (int i = cfg.win_size; i < fftN; i++) { re[i] = 0.0f; im[i] = 0.0f; }
|
for (int i = cfg.win_size; i < fftN; i++) { re[i] = 0.0f; im[i] = 0.0f; }
|
||||||
|
|
||||||
fft_radix2(re.data(), im.data(), fftN);
|
fft_radix2(re.data(), im.data(), fftN);
|
||||||
|
for (int k = 0; k < fft_bins; k++)
|
||||||
// Power spectrum [0..n_fft/2]
|
|
||||||
for (int k = 0; k < fft_bins; k++) {
|
|
||||||
spec[(size_t)k * T + t] = re[k]*re[k] + im[k]*im[k];
|
spec[(size_t)k * T + t] = re[k]*re[k] + im[k]*im[k];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// CORRECT PATH : DFT directe N=n_fft (Whisper N_FFT=400)
|
||||||
|
// Precompute twiddles cos/sin[k=0..fft_bins-1][n=0..N-1]. ~256 KB pour Whisper.
|
||||||
|
const int N = cfg.n_fft;
|
||||||
|
std::vector<float> cos_tbl((size_t)fft_bins * N), sin_tbl((size_t)fft_bins * N);
|
||||||
|
for (int k = 0; k < fft_bins; k++) {
|
||||||
|
for (int n = 0; n < N; n++) {
|
||||||
|
float angle = -2.0f * (float)M_PI * (float)k * (float)n / (float)N;
|
||||||
|
cos_tbl[(size_t)k * N + n] = std::cos(angle);
|
||||||
|
sin_tbl[(size_t)k * N + n] = std::sin(angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::vector<float> windowed((size_t)N);
|
||||||
|
for (int t = 0; t < T; t++) {
|
||||||
|
const int off = t * cfg.hop_size;
|
||||||
|
for (int i = 0; i < N; i++) windowed[i] = pad[off + i] * hann[i];
|
||||||
|
for (int k = 0; k < fft_bins; k++) {
|
||||||
|
const float * ct = &cos_tbl[(size_t)k * N];
|
||||||
|
const float * st = &sin_tbl[(size_t)k * N];
|
||||||
|
float re = 0.0f, im = 0.0f;
|
||||||
|
for (int n = 0; n < N; n++) { re += windowed[n] * ct[n]; im += windowed[n] * st[n]; }
|
||||||
|
spec[(size_t)k * T + t] = re*re + im*im;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mel = basis [n_mels, fft_bins] @ spec [fft_bins, T] -> [n_mels, T]
|
// mel = basis [n_mels, fft_bins] @ spec [fft_bins, T] -> [n_mels, T]
|
||||||
|
|
|
||||||
|
|
@ -50,23 +50,25 @@ int main(int argc, char** argv) {
|
||||||
setvbuf(stderr, nullptr, _IONBF, 0);
|
setvbuf(stderr, nullptr, _IONBF, 0);
|
||||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr, "usage: %s <model_dir> <audio16k_mono.wav> [language=fr] [cpu|htp=htp]\n",
|
fprintf(stderr, "usage: %s <model_dir> <audio1.wav> [audio2.wav ...] [-- htp|cpu]\n", argv[0]);
|
||||||
argv[0]);
|
fprintf(stderr, " Si plusieurs audios passés, ils sont transcrits en séquence sur le même engine\n");
|
||||||
|
fprintf(stderr, " (reproduit le pattern d'usage in-app où l'engine est load-once N transcribes).\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const char * model_dir = argv[1];
|
const char * model_dir = argv[1];
|
||||||
const char * wav_path = argv[2];
|
|
||||||
const char * lang = (argc >= 4) ? argv[3] : "fr";
|
|
||||||
const bool use_htp = !(argc >= 5 && !strcmp(argv[4], "cpu"));
|
|
||||||
|
|
||||||
int sr = 0;
|
// Parse audio list + optional flags (lang, htp/cpu) à la fin
|
||||||
auto pcm = read_wav_pcm16(wav_path, sr);
|
std::vector<std::string> wav_paths;
|
||||||
if (pcm.empty()) return 2;
|
const char * lang = "fr";
|
||||||
fprintf(stderr, "wav : %zu samples @ %d Hz = %.2f s\n",
|
bool use_htp = true;
|
||||||
pcm.size(), sr, (double)pcm.size() / sr);
|
for (int i = 2; i < argc; ++i) {
|
||||||
if (sr != 16000) {
|
std::string a = argv[i];
|
||||||
fprintf(stderr, "ERROR: sr=%d != 16000 (Whisper requiert 16kHz)\n", sr); return 3;
|
if (a == "cpu") { use_htp = false; }
|
||||||
|
else if (a == "htp") { use_htp = true; }
|
||||||
|
else if (a == "fr" || a == "en" || a == "auto") { lang = argv[i]; }
|
||||||
|
else wav_paths.push_back(a);
|
||||||
}
|
}
|
||||||
|
if (wav_paths.empty()) { fprintf(stderr, "ERROR: au moins 1 audio requis\n"); return 1; }
|
||||||
|
|
||||||
SttEngineLoadCfg lc;
|
SttEngineLoadCfg lc;
|
||||||
lc.model_dir = model_dir;
|
lc.model_dir = model_dir;
|
||||||
|
|
@ -75,6 +77,19 @@ int main(int argc, char** argv) {
|
||||||
auto * eng = stt_engine_load(lc);
|
auto * eng = stt_engine_load(lc);
|
||||||
if (!eng) { fprintf(stderr, "stt_engine_load FAIL\n"); return 4; }
|
if (!eng) { fprintf(stderr, "stt_engine_load FAIL\n"); return 4; }
|
||||||
|
|
||||||
|
int n_runs = 1;
|
||||||
|
if (const char* p = std::getenv("KZSTT_RUNS")) { int v = atoi(p); if (v > 0) n_runs = v; }
|
||||||
|
|
||||||
|
fprintf(stderr, "\n=== Sequence de %zu audios, %d runs/audio, lang=%s, %s ===\n",
|
||||||
|
wav_paths.size(), n_runs, lang, use_htp ? "NPU" : "CPU");
|
||||||
|
|
||||||
|
for (size_t ai = 0; ai < wav_paths.size(); ++ai) {
|
||||||
|
const std::string & wp = wav_paths[ai];
|
||||||
|
int sr = 0;
|
||||||
|
auto pcm = read_wav_pcm16(wp.c_str(), sr);
|
||||||
|
if (pcm.empty()) { fprintf(stderr, "skip %s (read fail)\n", wp.c_str()); continue; }
|
||||||
|
if (sr != 16000) { fprintf(stderr, "skip %s (sr=%d)\n", wp.c_str(), sr); continue; }
|
||||||
|
|
||||||
SttTranscribeCfg tc;
|
SttTranscribeCfg tc;
|
||||||
tc.pcm16 = pcm.data();
|
tc.pcm16 = pcm.data();
|
||||||
tc.n_samples = (int)pcm.size();
|
tc.n_samples = (int)pcm.size();
|
||||||
|
|
@ -82,23 +97,15 @@ int main(int argc, char** argv) {
|
||||||
tc.language = lang;
|
tc.language = lang;
|
||||||
tc.force_transcribe = true;
|
tc.force_transcribe = true;
|
||||||
|
|
||||||
// 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; }
|
|
||||||
|
|
||||||
for (int run = 0; run < n_runs; ++run) {
|
for (int run = 0; run < n_runs; ++run) {
|
||||||
auto R = stt_engine_transcribe(eng, tc);
|
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; }
|
if (R.err) { fprintf(stderr, "transcribe FAIL err=%d on %s\n", R.err, wp.c_str()); continue; }
|
||||||
const char * tag = (n_runs > 1) ? (run == 0 ? "[cold]" : "[warm]") : "";
|
printf("[%zu/%zu run=%d] %-30s : mel %3d enc %3d dec %4d (%2d tok) total %4d ms | text='%s'\n",
|
||||||
if (run == 0 || n_runs == 1) {
|
ai+1, wp.size() ? ai+1 : 0, run,
|
||||||
printf("\n=== TRANSCRIPTION (%s, %s) %s ===\n", use_htp ? "NPU" : "CPU", lang, tag);
|
wp.substr(wp.find_last_of('/') + 1).c_str(),
|
||||||
printf("%s\n", R.text.c_str());
|
R.mel_ms, R.encoder_ms, R.decoder_ms, R.n_tokens, R.total_ms,
|
||||||
|
R.text.c_str());
|
||||||
}
|
}
|
||||||
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);
|
stt_engine_free(eng);
|
||||||
|
|
|
||||||
|
|
@ -311,11 +311,36 @@ constexpr int SOT = 50258;
|
||||||
constexpr int EOT = 50257;
|
constexpr int EOT = 50257;
|
||||||
constexpr int TRANSLATE_TOK = 50358;
|
constexpr int TRANSLATE_TOK = 50358;
|
||||||
constexpr int TRANSCRIBE_TOK = 50359;
|
constexpr int TRANSCRIBE_TOK = 50359;
|
||||||
|
constexpr int NOTIMESTAMPS = 50362;
|
||||||
|
constexpr int LANG_FR = 50265;
|
||||||
|
constexpr int LANG_EN = 50259;
|
||||||
|
constexpr int LANG_AUTO = -1; // sentinel: laisse le modèle prédire la langue au step 1
|
||||||
constexpr int VOCAB_SIZE = 51865;
|
constexpr int VOCAB_SIZE = 51865;
|
||||||
constexpr int MEAN_DECODE_LEN = 200;
|
constexpr int MEAN_DECODE_LEN = 200;
|
||||||
constexpr int HEAD_DIM = 64;
|
constexpr int HEAD_DIM = 64;
|
||||||
constexpr float MASK_NEG = -100.0f;
|
constexpr float MASK_NEG = -100.0f;
|
||||||
|
|
||||||
|
// Map ISO -> Whisper language token id (tous les codes Whisper officiels du modèle multilingue)
|
||||||
|
static int lang_to_token(const char * lang) {
|
||||||
|
if (!lang || !*lang) return LANG_FR;
|
||||||
|
std::string s = lang;
|
||||||
|
if (s == "auto") return LANG_AUTO;
|
||||||
|
// Mapping principal (les 99 langues Whisper utilisent les ids 50259..50357)
|
||||||
|
if (s == "en") return 50259;
|
||||||
|
if (s == "zh") return 50260;
|
||||||
|
if (s == "de") return 50261;
|
||||||
|
if (s == "es") return 50262;
|
||||||
|
if (s == "ru") return 50263;
|
||||||
|
if (s == "ko") return 50264;
|
||||||
|
if (s == "fr") return 50265;
|
||||||
|
if (s == "ja") return 50266;
|
||||||
|
if (s == "pt") return 50267;
|
||||||
|
if (s == "it") return 50274;
|
||||||
|
if (s == "nl") return 50271;
|
||||||
|
if (s == "ar") return 50272;
|
||||||
|
return LANG_FR; // default raisonnable pour Kazeia
|
||||||
|
}
|
||||||
|
|
||||||
double now_s() {
|
double now_s() {
|
||||||
using clk = std::chrono::steady_clock;
|
using clk = std::chrono::steady_clock;
|
||||||
return std::chrono::duration<double>(clk::now().time_since_epoch()).count();
|
return std::chrono::duration<double>(clk::now().time_since_epoch()).count();
|
||||||
|
|
@ -442,29 +467,38 @@ SttEngine * stt_engine_load(const SttEngineLoadCfg & cfg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Sessions encoder + decoder. QNN EP si use_htp.
|
// 3) Sessions encoder + decoder. QNN EP si use_htp.
|
||||||
// Options HTP V79 importantes (cf QNN ExecutionProvider doc) :
|
// Options HTP V79 réglables via env (debug bug "empty output sur certains audios") :
|
||||||
// backend_path : libQnnHtp.so (CPU stub vers HTP)
|
// KZSTT_QNN_BURST : "1" (def) = htp_performance_mode=burst, "0" = laisse default
|
||||||
// htp_performance_mode : "burst" = max NPU freq (vs "default" = balanced)
|
// KZSTT_QNN_FP16 : "1" (def) = enable_htp_fp16_precision=1, "0" = laisse default
|
||||||
// htp_arch : "v79" = SM8750 (Snapdragon 8 Elite)
|
// KZSTT_QNN_ARCH : "1" (def) = htp_arch=79 explicite, "0" = auto-detect
|
||||||
// enable_htp_fp16_precision: "1" = NPU fp16 native (Whisper a fp16 inputs/outputs)
|
// KZSTT_QNN_OPTALL : "1" (def) = SetGraphOptimizationLevel(ALL_OPT), "0" = default
|
||||||
// profiling_level : "off" en prod (sinon overhead)
|
auto envb = [](const char * k, bool dflt) {
|
||||||
// rpc_control_latency : "100" us pour les transferts CPU↔NPU (défaut peut être plus lent)
|
const char * v = std::getenv(k); return v ? (atoi(v) != 0) : dflt;
|
||||||
|
};
|
||||||
|
const bool opt_burst = envb("KZSTT_QNN_BURST", true);
|
||||||
|
const bool opt_fp16 = envb("KZSTT_QNN_FP16", true);
|
||||||
|
const bool opt_arch = envb("KZSTT_QNN_ARCH", true);
|
||||||
|
const bool opt_optall = envb("KZSTT_QNN_OPTALL", true);
|
||||||
|
|
||||||
auto make_opts = [&](Ort::SessionOptions & opts) {
|
auto make_opts = [&](Ort::SessionOptions & opts) {
|
||||||
if (cfg.use_htp) {
|
if (cfg.use_htp) {
|
||||||
opts.AppendExecutionProvider("QNN", {
|
std::vector<std::pair<std::string, std::string>> qnn_opts = {
|
||||||
{"backend_path", "libQnnHtp.so"},
|
{"backend_path", "libQnnHtp.so"},
|
||||||
{"htp_performance_mode", "burst"},
|
|
||||||
{"htp_arch", "79"},
|
|
||||||
{"enable_htp_fp16_precision", "1"},
|
|
||||||
{"profiling_level", "off"},
|
{"profiling_level", "off"},
|
||||||
{"rpc_control_latency", "100"},
|
{"rpc_control_latency", "100"},
|
||||||
});
|
};
|
||||||
|
if (opt_burst) qnn_opts.push_back({"htp_performance_mode", "burst"});
|
||||||
|
if (opt_arch) qnn_opts.push_back({"htp_arch", "79"});
|
||||||
|
if (opt_fp16) qnn_opts.push_back({"enable_htp_fp16_precision", "1"});
|
||||||
|
std::unordered_map<std::string, std::string> qnn_map(qnn_opts.begin(), qnn_opts.end());
|
||||||
|
opts.AppendExecutionProvider("QNN", qnn_map);
|
||||||
}
|
}
|
||||||
opts.SetIntraOpNumThreads(cfg.n_threads);
|
opts.SetIntraOpNumThreads(cfg.n_threads);
|
||||||
// Optim générale : graph opt level max + disable mem pattern (HTP gère sa propre mémoire)
|
if (opt_optall) opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
|
||||||
opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
|
|
||||||
opts.DisableMemPattern();
|
opts.DisableMemPattern();
|
||||||
};
|
};
|
||||||
|
fprintf(stderr, "stt_engine_load: QNN opts: burst=%d fp16=%d arch79=%d optall=%d\n",
|
||||||
|
opt_burst, opt_fp16, opt_arch, opt_optall);
|
||||||
|
|
||||||
std::string enc_path = D + "/HfWhisperEncoder.onnx";
|
std::string enc_path = D + "/HfWhisperEncoder.onnx";
|
||||||
std::string dec_path = D + "/HfWhisperDecoder.onnx";
|
std::string dec_path = D + "/HfWhisperDecoder.onnx";
|
||||||
|
|
@ -731,9 +765,27 @@ SttTranscribeResult stt_engine_transcribe(SttEngine * eng, const SttTranscribeCf
|
||||||
|
|
||||||
std::vector<int> generated;
|
std::vector<int> generated;
|
||||||
generated.reserve(MEAN_DECODE_LEN);
|
generated.reserve(MEAN_DECODE_LEN);
|
||||||
int current_token = SOT;
|
|
||||||
|
// Forced decoder prompt : <SOT> <|lang|> <|transcribe|>
|
||||||
|
// On NE force PAS <|notimestamps|> : le decoder QAIRT préfère le mode timestamps
|
||||||
|
// (top-1 step 2 = 50363 = timestamp_begin). Forcer notimestamps fait EOT immédiat.
|
||||||
|
// En laissant le modèle décider timestamps vs notimestamps, on assure la cohérence
|
||||||
|
// avec le decoder QAIRT.
|
||||||
|
//
|
||||||
|
// Pourquoi le forced prompt : sans lui, sur audios borderline (voix aiguës,
|
||||||
|
// énergie faible), Whisper saute la prédiction de langue (top-1 = 50362
|
||||||
|
// notimestamps direct) et sort EOT step 1. Forcer lang+task résout 3/6 audios.
|
||||||
|
const int lang_tok = lang_to_token(cfg.language);
|
||||||
|
std::vector<int> forced_prompt = { SOT };
|
||||||
|
if (lang_tok != LANG_AUTO) {
|
||||||
|
forced_prompt.push_back(lang_tok);
|
||||||
|
forced_prompt.push_back(TRANSCRIBE_TOK);
|
||||||
|
}
|
||||||
|
// Si lang=auto, on garde juste SOT et on laisse Whisper prédire la langue+task.
|
||||||
|
int forced_idx = 0;
|
||||||
|
int current_token = forced_prompt[0];
|
||||||
int position_id = 0;
|
int position_id = 0;
|
||||||
int real_vocab_size = -1; // détecté au 1er step depuis le shape des logits
|
int real_vocab_size = -1;
|
||||||
|
|
||||||
for (int step = 0; step < MEAN_DECODE_LEN - 1; ++step) {
|
for (int step = 0; step < MEAN_DECODE_LEN - 1; ++step) {
|
||||||
// Update mask : un seul fp16 à écrire par step (le slot R-to-L)
|
// Update mask : un seul fp16 à écrire par step (le slot R-to-L)
|
||||||
|
|
@ -758,14 +810,30 @@ SttTranscribeResult stt_engine_transcribe(SttEngine * eng, const SttTranscribeCf
|
||||||
if (real_vocab_size < 0) {
|
if (real_vocab_size < 0) {
|
||||||
size_t n = 1; for (auto d : logits_shape) n *= (size_t)d;
|
size_t n = 1; for (auto d : logits_shape) n *= (size_t)d;
|
||||||
real_vocab_size = (int)n;
|
real_vocab_size = (int)n;
|
||||||
if (step == 0) {
|
|
||||||
fprintf(stderr, "stt_engine_transcribe: logits shape detected = %d elements (VOCAB_SIZE const=%d)\n",
|
|
||||||
real_vocab_size, VOCAB_SIZE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const uint16_t * logits_data = dec_outputs[logits_out_idx].GetTensorMutableData<uint16_t>();
|
const uint16_t * logits_data = dec_outputs[logits_out_idx].GetTensorMutableData<uint16_t>();
|
||||||
int token = argmax_fp16_logits(logits_data, real_vocab_size);
|
int token = argmax_fp16_logits(logits_data, real_vocab_size);
|
||||||
|
|
||||||
|
// Forced prompt : tant qu'on est dans le prompt, on impose le token suivant
|
||||||
|
// (les self_k/v se construisent normalement, mais on ignore le logit de sortie).
|
||||||
|
if (forced_idx + 1 < (int)forced_prompt.size()) {
|
||||||
|
forced_idx += 1;
|
||||||
|
token = forced_prompt[forced_idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step < 8 && std::getenv("KZSTT_DEBUG_STEPS")) {
|
||||||
|
std::vector<std::pair<float,int>> scores(real_vocab_size);
|
||||||
|
for (int i = 0; i < real_vocab_size; ++i)
|
||||||
|
scores[i] = {fp16_to_fp32(logits_data[i]), i};
|
||||||
|
std::partial_sort(scores.begin(), scores.begin() + 5, scores.end(),
|
||||||
|
[](auto & a, auto & b){ return a.first > b.first; });
|
||||||
|
fprintf(stderr, " step=%d current_token=%d pos=%d top5:", step, current_token, position_id);
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
fprintf(stderr, " [%d:%+.3f]", scores[i].second, scores[i].first);
|
||||||
|
}
|
||||||
|
fprintf(stderr, " -> picked=%d\n", token);
|
||||||
|
}
|
||||||
|
|
||||||
if (cfg.force_transcribe && token == TRANSLATE_TOK) token = TRANSCRIBE_TOK;
|
if (cfg.force_transcribe && token == TRANSLATE_TOK) token = TRANSCRIBE_TOK;
|
||||||
|
|
||||||
// Update self KV from outputs : memcpy direct dans nos buffers persistants
|
// Update self KV from outputs : memcpy direct dans nos buffers persistants
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue