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.
Optim decoder identifiée après bench sur audio FR continu (39 tokens) :
le `std::vector<>::emplace_back(data)` à chaque step recopiait 12 MB de
self KV inutilement = 2.4 GB de bande passante mémoire sur le loop entier.
Refactor zero-copy :
- Pré-allocation UNE FOIS des buffers persistants (self_k/v, mask, input_ids,
position_ids) au début de transcribe()
- Pré-création UNE FOIS des Ort::Value pointant sur ces buffers (les Ort::Value
sont des "borrowing views" — ORT lit les buffers au Run())
- Cache des indices k_self_out / v_self_out / logits_out (parsing 1 fois,
pas N fois dans le loop)
- Update self KV : memcpy direct depuis dec_outputs vers nos buffers persistants
(qui sont déjà les inputs du step suivant)
- mask_buf updaté à 1 fp16 par step (juste le slot R-to-L), pas 200
Bench zero-copy sur Snapdragon 8 Elite :
- Decoder/token : 98 ms -> 36 ms (-63%)
- Decoder total 39 tokens : 3831 ms -> 1390 ms (-64%)
- RTF audio 10s FR continu : 0.42 -> 0.18 (mieux que prod 0.51)
- RAM peak : 378 MB inchangée (zero-copy n'augmente pas la peak)
Sanity vérifié au runtime :
- Logits decoder shape = 51865 elements (= constante VOCAB_SIZE OK,
pas de buffer over-read sur argmax). Le vocab.json contient 50258 tokens
texte, les 1607 manquants sont les tokens langue/timestamp non décodés.
Doc STT_INTEGRATION.md :
- Chiffres perf à jour (RTF 0.18, decoder 36 ms/token)
- Nouvelle section "Checklist d'intégration" (6 points à vérifier en premier
côté app : System.loadLibrary, transcribe vs WhisperHybridEngine, null/edge,
VAD drop-in, threading IO, cycle de vie release)
- Roadmap optim future via Ort::IoBinding (gain estimé 30% sur decoder restant)
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>
Découverte historique remise au jour : la mémoire 'TTS RTF<1 atteint via
chraac-llama' (projet d'origine /opt/Kazeia, 02/05) documente que le
décodeur TTS atteint RTF 0.96 avec ggml-cpu de chraac-llama (dev-refactoring
commit 897501a) vs 1.47 avec llama-upstream (= notre ql/ggml actuel).
Notre Kazeia-Engine builde contre ql/ggml = github.com/qualcomm/llama.cpp
fork = essentiellement llama-upstream + hexagon backend. NEON heads optim
sortie cette session a déjà compensé une partie mais pas tout.
Validation reproductibilité :
Bench historique decoder seul (T=56) sur Pad3 chargée : RTF 0.935
(legèrement mieux que 0.961 historique, batterie pleine + device froid).
Build chraac variant :
- libs runtime : copies de /opt/Kazeia/chraac-llama/build-android-kazeia/bin
poussées dans /data/local/tmp/kz-engine/bin-chraac
- libqwen3tts-decoder.a : rebuild fresh contre chraac/ggml dans
/opt/Kazeia/kazeia-tts-decoder-ggml/build-android-chraac-fresh
(code actuel avec load_with_backends + snake_consts)
- tts_pipeline_chraac : linké contre ces libs, headers
/opt/Kazeia/chraac-llama/include + ggml/include
Mesure A/B Pad3 (KZTTS_THREADS=6 GGML_NUM_THREADS=8 seed=42) :
Phrase 'Bonsoir, comment tu te sens ce soir ?' :
Baseline ql/ggml : N=41 RTF 2.43 talker=31.5 cp=70.2 decoder/frame=98.1 ms
chraac-llama : N=64 RTF 2.04 talker=31.2 cp=57.5 decoder/frame=76.9 ms
Delta per-frame : -16% -1% -18% -22%
Phrase 'Bonjour Kazeia' :
Baseline : N=33 RTF 2.47 / Chraac : N=64 RTF 2.02
(Différence N = trajectoire diverge — précision f32 différente entre
stacks fait que le sampler talker produit des codes différents et
génère une phrase plus longue avant EOS)
Gains :
- Decoder -22% confirmé (cohérent avec ratio chraac 0.96 / llama-upstream 1.47)
- CP -18 à -21% (ggml_graph_compute repack/sgemm ARM optimisé chraac)
- Talker -1% (déjà au plafond NEON via llama.cpp)
- RTF total -16 à -18%
Bug découvert (à traiter plus tard) :
Phrase historique 56 codes 'Bonjour, je m'appelle Kazeia, je suis encore en phase de développement' (= ~60 frames) crash ggml_sin chraac (GGML_ABORT 'unsupported types' probable). Phrases moyennes (<= 50 frames)
passent OK. Sans doute incompat de types entre snake_consts (ctx_const dans
Decoder) et le path ggml_sin de chraac (qui n'a peut-être pas la même
variante f16/f32 que les ops récentes).
Path actuel ql/ggml RESTE le défaut. Le chraac est build séparé tts_pipeline_chraac
opt-in pour A/B. Libs chraac dans dist/lib-chraac/ (gitignored).
À faire next :
- Investiger crash ggml_sin chraac sur T>= ~50 frames
- Décider : swap définitif Kazeia-Engine sur chraac (avec lib-chraac
comme runtime principal) OU rester sur ql/ggml et patch chraac repack
optims dedans
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Re-analyse globale plateforme : le GPU n'avait JAMAIS été testé. Build
ggml-vulkan pour Android arm64 (NDK r27d + SPIRV-Headers + vulkan.hpp
téléchargés depuis github, libomp.so poussé sur tablette).
Adreno 830 détecté : uma=1 fp16=1 matrix cores=NONE (compute shaders seuls).
3 patterns testés :
(1) Decoder weights Vulkan buft + CPU compute : NEUTRE
RTF 2.45 -> 2.40-2.47 (bruit), WAV bit-exact
Cause : UMA = même DRAM, pas de gain BW
(2) Decoder + sched (GPU compute attendu) : tout CPU
sched route UMA buffer -> CPU (logique is_host)
Cause : sched ne déclenche pas l'offload pour buffers UMA
(3) Talker offload 29/29 layers via llama.cpp : REGRESSION +128%
Prefill 87->775 ms, talker 32->45 ms, decoder 3.3->6.6s
Cause : 0.6B + batch=1 + bus contention GPU/CPU sur DRAM partagée
Verdict cumulé HTP + Vulkan : sur SM8750 ce TTS spécifique, ni HTP ni
GPU n'apportent gain net. NEON CPU optimisé reste la voie unique
réaliste sur ce hardware.
Conso énergie : Vulkan talker = pipeline ×2.3 plus long, conso ~×2.
NEON CPU = meilleure efficience.
Verdict : RESTE SUR NEON CPU pour la prod.
Code committé opt-in (KZTTS_VULKAN_LIB, KZTTS_DECODER_VULKAN,
KZTTS_TALKER_VULKAN), désactivé par défaut. Path principal RTF 2.45 intact.
Détails complets dans dist/PERF_VULKAN_TESTS.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le profile fin (KZTTS_CP_PROFILE=1) a révélé que ~75% du temps CP par frame
est dans ggml_graph_compute (4-6 ms/sub-step × 15 = 80 ms) et ~22% dans le
delta CPU host (~24 ms/frame). Ce delta était entièrement le sample_head :
2048 dot products de 1024 floats SCALAIRE × 15 heads/frame = 31.5M ops/frame.
Implémentation NEON aarch64 16-way unroll (vfmaq_f32 × 4 accumulators) sur
dot_neon_1024(). Gated par KZTTS_CP_NO_NEON=1 pour fallback debug.
Mesure Pad3 (KZTTS_THREADS=6 GGML_NUM_THREADS=8, seed=42, Bonjour Kazeia) :
scalar : CP 106.8 ms/frame, total 8.05s, RTF 2.93
NEON : CP 72.1 ms/frame, total 6.75s, RTF 2.45 (-33% CP, -16% pipe)
WAV md5 identiques c3dd71cbc783013842a3dacedfefd758 (BIT-EXACT)
Le scalaire compilé par clang -O3 -march=armv8.6-a n'a apparemment pas
auto-vectorisé cette boucle malgré l'évidence (peut-être à cause des
indices non-trivials avec head_idx). NEON explicite débloque immédiatement.
Reste : decoder = 51% du total maintenant (3.34s sur 6.75s).
BigVGAN ggml-cpu déjà NEON vectorisé donc moins de marge évidente côté
host code. Cibles potentielles : text_projection 50ms one-shot au prefill,
mais marginal sur pipeline total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tests systématiques sur les hypothèses non-explorées :
A. USE_HMX=0 vs USE_HMX=1 sur sched CP :
- HVX 20% plus rapide que HMX à batch=1 (tile 32×32 sous-utilisé)
- Mais aucun ne bat baseline CPU (110 ms/frame)
- HVX et HMX bit-exact entre eux ; les deux divergent de CPU (codes
différents -> trajectoire talker diverge -> N=64 vs 33).
B. op_offload=true vs false : false -16% sur CP HMX (placement strict
moins coûteux). Quick win retenu (env KZTTS_CP_OFFLOAD).
C. Profile fin (KZTTS_CP_PROFILE=1) par sub-step :
- sched_reset+alloc : 0.1 ms (négligeable !)
- tensor_set/get : 0.0 ms (opt_hostbuf=1 = ION partagé)
- compute : 6-11 ms (95 % du temps !)
-> L'overhead sched n'est PAS le problème. Le compute HTP
lui-même est plus lent que CPU pour batch=1. Hypothèse #1
(sched_reserve + ctx persistant) INVALIDÉE.
D. Talker option C (use_htp=true, GGML_HEXAGON_GDN_PREFILL=1) :
- Prefill : 87 ms -> 185 ms (+112%)
- Talker per-frame : 32 ms -> 81 ms (+154%)
-> Pour un petit modèle (0.6B), l'overhead routing HTP par token
dépasse le gain compute. Le pattern option C documenté
marche pour Qwen3.5-4B, PAS pour Qwen3-TTS-Talker.
VERDICT GLOBAL : le HTP est non-rentable sur TOUS nos workloads TTS.
- BigVGAN : bloqué (nrows > 1024)
- CP : régresse (batch=1)
- Talker : régresse (trop petit pour amortir routing)
CLAUDE.md le notait partiellement ('CPU NEON bat HVX 3.4× sur M=1') mais
on n'avait pas tiré la conséquence globale : notre TTS n'a pas de
workload HTP-friendly.
LEVIERS RESTANTS (sans HTP) :
1. Streaming pipeline (1 session) : RTF inchangé mais TTFB 5s->500ms
2. Réduction overhead CP CPU (½ session) : ctx réutilisé entre sub-forwards
3. Decoder optim NEON conv1d : R&D long
4. Quantization Q8/Q4 CP : compromis qualité
Code committé reste opt-in (KZTTS_CP_HTP, KZTTS_DECODER_HTP) et neutre
au runtime. Le path par défaut RTF=3.0 est intact.
Détails complets dans dist/PERF_INFRA_TESTS.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cp_inference.h/.cpp : nouveau cp_load_with_backends + sched path dans
cp_forward_cached_step. KV cache aussi alloué sur backend buffer en mode sched
(no_alloc=true + ggml_backend_alloc_ctx_tensors_from_buft). Inputs (x, pos_ids,
mask) marqués ggml_set_input ; output ggml_set_output. Compute via sched_reset
+ alloc + tensor_set + sched_compute + tensor_get. Path legacy CPU pur (sans
sched) reste intact via détection s.sched.
tts_engine.cpp : KZTTS_CP_HTP=1 + KZTTS_CP_SCHED=1 active le path sched HMX.
KZTTS_CP_HTP=1 seul = poids HTP, compute CPU pur via opt_hostbuf=1 (baseline).
Mesures Pad3 (KZTTS_CP_CACHE=1, seed=42, 'Bonjour Kazeia') :
CPU baseline : CP 111 ms/frame, RTF 2.95, N=33
CP poids HTP, compute CPU : CP 108 ms/frame, RTF 2.93, N=33 (~baseline)
CP sched HMX (HTP-routé) : CP 181 ms/frame, RTF 3.87, N=64 REGRESSION
GGML_SCHED_DEBUG=2 confirme que ~95% des MUL_MAT CP tombent bien sur HTP0
(blk.0.attn_q.weight : 360 HTP / 21 CPU). Donc HMX est techniquement actif.
Cause de la régression :
- Overhead par sub-forward sched_reset + sched_alloc_graph + tensor_set/get
× 15 sub-forwards par frame. ~5 ms/appel × 15 = 75 ms ajoutés par frame
(cohérent avec 111 -> 181 ms).
- N=64 vs N=33 = trajectoire talker diverge à cause des codes CP qui
diffèrent (perte précision HMX f16 tile vs CPU NEON f32).
Pour livrer un gain CP HMX réel, il faudrait :
- (a) ctx persistant entre sub-forwards (~refactor architectural)
- (b) batcher les 15 sub-forwards en 1 graph autoregressif (~lourd)
Le path sched CP est laissé en opt-in (KZTTS_CP_SCHED=1, désactivé par défaut)
pour itérer dessus ultérieurement. KZTTS_CP_HTP=1 seul est neutre (~baseline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor stage_bigvgan en variante stage_bigvgan_sched :
- ggml_init(no_alloc=true) + mem_size réduit 12 GB -> 16 MB
- ggml_set_input/output sur input/output
- causal_conv1d_TC : ggml_pad_ext au lieu de memset zeros + concat
- apply_snake_TC : lookup d.snake_consts pré-calculé (au lieu de host exp() per-frame)
- Pattern sched_reset + alloc_graph + tensor_set + compute + tensor_get
- Switch dans Decoder::forward : sched ? stage_bigvgan_sched : stage_bigvgan
Snake constants pré-calculées au load_with_backends :
- 29 paires (a_eff, inv_b) stockées dans ctx_const + buf_const sur le même
backend que buf_w. exp() host-side une fois au load au lieu de chaque frame.
- buf_const ~0.1 MB sur HTP0 (29 × 2 × C × f32 avec C de 96 à 1536).
Mesure tablette (KZTTS_DECODER_HTP=1 KZTTS_DECODER_SCHED=1 GGML_HEXAGON_USE_HMX=1) :
baseline CPU pur : decoder 3.34s, bigvgan 3.19s
sched HTP path neuf : decoder 3.95s, bigvgan 3.76s -> REGRESSION 18%
Cause : GGML_SCHED_DEBUG=2 révèle que TOUS les MUL_MAT tombent sur CPU.
ggml_hexagon_supported_mul_mat rejette ggml_nrows(src1) > 1024 (commentaire
'no huge batches (for now)'). BigVGAN block 3 résiduels : conv1d k=7 sur
T=59520, im2col_output [672, 59520] -> 59520 rows >> 1024. Tous les
MUL_MAT BigVGAN sont rejetés -> fallback CPU + copies inutiles -> régression.
PHASE 2b STATUT : refactor fait et stable, mais le gain HMX nécessite
soit (a) modifier ggml-hexagon pour gros batches, (b) splitter MUL_MAT
en chunks ≤ 1024, (c) reprendre via étape F (CP HMX) qui n'a pas ce
problème (nrows ≤ 16 par sub-forward).
Snapshots updatés dans dist/decoder_patches/.
Le path activé seulement si KZTTS_DECODER_SCHED=1 (opt-in, désactivé par
défaut). En mode KZTTS_DECODER_HTP=1 sans SCHED : path legacy CPU pur, perf
inchangée vs baseline (poids juste relogés sur HTP buffer via opt_hostbuf=1).
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>
cp_inference.h/cpp : extrait de cp_runner.cpp standalone (kazeia-tts-decoder-ggml/),
adapté en module réutilisable. Architecture identique (qwen3 5L, GQA 16/8, head_dim
128, RoPE NEOX theta=1e6, q/k-norm AVANT RoPE, recompute T<=16 sans KV cache).
API : cp_load() + cp_predict(hidden[1024], cb0_emb[1024]) -> int32[15] = CB1..CB15.
cp_validate.cpp : reproduit sur tablette les conditions du standalone cp_runner
-> 495/495 codes match (33/33 frames parfaits) vs cp.generate(do_sample=False)
greedy golden, sur les 33 frames du dump 'Bonjour' historique. CP intégré =
bit-exact au standalone, qui est lui-même bit-exact à PyTorch.
tts_orchestrate.cpp mis à jour : si cp_f16.gguf/cp_heads/cp_codec_embs présents
dans <dump_dir>, le CP est ENABLED et remplace le teacher-forcing CB1..15.
Hidden state Talker capturé via llama_get_embeddings_ith(ctx,-1) après chaque
decode_embeds (pas besoin de re-instrumenter le forward Talker).
Mesure sur 'Bonjour je m'appelle Kazeia' (N=26 frames, audio 2.17s) tablette CPU 4t :
Talker prefill (T=19) : 0.124 s
Loop (N=26 steps) : 7.705 s talker_decode=0.763, cp=6.942
per-step talker : 29.3 ms
per-step CP (15 pass): 267.0 ms
Total Talker+CP : 7.829 s -> RTF talker+cp = 3.61
Le CP est le nouveau goulot (267ms × 15 passes/step). Levier P3.5 = KV cache CP
(recompute -> incremental), threads 8, ou Q8_0 du CP.
CB0 match golden = 3/26 = trajectoire stochastique Python diverge rapidement vs
notre greedy ; PAS un bug CP (cp_validate prouve la conformité bit-exact).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P3.1 - x_vector Damien dumpe offline : tts.model.extract_speaker_embedding(damien_15s_24k.wav)
-> f32[1024] = 4 KB. Embed dans l'app comme ressource. Pas de port du speaker
encoder Python (8.9M params), voix fixe = pre-calcul trivial.
P3.2 - reproduction C++ de la construction Python des talker_input_embeds.
Reference modeling_qwen3_tts.py:2124-2233 (mode x_vector_only + non_streaming).
Sequence prefill (19 positions pour 'Bonjour je m'appelle Kazeia', 8 tokens texte):
0..2 : text_projection(text_embed[input_ids[:3]]) role <im_start>assistant\n
3..7 : tts_pad + tok_embd[think/think_bos/lang_fr/think_eos/x_vec] 5 positions codec prefix
8 : tts_bos + tok_embd[codec_pad] 1 position
9..N+8: text_projection(text_embed[input_ids[3:-5]]) + tok_embd[codec_pad] text body
N+9 : tts_eos + tok_embd[codec_pad]
N+10 : tts_pad + tok_embd[codec_bos]
ou tts_*_embed = text_projection(text_embed[tts_bos/eos/pad_token_id]).chunk(3).
Composants : text_embed [151936, 2048] f32 (1.21 GB, a quantiser en P3.5),
text_projection ResizeMLP(2048->2048->1024, SiLU, bias=True), tok_embd talker
[3072, 1024], x_vector [1024], constants (special token ids).
Validation host (g++ -O2) sur phrase 'Bonjour je m'appelle Kazeia' :
mean_rmse = 3.2e-8, max_abs = 1.8e-6 sur les 19 positions vs prefill_embeds
capture du dump Python -> noise pure du f32 (eps ~1.2e-7).
Construction bit-exact.
Reste pour usage texte arbitraire : tokenizer BPE Qwen3 en C++ (llama_vocab
supporte cela en standard, a brancher en P3.4 quand on assemble la lib finale).
Pour l'instant input_ids dumpes depuis Python pour 1 phrase fixe = suffisant
pour P3.3 (integration CP) et P3.4 (assemblage).
Fixtures /opt/Kazeia/tts_talker_dump/ : text_embed.bin, tp_fc1_w/b.bin,
tp_fc2_w/b.bin, damien_xvector.bin, input_ids_full.bin, manifest_text.txt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug subtil dans llm_graph_input_pos::set_input (llama-graph.cpp:121-123) : pour
M-RoPE (n_pos_per_embd=4), la conversion 1D->4D des positions n'est faite QUE
pour ubatch.token. En embeds-mode (ubatch.embd != nullptr), la branche else fait
un memcpy raw de ubatch.pos avec taille T*4, lisant T*3 entiers out-of-bounds
au-dela du buffer T-only que j'allouais. -> positions garbage -> sortie cassee
(cos=-0.02 vs Python avant fix, soit pire qu'aleatoire).
Fix cote JNI (chantier P1) : detecte rope_type via llama_model_rope_type et
alloue T*4 positions quand MROPE/IMROPE, en remplissant les 3 premiers axes
avec la position et le 4eme avec 0 (text-only) -- exactement ce que le framework
fait pour les tokens. Memoire : decode_embd_batch dans kazeia_engine_jni.cpp.
Smoke test bit-exact Python vs engine (test_talker_replay.cpp, teacher-forced) :
- 19 positions prefill + 26 steps decode sur talker_f32.gguf (Bonjour je m'appelle Kazeia)
- hidden : mean_cos=1.000000, mean_rmse=2.9e-3
- logits : mean_cos=1.000000, mean_rmse=1.8e-3
- argmax engine == argmax Python sur les 26 steps (info only -- la divergence
RMSE residuelle est du f32 noise, ordre de sommation).
Le port Talker engine est valide bit-exact (precision f32) sur tablette CPU.
Reste P3 etape 3 : orchestration finale (BPE tokenizer + x-vector encoder +
cablage CP runner + decoder ggml) en mode standalone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Premières 2 pieces du chantier B (porter le Talker Qwen3-TTS sur l'engine
pour TTS standalone tablette, sans dépendre du pipeline Python).
P1 - API embeds-only dans le JNI (dist/jni/kazeia_engine_jni.cpp)
Le Talker a vocab=3072 codes audio (pas de BPE), entrée = embeds
pré-mélangés (text+x-vector au prefill, sum 16 codecs + tts_pad au decode),
sortie = logits[3072] + hidden[1024] (pour le Code Predictor).
Solution: pas un appel monolithique, 5 primitives qui laissent l'orchestration
côté caller:
- nEmbd(h), nVocab(h) - dimensionnement des buffers Kotlin
- resetEmbeds(h) - KV clear + pos=0
- prefillEmbeds(h, embds[T*n_embd], T, outHidden[n_embd])
- decodeEmbed(h, embd[n_embd], outLogits[vocab], outHidden[n_embd])
Implementation: llama_set_embeddings(ctx, true), llama_batch.embd au lieu
de .token, logits=1 sur la derniere position seulement (economie KV).
KEngine porte un compteur pos_embd interne. Kotlin wrapper miroir dans
dist/jni/EngineLlmEngine.kt.
P2 - M-RoPE qwen3 dense (sous-module ql, commit c1609a1)
Pointeur sous-module avance pour embarquer le support.
Validation device: dist/jni/test_talker.cpp charge talker_f32.gguf, lance
llama_decode embeds-only T=4 prefill puis 1 step, recupere hidden+logits
sans crash. Log device: print_info rope_type=8, mrope sections=[24,20,20,0],
prefill OK / step OK. argmax stable (entree bidon, juste smoke).
Libs rebuiltes: dist/lib/libllama.so + libkazeia_engine.so + libggml-base.so.
Reste P3 = cablage Talker->CP->Decoder cote orchestration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 hypothèses éliminées par mesure: overflow fp16 (act=9.8), NaN/Inf inputs (0), shape pure (isolation
synthétique k=4096 ne crashe pas). Op fautive = o_proj MUL_MAT q4_0 k=4096 après FLASH_ATTN_EXT.
Prochaines étapes révisées (dump-and-replay value-vs-context, instrumentation DSP log-vers-buffer).
OPLOG mis à jour (nan/inf) dans dist/lib. ql -> commit OPLOG.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CHANTIER_HMX_DENSE.md: analyse + outils pour le crash matmul HMX dense. Finding: PAS un overflow
fp16 (activation du matmul fautif = 9.8) — c'est un bug de tiling/buffer HMX pour k=4096 (o_proj
après FLASH_ATTN_EXT). Localisé via OPLOG CPU-side (FARF DSP bloqué sans root). Étapes + repro documentés.
HANDOFF: .pte retiré -> engine seul runtime LLM. Dense=HVX (40) sur l'engine; HMX (295)=ce chantier.
dist/lib refreshé (libggml-hexagon.so avec OPLOG dormant, htp-v79 propre). ql -> 7d4cade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Analyse complète (méthode SSM_CONV) du crash dense-HTP:
- bench HTP synthétique (pp512, +tg8) = OK ; cli/engine texte réel crashe 0x2e > ~14 tokens.
- OPFILTER bisect: SOFT_MAX/ROPE/RMS_NORM/GET_ROWS/CPY/CONT/ADD n'arrêtent PAS le crash ; MUL_MAT->CPU OUI.
- GGML_HEXAGON_USE_HMX=0 (matmul HVX) supprime le crash, =1 le reproduit. => le matmul HMX (fp16 tile)
faute sur les activations réelles du dense (massive activations Qwen3 > plage fp16 ; bench synthétique
ne déclenche pas l'overflow). Analogue du SSM_CONV pour la 3.5, mais ici c'est le matmul HMX coeur.
Câblage: lecture general.architecture dans le GGUF AVANT init backend -> hybride (qwen35/qwen3next):
HMX on + option C (préservé) ; dense: USE_HMX=0 + HTP contexte-unique (HVX, ~40 prefill =3x CPU, decode ~11,
stable). Repli CPU si pas de HTP. Validé test_jni_native: dense (HVX HTP) + 3.5 (option C) cohérents.
TODO backend: matmul HMX robuste aux activations hors plage fp16.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Détection au load via llama_model_is_hybrid/is_recurrent (structure, pas string d'archi):
- hybride GDN (qwen3.5/qwen3next) -> option C (prefill HTP/decode CPU), préservé+validé.
- dense (qwen3, etc.) / pas de HTP -> CPU pur (universel, robuste).
Charge l'instance CPU d'abord (détection + universel), n'ajoute l'instance HTP que pour l'hybride.
Diagnostic dense-HTP: le prefill dense sur HTP CRASHE (dspqueue_read 0x2e) sur prompt réaliste
(reproduit aussi en llama-cli -dev HTP0 -fa 1; le "ça marche" antérieur = prompt minuscule sans fa,
cas chanceux). Bug backend HTP attention dense multi-token, non corrigé. Dense a un .pte (451) de
toute façon -> dense=CPU robuste. Validé test_jni_native: dense (CPU) + 3.5 (option C) cohérents.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le JNI lit general.architecture: qwen35/qwen3next -> option C (prefill HTP/decode CPU, préservé
et revalidé cohérent+mémoire). Tout le reste (qwen3 dense, etc.) -> CPU pur (universel, jamais de
crash). Dense Qwen3-4B validé via le moteur (CPU, cohérent). 3.5 non régressée.
Diagnostic crash dense-HTP: ce n'est PAS le HTP (dense single-context HTP marche: cli 36.8/11.4
cohérent, llama-bench pp512=98). Le crash 0x2e (dspqueue_read, flush_pending) survient UNIQUEMENT
en dual-load (option C: 2 instances modèle) appliqué au dense. Or le dense n'a pas besoin de
l'option C (decode HTP 11.4 ≈ CPU 11.7, pas de pénalité GDN). Donc dense -> CPU (ou single-ctx HTP).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ferme les 2 derniers trous d'intégration:
- generateRaw(prompt) exposé (cœur option C partagé avec generate). EngineLlmEngine.kt: ChatSession
gère l'historique + construit le ChatML Qwen3.5 validé -> multi-tour propre sans que l'app connaisse
le template (structure identique à dual_ctx_mt).
- test_jni_native.cpp: dlopen + JNIEnv mock teste le .so shippé bout-en-bout sur device
(load/generate/generateRaw/free). Résultat: FR cohérent + mémoire conversationnelle OK
("Marc, je me souviens parfaitement de ton prénom"). Marshalling JNI réel validé.
libkazeia_engine.so reconstruit (5 symboles). Paquet dist/ prêt à intégrer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
libkazeia_engine.so reconstruit depuis le JNI option C (37KB strippé, 4 symboles JNI, SHARED,
NEEDED llama/ggml/ggml-base/log/c++_shared). Le paquet dist/ est désormais interne-cohérent:
sources + prebuilts alignés (option C + fix SSM_CONV). Docs: prebuilts à jour, rebuild app-side
recommandé pour ABI/STL. Logique validée via dual_ctx_mt (JNI non testé via Java ici).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
generate() = prefill prompt sur instance HTP (ngl99, device HTP0, OPFILTER=SSM_CONV +
GDN_PREFILL posés au load) -> llama_state_seq transfère le KV -> decode sur instance CPU
(ngl0, t4+fa, KV f16). 2 instances (~4.7GB). Sans état entre appels (app passe l'historique).
Validé multi-tour via jni/dual_ctx_mt.cpp: 3 tours cohérents FR + mémoire conversationnelle
(rappelle prénom/âge du tour 1 au tour 3), prefill HTP 103-144 t/s, decode CPU ~7-9.
Compile+linke OK (4 symboles JNI, CMakeLists inchangé). Rebuild libkazeia_engine.so requis
avant ship (le .so livré = ancienne version CPU-only/q8_0).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Investigation détaillée: décomposé le prefill HTP charabia op par op (test-backend-ops -b HTP0
par op + OPFILTER bisect en génération). Coupable unique = SSM_CONV (conv1d) cassé sur HTP en
multi-token (prefill) pour d_inner=1024/2048 (oracle FAIL ERR~1.3; decode n=1 OK; 1536 passe;
bug backend hexagon, HVX+scalaire HTP faux, ggml-cpu correct). FIX immédiat: GGML_HEXAGON_OPFILTER
=SSM_CONV (conv sur CPU, reste HTP) -> prefill HTP 181 t/s + sortie FR cohérente, x13 vs CPU 14.
Renverse le "prefill HTP mort": il est récupérable. 3 configs (A CPU-only livrée / B mono-ctx
HTP+OPFILTER 181/6.4 / C dual-ctx 181/10.9 +2.3GB). TODO: corriger SSM_CONV HTP (gate oracle).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vérifié 27/05 batterie pleine, 3 fois (harness dual-ctx, device HTP0 explicite, llama-cli
-dev HTP0 sortant de l'anglais cassé sur prompt FR). Le débit HTP (126-189 t/s) est réel mais
l'état/logits produits sont corrompus -> generation inutilisable. Preuve que c'est le prefill et
non le transfert: decode CPU depuis état prefillé-CPU = FR cohérent, depuis état prefillé-HTP =
charabia. Dense Qwen3-4B sur HTP crashe (V79 0x2e). Donc les "189/98/285 prefill HTP" cités partout
= débit jamais validé en sortie. Renverse l'hypothèse de fond "prefill HTP utilisable".
=> JNI reste ngl0 (CPU prefill 14 + CPU decode 10.9), seule config correcte. jni/dual_ctx.cpp =
harness de validation (prouve aussi que le KV transfer 2-contextes CPU->CPU est cohérent et prêt
le jour où le prefill HTP sera numériquement correct = bug backend frontière). Docs corrigées.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reconfirmé device froid 90% (les chiffres throttlés d'hier soir étaient faux):
- KV f16 >> q8_0 au decode: q35-lmq4 f16=10.9 vs q8_0=6.5 (-40%, idem d=512). Le déquant KV
flash-attn coûte plus que le BW. JNI corrigé type_k/v=F16 (rebuild .so requis avant ship;
le .so livré=q8_0 validé, inchangé).
- q35-lmq4 prefill HTP=189 ~= 2x Qwen3.5-Q4_0 (98): embeds/output Q4 restent sur NPU vs Q6_K
fallback CPU. Argument fort de plus pour q35-lmq4 (+5% decode ET ~2x prefill HTP).
- decode sain: q35-lmq4 10.9, dense 11.7. prefill CPU JNI=14 (t4).
Decision KV résolue; decision prefill CPU-only vs HTP ouverte (189 motive le câblage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>