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>
Plutôt que de porter Qwen3 BPE (NFC + pre-tokenize regex Unicode + ByteLevel +
merges + ignore_merges) à la main en C++ (~500 LOC risquées), on charge
n'importe quelle Qwen3 GGUF en vocab_only=true (~50 MB RAM, pas de poids) et
on appelle llama_tokenize(parse_special=true). Bit-exact garanti par
construction.
kazeia_text_tokenizer.h/cpp : API courte
kz_tok_load(tok, gguf_path) / kz_tok_free / kz_tok_encode
kz_tok_encode_tts_prompt(tok, content) -> enveloppe dans le template chat
<|im_start|>assistant\n{content}<|im_end|>\n<|im_start|>assistant\n
(déduit du dump golden : 16 tokens pour 'Bonjour je m'appelle Kazeia').
test_tokenizer : binaire standalone vérifiant bit-exact vs input_ids_full.bin
./test_tokenizer Qwen3-4B-Q4_0.gguf tts_dump/input_ids_full.bin 'Bonjour je m'appelle Kazeia'
-> OK BIT-EXACT (16/16 tokens identiques au golden HF).
tts_pipeline : si KZTTS_TEXT et KZTTS_VOCAB_GGUF posés, tokenize la phrase au
lieu de lire input_ids_full.bin. La structure attendue (3 role + Nt body + 5
trailing) est exactement ce que kz_tok_encode_tts_prompt produit, donc
input_ids_len = Nt + 8 sans rien d'autre à changer dans la construction
prefill_embeds.
Mesure tablette Pad3 (cpu 6t, KZTTS_CP_CACHE=1, seed=42) :
A fixture : 31 frames, total 7.866s, RTF 3.04
B tokenizer C++ : 31 frames, total 7.842s, RTF 3.04 + WAV md5 identique
C 'Bonsoir...' : 41 frames audio 3.4s, total 10.5s, RTF 3.09 (phrase neuve OK)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cp_inference: nouvelle voie cp_predict_cached (1 prefill 2 tokens +
14 decode 1 token via KV cache f32 persistant) à côté de cp_predict
(oracle recompute conservé pour A/B). cp_forward_cached_step écrit le
K/V post-RoPE dans s.K_cache/V_cache via ggml_view_3d + ggml_cpy, et
lit le full cache [0..T_full) pour l'attention. Ordre topo garanti
par build_forward_expand(cpy) AVANT build_forward_expand(x).
tts_pipeline: switch KZTTS_CP_CACHE=0/1 (défaut 0 = oracle bit-exact).
build_tts_pipeline.sh: rebuild reproductible (CMakeLists ne couvrait
que libkazeia_engine.so).
Mesure tablette Pad3 (talker_f32+tts_dump, CPU 6t, seed=42, 31 frames):
oracle : CP 227.9 ms/frame, total 12.90s, RTF 4.99
cached : CP 137.8 ms/frame, total 8.91s, RTF 3.45
codes_ref.bin == codes_cache.bin (cmp), out_ref.wav md5 == out_cache.wav
Speedup CP ×1.65, pas le ×8 espéré : le forward est BW-bound (~100 MB
de poids f32 streamés par sub-forward, on lit toujours 15 fois). Le
gain est sur la part compute (sum L=2..16 -> 16 token-fwd au lieu de
135). Pour aller plus loin : (a) cp_load en f16 conservé (×2 BW),
(b) fusion graphe multi-step. Decoder RTF 1.37 reste aussi à attaquer.
Bit-exactness : f32 mul_mat stable entre L=k+1 et L=1 à threads=6,
les sommations donnent les mêmes bits. Math identique, ordre identique
de fait.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sampler.{h,cpp} : module isolé reproduisant la chaine HF
LogitsProcessor :
1. RepetitionPenaltyLogitsProcessor (logit/penalty si >0, logit*penalty sinon)
2. TemperatureLogitsWarper (logit /= temp)
3. TopKLogitsWarper (garde top_k, autres -> -inf)
4. TopPLogitsWarper (nucleus : garde la masse cumulee jusqu'a top_p)
5. multinomial sample (xorshift32, deterministe via sampler_seed)
PRNG xorshift32 != torch.mt19937 donc meme seed != audio Python, mais stable
sur n'importe quel texte/voix (pas d'attracteur greedy).
Plumbing :
- CPState.sampler (defaut greedy top_k=1, temp=0 -> cp_validate continue a sortir
495/495 codes match). Pipeline override pour temp=0.9 top_k=50 rep_penalty=1.05
rep_window=16 (intra-frame, 15 codebooks max). CB1..15 utilisent
sampler_sample_local() qui prend les tokens deja samples CE step pour penaliser
repetition intra-frame.
- tts_pipeline : Sampler talker_sampler avec rep_window=64 (historique long sur
CB0). Override via env KZTTS_TEMP/TOPK/TOPP/REPP + KZTTS_CP_TEMP/TOPK/TOPP/REPP.
Validation sur 5 seeds (1, 42, 100, 777, 12345), phrase 'Bonjour je m'appelle Kazeia' :
- Tous EOS naturel (step 31-44, 2.48-3.67s audio @12Hz, proche du PY_REF 2.08s)
- silence_pct : 25-36% (vs > 60% avec greedy/sampler simple)
- cp_validate continue a sortir 495/495 codes match (greedy par defaut)
Vs WAV pipeline_v3 user-confirme 'parfait' (seed=123 temp=0.8 sans rep_penalty),
le sampler v4 est generique sur tout seed/texte, plus de besoin de chercher un
seed favorable. Trade-off : on n'a plus le PRNG torch donc l'audio diffère de
PyTorch a chaque run (different trajectoire), mais reste audio TTS coherent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>