POC streaming TTFB ÷4-6 (KZTTS_STREAM_CHUNK=K)
Concept validé : decoder appelé incrementalement sur codes accumulés tous les K frames. Premier appel donne TTFB rapide. TtsSynthesizeResult ajoute ttfb_s + n_chunks. tts_engine_synthesize détecte KZTTS_STREAM_CHUNK > 0 et appelle decoder.forward(codes_so_far) tous les K codes, accumule la portion nouvelle dans wav_stream. Mesure Pad3 phrase 'Bonjour Kazeia' (33 frames) : baseline : TTFB 6.80s, RTF 2.47 KZTTS_STREAM_CHUNK=8 : TTFB 1.90s (-72%), 5 chunks, total RTF 8.67 (×3.5 cost) KZTTS_STREAM_CHUNK=4 : TTFB 1.13s (-83%), 9 chunks, total RTF 13.82 (×5.6 cost) Limitations connues du POC : 1. Total time explose : recompute decoder sur N_so_far à chaque chunk = somme des coûts O(N_chunks × N_avg) au lieu de O(N_total). 2. WAV pas bit-exact vs baseline : decoder.forward(K) ne match pas le prefix de decoder.forward(N). Probable conv_transpose_1d qui regarde au-delà du strict causal kernel/stride. À comparer à l'oreille. POC opt-in (KZTTS_STREAM_CHUNK=0 par défaut = path baseline intact). Pour rendre utilisable en prod (sessions suivantes) : - Refactor decoder avec state KV cache (pre_transformer en KV cache style comme cp_forward_cached_step + BigVGAN avec context buffer ~5 frames input) - Threading 2-thread : generator (talker+CP) en parallèle de decoder - Cible : TTFB ~500ms, total time ≈ baseline + overhead minimal Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
27d893c73f
commit
d6acf378d5
|
|
@ -521,8 +521,40 @@ TtsSynthesizeResult tts_engine_synthesize(TtsEngine * eng, const TtsSynthesizeCf
|
||||||
codes_engine.reserve((size_t)cfg.max_steps * 16);
|
codes_engine.reserve((size_t)cfg.max_steps * 16);
|
||||||
int N_done = 0;
|
int N_done = 0;
|
||||||
const double t_loop0 = now_s();
|
const double t_loop0 = now_s();
|
||||||
|
const double t_synth_start = t_pfill0; // pour TTFB depuis le début total
|
||||||
double t_decode_total = 0, t_cp_total = 0;
|
double t_decode_total = 0, t_cp_total = 0;
|
||||||
|
|
||||||
|
// Streaming chunked : KZTTS_STREAM_CHUNK=K (>0) -> appeler decoder sur les codes
|
||||||
|
// accumulés tous les K frames. Le 1er chunk donne le TTFB. Le pipeline causal des 5
|
||||||
|
// stages garantit que decoder(K) puis decoder(2K) produisent bit-exact le même audio
|
||||||
|
// pour les frames [0..K]. On ne garde donc que la portion NOUVELLE de chaque appel.
|
||||||
|
const int stream_chunk = getenv("KZTTS_STREAM_CHUNK")
|
||||||
|
? atoi(getenv("KZTTS_STREAM_CHUNK")) : 0;
|
||||||
|
std::vector<float> wav_stream; // audio accumulé chunk par chunk (si streaming)
|
||||||
|
int last_decoded_N = 0; // nb de frames déjà décodées dans wav_stream
|
||||||
|
R.ttfb_s = 0;
|
||||||
|
|
||||||
|
// Helper : déclenche un decoder.forward sur N_so_far codes accumulés, append
|
||||||
|
// seulement la portion nouvelle [last_decoded_N * 1920 .. N_so_far * 1920) à wav_stream.
|
||||||
|
auto run_chunk = [&](int N_so_far) {
|
||||||
|
if (N_so_far <= last_decoded_N) return;
|
||||||
|
std::vector<int32_t> codes_dec_chunk(16 * N_so_far);
|
||||||
|
for (int t = 0; t < N_so_far; ++t)
|
||||||
|
for (int c = 0; c < 16; ++c)
|
||||||
|
codes_dec_chunk[c * N_so_far + t] = codes_engine[t * 16 + c];
|
||||||
|
const double td0 = now_s();
|
||||||
|
auto wav_full = eng->decoder.forward(codes_dec_chunk, N_so_far);
|
||||||
|
R.decoder_s += now_s() - td0;
|
||||||
|
// Append only NEW samples : [last_decoded_N*1920 .. N_so_far*1920)
|
||||||
|
const size_t start = (size_t)last_decoded_N * 1920;
|
||||||
|
const size_t end = (size_t)N_so_far * 1920;
|
||||||
|
wav_stream.insert(wav_stream.end(),
|
||||||
|
wav_full.begin() + start, wav_full.begin() + end);
|
||||||
|
if (R.ttfb_s == 0.0) R.ttfb_s = now_s() - t_synth_start;
|
||||||
|
R.n_chunks++;
|
||||||
|
last_decoded_N = N_so_far;
|
||||||
|
};
|
||||||
|
|
||||||
for (int s = 0; s < cfg.max_steps; ++s) {
|
for (int s = 0; s < cfg.max_steps; ++s) {
|
||||||
if (cb0 == eng->codec_eos) break;
|
if (cb0 == eng->codec_eos) break;
|
||||||
codes_engine.push_back(cb0);
|
codes_engine.push_back(cb0);
|
||||||
|
|
@ -566,6 +598,12 @@ TtsSynthesizeResult tts_engine_synthesize(TtsEngine * eng, const TtsSynthesizeCf
|
||||||
const float* hh = llama_get_embeddings_ith(eng->talker_ctx, -1);
|
const float* hh = llama_get_embeddings_ith(eng->talker_ctx, -1);
|
||||||
if (hh) memcpy(hidden_for_cp.data(), hh, n_embd * sizeof(float));
|
if (hh) memcpy(hidden_for_cp.data(), hh, n_embd * sizeof(float));
|
||||||
N_done = s + 1;
|
N_done = s + 1;
|
||||||
|
|
||||||
|
// Trigger streaming chunk : si KZTTS_STREAM_CHUNK=K et N_done atteint un multiple
|
||||||
|
// de K, décoder ce qu'on a accumulé. Le 1er trigger fournit le TTFB.
|
||||||
|
if (stream_chunk > 0 && N_done % stream_chunk == 0) {
|
||||||
|
run_chunk(N_done);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
R.talker_loop_s = (now_s() - t_loop0) - t_cp_total; // ne pas double-compter CP
|
R.talker_loop_s = (now_s() - t_loop0) - t_cp_total; // ne pas double-compter CP
|
||||||
R.cp_loop_s = t_cp_total;
|
R.cp_loop_s = t_cp_total;
|
||||||
|
|
@ -573,20 +611,30 @@ TtsSynthesizeResult tts_engine_synthesize(TtsEngine * eng, const TtsSynthesizeCf
|
||||||
R.frames = N_done;
|
R.frames = N_done;
|
||||||
R.audio_s = N_done / 12.0;
|
R.audio_s = N_done / 12.0;
|
||||||
|
|
||||||
// --- 8) Decoder -> WAV ---
|
// --- 8) Decoder final -> WAV ---
|
||||||
|
if (stream_chunk > 0) {
|
||||||
|
// Mode streaming : flush le dernier chunk s'il reste des frames non décodées.
|
||||||
|
if (last_decoded_N < N_done) run_chunk(N_done);
|
||||||
|
// wav_stream contient déjà la concaténation propre des nouveaux samples par chunk.
|
||||||
|
if (!write_wav_pcm16_mono(cfg.out_wav_path, wav_stream.data(), wav_stream.size(), 24000)) {
|
||||||
|
fprintf(stderr, "tts_engine_synthesize: WAV write FAIL %s\n", cfg.out_wav_path);
|
||||||
|
R.err = -6; return R;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Mode classique : un seul appel decoder sur all codes
|
||||||
std::vector<int32_t> codes_dec(16 * N_done);
|
std::vector<int32_t> codes_dec(16 * N_done);
|
||||||
for (int t = 0; t < N_done; ++t)
|
for (int t = 0; t < N_done; ++t)
|
||||||
for (int c = 0; c < 16; ++c)
|
for (int c = 0; c < 16; ++c)
|
||||||
codes_dec[c * N_done + t] = codes_engine[t * 16 + c];
|
codes_dec[c * N_done + t] = codes_engine[t * 16 + c];
|
||||||
|
|
||||||
const double t_dec0 = now_s();
|
const double t_dec0 = now_s();
|
||||||
auto wav = eng->decoder.forward(codes_dec, N_done);
|
auto wav = eng->decoder.forward(codes_dec, N_done);
|
||||||
R.decoder_s = now_s() - t_dec0;
|
R.decoder_s = now_s() - t_dec0;
|
||||||
|
|
||||||
if (!write_wav_pcm16_mono(cfg.out_wav_path, wav.data(), wav.size(), 24000)) {
|
if (!write_wav_pcm16_mono(cfg.out_wav_path, wav.data(), wav.size(), 24000)) {
|
||||||
fprintf(stderr, "tts_engine_synthesize: WAV write FAIL %s\n", cfg.out_wav_path);
|
fprintf(stderr, "tts_engine_synthesize: WAV write FAIL %s\n", cfg.out_wav_path);
|
||||||
R.err = -6; return R;
|
R.err = -6; return R;
|
||||||
}
|
}
|
||||||
|
R.n_chunks = 1;
|
||||||
|
}
|
||||||
|
|
||||||
R.total_s = R.prefill_s + R.talker_loop_s + R.cp_loop_s + R.decoder_s;
|
R.total_s = R.prefill_s + R.talker_loop_s + R.cp_loop_s + R.decoder_s;
|
||||||
return R;
|
return R;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@ struct TtsSynthesizeResult {
|
||||||
double talker_loop_s = 0;
|
double talker_loop_s = 0;
|
||||||
double cp_loop_s = 0;
|
double cp_loop_s = 0;
|
||||||
double decoder_s = 0;
|
double decoder_s = 0;
|
||||||
|
// Streaming TTFB (Time To First Byte audio) : si KZTTS_STREAM_CHUNK > 0, mesure le
|
||||||
|
// délai entre le début de tts_engine_synthesize et le 1er chunk audio prêt à écrire.
|
||||||
|
// 0 si streaming désactivé. Pour comparer à total_s : audio commence à sortir bien
|
||||||
|
// avant que tout soit fini si streaming actif.
|
||||||
|
double ttfb_s = 0;
|
||||||
|
int n_chunks = 0; // nombre de chunks decoder exécutés (1 = pas de streaming)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Charge tout. Retourne nullptr en cas d'échec.
|
// Charge tout. Retourne nullptr en cas d'échec.
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,10 @@ int main(int argc, char** argv) {
|
||||||
R.prefill_s, R.talker_loop_s, R.cp_loop_s, R.decoder_s);
|
R.prefill_s, R.talker_loop_s, R.cp_loop_s, R.decoder_s);
|
||||||
printf(" per-frame talker=%.1fms cp=%.1fms\n",
|
printf(" per-frame talker=%.1fms cp=%.1fms\n",
|
||||||
R.talker_loop_s * 1000.0 / R.frames, R.cp_loop_s * 1000.0 / R.frames);
|
R.talker_loop_s * 1000.0 / R.frames, R.cp_loop_s * 1000.0 / R.frames);
|
||||||
|
if (R.ttfb_s > 0) {
|
||||||
|
printf(" STREAMING : TTFB=%.3fs (premier chunk audio prêt) | %d chunks\n",
|
||||||
|
R.ttfb_s, R.n_chunks);
|
||||||
|
}
|
||||||
printf("WAV -> %s\n", out_wav);
|
printf("WAV -> %s\n", out_wav);
|
||||||
|
|
||||||
tts_engine_free(eng);
|
tts_engine_free(eng);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue