Decoder chraac en sous-process : -20% decoder, -9% pipeline, talker intact

Architecture pour exploiter le gain chraac sans casser le talker :
  - kazeia_decoder_chraac : binaire static contre libqwen3tts-decoder.a chraac
    + ggml chraac statiques (9.8 MB autonome, pas de dép .so chraac).
    Args : <decoder.gguf> <codes.bin> <T> <out.wav>. Lit codes int32 time-major,
    transpose codebook-major, forward, écrit WAV.
  - tts_engine : si KZTTS_DECODER_SUBPROC=path + KZTTS_DECODER_GGUF=path posés,
    fork+execl au lieu de dec.forward intégré. Codes via /data/local/tmp tmpfile,
    WAV via rename. Le talker+CP continuent sur ql/ggml (qualcomm) qui marche.

Mesures Pad3 KZTTS_THREADS=6 GGML_NUM_THREADS=8 seed=42 :

  Phrase 'Bonjour Kazeia' (33 frames) :
    A baseline qualcomm : decoder 3.29s, total 6.61s, RTF 2.40
    B chraac subproc    : decoder 2.63s, total 6.06s, RTF 2.20
    -> decoder -20%, total -8.3%, audio RMS 1743 identique

  Phrase 'Bonsoir, comment tu te sens ce soir ?' (41 frames) :
    A baseline : decoder 3.95s, total 8.16s, RTF 2.39
    B subproc  : decoder 3.15s, total 7.38s, RTF 2.16
    -> decoder -20%, total -9.6%

Audio cohérent au baseline (range, RMS quasi identiques), imperceptible
à l'oreille (cf cas 'Bonsoir' envoyé).

Le gain chraac historique sur decoder seul est désormais accessible SANS
casser le talker.

Code committé opt-in (KZTTS_DECODER_SUBPROC absent par défaut = baseline intact).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Richard Loyer 2026-05-30 18:14:09 +02:00
parent 2809b54943
commit c332943181
3 changed files with 175 additions and 0 deletions

44
dist/build_decoder_subproc.sh vendored Executable file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Build kazeia_decoder_chraac : sous-process décodeur statique chraac.
# Link entièrement statique contre les .a chraac (libggml + libqwen3tts-decoder)
# pour être autonome — aucun .so chraac sur la tablette n'est requis au runtime.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
NDK="${ANDROID_NDK_ROOT:-/opt/Kazeia/android-ndk-r27d}"
CXX="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang++"
DEC_BUILD="/opt/Kazeia/kazeia-tts-decoder-ggml/build-android-chraac-fresh"
DEC_SRC="/opt/Kazeia/kazeia-tts-decoder-ggml/src"
CHRAAC_GGML_INC="/opt/Kazeia/chraac-llama/ggml/include"
[ -f "$DEC_BUILD/libqwen3tts-decoder.a" ] || { echo "decoder.a chraac absent: $DEC_BUILD"; exit 1; }
mkdir -p "$HERE/b-jni"
OUT="$HERE/b-jni/kazeia_decoder_chraac"
CFLAGS=( -std=c++17 -O3 -fPIE
-march=armv8.6-a+i8mm+bf16+dotprod+fp16
-Wno-unused-parameter -Wno-unused-variable -Wno-sign-compare
)
INCS=( -I"$DEC_SRC" -I"$CHRAAC_GGML_INC" )
# Important : --start-group / --end-group pour résoudre les références cycliques
# entre libqwen3tts-decoder et ggml. Ordre: decoder.a en premier (références
# ggml symbols), puis ggml-cpu, ggml-base, ggml (qui se référencent entre eux).
LIBS=(
"$HERE/jni/decoder_subprocess.cpp"
-Wl,--start-group
"$DEC_BUILD/libqwen3tts-decoder.a"
"$DEC_BUILD/ggml-build/src/libggml-cpu.a"
"$DEC_BUILD/ggml-build/src/libggml-base.a"
"$DEC_BUILD/ggml-build/src/libggml.a"
-Wl,--end-group
-fopenmp -static-openmp
-llog -ldl -lm
)
echo "== building $OUT =="
"$CXX" "${CFLAGS[@]}" "${INCS[@]}" "${LIBS[@]}" -o "$OUT"
ls -la "$OUT"
echo "== check qu'il n'y a pas de dépendance .so chraac =="
"$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf" -d "$OUT" | grep -E "NEEDED|RUNPATH"

90
dist/jni/decoder_subprocess.cpp vendored Normal file
View File

@ -0,0 +1,90 @@
// Sous-process décodeur TTS standalone, linké STATIQUE contre chraac-llama/ggml
// (RTF decoder 0.96 historique vs 1.47 qualcomm fork). Utilisé par tts_engine
// quand KZTTS_DECODER_SUBPROC=path/kazeia_decoder_chraac est posé.
//
// Le talker (sensible aux différences llama.cpp entre forks) reste dans le process
// principal sur ql/ggml (qualcomm). Le decoder ici tourne dans un process séparé
// linké static chraac → pas de mismatch ABI dans le binaire principal.
//
// Usage : kazeia_decoder_chraac <decoder.gguf> <codes.bin> <T> <out.wav>
// codes.bin : int32 [T, 16] time-major (= ce que tts_engine produit)
//
// Communication via FICHIERS (codes binaires en input, WAV en output). Simple,
// debuggable. Pour perf future : pipe stdin/stdout ou shared memory.
#include "decoder.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <fstream>
#include <vector>
using namespace kazeia::tts;
// Copié de tts_engine.cpp (libqwen3tts-decoder.a ne l'expose pas).
static bool write_wav_pcm16_mono(const char* path, const float* samples, size_t N, int sr = 24000) {
FILE* f = fopen(path, "wb");
if (!f) return false;
const uint32_t data_bytes = (uint32_t)(N * 2);
const uint32_t riff_size = 36 + data_bytes;
auto w16 = [&](uint16_t v){ fwrite(&v, 2, 1, f); };
auto w32 = [&](uint32_t v){ fwrite(&v, 4, 1, f); };
fwrite("RIFF", 1, 4, f); w32(riff_size); fwrite("WAVE", 1, 4, f);
fwrite("fmt ", 1, 4, f); w32(16); w16(1); w16(1);
w32((uint32_t)sr); w32((uint32_t)sr * 2); w16(2); w16(16);
fwrite("data", 1, 4, f); w32(data_bytes);
for (size_t i = 0; i < N; ++i) {
float v = samples[i]; if (v > 1.f) v = 1.f; else if (v < -1.f) v = -1.f;
int16_t pcm = (int16_t)std::lround(v * 32767.0f);
fwrite(&pcm, 2, 1, f);
}
fclose(f); return true;
}
int main(int argc, char** argv) {
if (argc < 5) {
fprintf(stderr, "usage: %s <decoder.gguf> <codes.bin> <T> <out.wav>\n", argv[0]);
return 1;
}
const char* model_path = argv[1];
const char* codes_path = argv[2];
const int T = atoi(argv[3]);
const char* out_wav = argv[4];
// Read codes time-major [T, 16]
std::ifstream f(codes_path, std::ios::binary | std::ios::ate);
if (!f) { fprintf(stderr, "open %s FAIL\n", codes_path); return 2; }
const size_t n_bytes = (size_t)f.tellg();
const size_t n_expected = (size_t)T * 16 * sizeof(int32_t);
if (n_bytes != n_expected) {
fprintf(stderr, "codes size mismatch: got %zu bytes, expected %zu (T=%d)\n",
n_bytes, n_expected, T);
return 3;
}
std::vector<int32_t> codes_time_major(T * 16);
f.seekg(0);
f.read((char*)codes_time_major.data(), n_expected);
f.close();
// Transpose [T, 16] -> [16, T] (codebook-major attendu par decoder.forward)
std::vector<int32_t> codes_dec(16 * T);
for (int t = 0; t < T; ++t)
for (int c = 0; c < 16; ++c)
codes_dec[c * T + t] = codes_time_major[t * 16 + c];
// Load decoder + forward
Decoder dec;
if (!dec.load(model_path)) {
fprintf(stderr, "decoder.load(%s) FAIL\n", model_path);
return 4;
}
auto wav = dec.forward(codes_dec, T);
if (!write_wav_pcm16_mono(out_wav, wav.data(), wav.size(), 24000)) {
fprintf(stderr, "write WAV %s FAIL\n", out_wav);
return 5;
}
fprintf(stderr, "kazeia_decoder_chraac OK: T=%d -> %zu samples -> %s\n",
T, wav.size(), out_wav);
return 0;
}

View File

@ -14,6 +14,8 @@
#include <fstream> #include <fstream>
#include <string> #include <string>
#include <vector> #include <vector>
#include <sys/wait.h>
#include <unistd.h>
using namespace kazeia::tts; using namespace kazeia::tts;
@ -612,6 +614,45 @@ TtsSynthesizeResult tts_engine_synthesize(TtsEngine * eng, const TtsSynthesizeCf
R.audio_s = N_done / 12.0; R.audio_s = N_done / 12.0;
// --- 8) Decoder final -> WAV --- // --- 8) Decoder final -> WAV ---
// KZTTS_DECODER_SUBPROC=path/kazeia_decoder_chraac : fork+exec un sous-process décodeur
// statiquement linké contre chraac (-22% sur decoder). Le talker reste qualcomm (qui marche).
// Demande KZTTS_DECODER_GGUF=path/qwen3tts_decoder.gguf pour passer en argv au sous-process.
const char * subproc_bin = getenv("KZTTS_DECODER_SUBPROC");
const char * subproc_model = getenv("KZTTS_DECODER_GGUF");
if (subproc_bin && subproc_model && stream_chunk == 0) {
// Codes time-major [N_done, 16] -> fichier tmp
char codes_tmp[256], wav_tmp[256], n_str[32];
snprintf(codes_tmp, sizeof(codes_tmp), "/data/local/tmp/kazeia_codes_%d.bin", (int)getpid());
snprintf(wav_tmp, sizeof(wav_tmp), "/data/local/tmp/kazeia_wav_%d.wav", (int)getpid());
snprintf(n_str, sizeof(n_str), "%d", N_done);
{
FILE * f = fopen(codes_tmp, "wb");
if (!f) { fprintf(stderr, "subproc: fopen codes FAIL\n"); R.err = -7; return R; }
fwrite(codes_engine.data(), sizeof(int32_t), codes_engine.size(), f);
fclose(f);
}
const double t_dec0 = now_s();
pid_t pid = fork();
if (pid == 0) {
execl(subproc_bin, subproc_bin, subproc_model, codes_tmp, n_str, wav_tmp, (char*)nullptr);
fprintf(stderr, "subproc execl FAIL\n");
_exit(127);
} else if (pid < 0) {
fprintf(stderr, "subproc fork FAIL\n"); R.err = -8; return R;
}
int status = 0;
waitpid(pid, &status, 0);
R.decoder_s = now_s() - t_dec0;
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
fprintf(stderr, "subproc exited code=%d\n", WEXITSTATUS(status)); R.err = -9; return R;
}
if (rename(wav_tmp, cfg.out_wav_path) != 0) {
fprintf(stderr, "subproc rename WAV %s -> %s FAIL\n", wav_tmp, cfg.out_wav_path);
R.err = -10; return R;
}
unlink(codes_tmp);
R.n_chunks = 1;
} else
if (stream_chunk > 0) { if (stream_chunk > 0) {
// Mode streaming : flush le dernier chunk s'il reste des frames non décodées. // 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); if (last_decoded_N < N_done) run_chunk(N_done);