diff --git a/dist/HANDOFF.md b/dist/HANDOFF.md index e527620..f75bdf6 100644 --- a/dist/HANDOFF.md +++ b/dist/HANDOFF.md @@ -24,19 +24,25 @@ Config déjà correcte et alignée : (−40%, idem à d=512). Le déquant KV en flash-attn coûte plus que le BW épargné. `type_k/v` repassé en `GGML_TYPE_F16`. → **rebuild `libkazeia_engine.so` avant de shipper** (le .so livré contient encore l'ancien q8_0). q8_0 seulement si OOM KV en très long contexte. -2. **Prefill : CPU-only vs HTP (à toi).** Le JNI est CPU-only (`ngl0`) → prefill **14 t/s** (t4). - Le HTP fait **189 t/s** avec q35-lmq4 (≈2× le Q4_0=98 : ses embeds Q4 restent sur NPU au lieu de - retomber CPU). Câbler le prefill HTP vaut donc le coup (prompt 500 tok : ~2,6 s HTP vs ~35 s CPU). - Obstacle : ping-pong du mono-contexte ngl99 (decode GDN rebondit NPU↔CPU = 0,2 t/s). Vrai fix = - 2 contextes (prefill HTP → transfert d'état → decode CPU ngl0) ou état partagé. **CPU-only suffit - pour des tours courts ; câbler HTP si prompts longs / RAG.** +2. **Prefill : CPU-only OBLIGATOIRE — le prefill HTP est CASSÉ (vérifié 27/05).** Le HTP a un gros + débit (q35-lmq4 126-189 t/s) MAIS produit un **état/logits corrompus → sortie charabia**. Confirmé 3× + (harness dual-contexte, device HTP0 explicite, et `llama-cli -dev HTP0` sortant de l'anglais cassé + sur un prompt FR). Le dense Qwen3-4B sur HTP **crashe** (V79 0x2e). Preuve que c'est le prefill et pas + le transfert : decode CPU repartant d'un état prefillé-CPU = FR cohérent ; repartant d'un état + prefillé-HTP = charabia. Donc les « 189/98/285 » sont du **débit jamais validé en sortie**, inutilisables. + → **JNI reste `ngl0` (CPU prefill 14 t/s + CPU decode 10.9).** Mécanique du split (2 contextes + transfert + KV) validée et prête (jni/dual_ctx.cpp) — réutilisable LE JOUR où le prefill HTP sera numériquement + correct (bug backend frontière). Pour accélérer le prefill maintenant : raccourcir le prompt/historique. ## Perf (sains, 27/05, device froid) | | prefill | decode | RAM | |---|--:|--:|--:| -| q35-lmq4 — JNI actuel (CPU-only) | 14 (CPU t4) | **10.9** (t4+fa, KV f16) | 2.4 GB | -| q35-lmq4 — capacité HTP (si câblé) | **189** | — | +~3.3 ION | -| dense Qwen3-4B (repli) | 98 HTP / ~14 CPU | 11.7 | 2.2 GB | +| q35-lmq4 — JNI (CPU-only, **config livrée**) | 14 (CPU t4) | **10.9** (t4+fa, KV f16) | 2.4 GB | +| q35-lmq4 — HTP | 126-189 *(débit, sortie CASSÉE → inutilisable)* | — | — | +| dense Qwen3-4B | ~14 CPU / HTP crash 0x2e | 11.7 | 2.2 GB | + +Decode = ce que l'utilisateur ressent (10.9, OK). Prefill CPU 14 t/s → prompt 200 tok ≈ 14 s ; +garder le system prompt + l'historique courts. Le prefill HTP rapide existe mais sort du charabia (cf décision 2). ## Système (prompt) — `system_fr.txt` fourni Inclut les garde-fous (3114, pas de prescription) + 2 correctifs issus de l'éval : diff --git a/dist/PERF.md b/dist/PERF.md index b8d1974..e1482b2 100644 --- a/dist/PERF.md +++ b/dist/PERF.md @@ -1,22 +1,23 @@ # Perf mesurée SM8750/V79 — MAJ 27/05 (fork ql=qualcomm/llama.cpp), batterie pleine device froid -Decode optimum = **t4 / fa1 / KV f16** (PAS t8 : contention ; PAS KV q8_0 : déquant coûteux). Prefill HTP : ngl99 / t8 / b512. +Decode optimum = **t4 / fa1 / KV f16** (PAS t8 : contention ; PAS KV q8_0 : déquant coûteux). **Prefill = CPU** (le HTP sort du charabia, cf ci-dessous). -| Modèle | prefill HTP | prefill CPU (t4) | decode CPU (t4+fa, KV f16) | RAM | +| Modèle | prefill CPU (t4) | prefill HTP (débit) | decode CPU (t4+fa, KV f16) | RAM | |---|--:|--:|--:|--:| -| Qwen3-4B dense | 98 | ~14 | 11.7 | 2.2 | -| **q35-lmq4** (Qwen3.5-4B, embeds Q4) | **189** | 14 | **10.9** | 2.4 | -| Qwen3.5-4B-Q4_0 (embeds Q6_K) | 98 | ~14 | 10.8 | 2.4 | -| Qwen3.5-9B | 70 | — | ~4-5 | 5.0 | +| **q35-lmq4** (Qwen3.5-4B, embeds Q4) | **14** | 189 *(sortie CASSÉE)* | **10.9** | 2.4 | +| Qwen3.5-4B-Q4_0 (embeds Q6_K) | ~14 | 98 *(cassé)* | 10.8 | 2.4 | +| Qwen3-4B dense | ~14 | crash 0x2e | 11.7 | 2.2 | +| Qwen3.5-9B | — | — | ~4-5 | 5.0 | Notes : -- **KV f16 ≫ q8_0 au decode** : q35-lmq4 f16=10.9 vs q8_0=6.5 (−40%, idem à d=512). Le déquant KV - en flash-attn coûte plus que le BW épargné. → JNI en `type_k/v=F16`. q8_0 seulement si OOM KV long. -- **q35-lmq4 prefill HTP = 189 ≈ 2× le Q4_0 (98)** : ses embeds/output Q4 restent sur NPU, alors que - le Q6_K par défaut retombe sur CPU (une partie du « mur prefill » était ce fallback lm_head, pas le GDN). -- **GDN-compute n'est PAS le goulot prefill** : serial f32 / fp16 / WY chunkwise = même pp512. Piste - kernel GDN **close** (RAPPORT_RD §7). Decode = plafond BW CPU ~25 GB/s. -- **Bridge JNI = CPU-only (ngl0)** → prefill 14 (pas 189). Le 189 = capacité HTP, non câblée (ping-pong ngl99). -- NPU≈CPU au decode (GEMV M=1 latency-bound, 77 GB/s injoignable). RAM = GGUF mmap + ~3.3 GB ION/session. +- **PREFILL HTP = CASSÉ (27/05, vérifié 3×).** Débit réel (126-189 t/s) mais **état/logits corrompus → + sortie charabia** (harness dual-ctx, device HTP0 explicite, et `llama-cli -dev HTP0` → anglais cassé sur + prompt FR). Preuve = c'est le prefill, pas le transfert : decode CPU depuis état prefillé-CPU = FR cohérent, + depuis état prefillé-HTP = charabia. Les anciens « 189/98/285 » = débit jamais validé en sortie, **inutilisables**. + Dense sur HTP **crashe** (V79 0x2e). → **prefill obligatoirement CPU.** Le KV transfer (Path A) marche + (CPU→CPU cohérent), prêt si le prefill HTP devient un jour correct (bug backend frontière). +- **KV f16 ≫ q8_0 au decode** : q35-lmq4 f16=10.9 vs q8_0=6.5 (−40%, idem à d=512). → JNI `type_k/v=F16`. +- **GDN-compute n'est PAS le goulot prefill** (débit) : serial f32 / fp16 / WY = même pp512. Piste kernel GDN close. +- NPU≈CPU au decode (GEMV M=1 latency-bound). RAM = GGUF mmap + ~3.3 GB ION/session. Leviers MORTS (mesurés) : spec-decode net-négatif ; ngram idem ; IQ4_NL = mêmes octets ; **quant sous-Q4 = decode CPU plus lent** (Q3_K_M −20%) ; **KV q8_0 = decode −40%** ; kernel GDN (chunkwise/HMX/fp16) = 0 sur prefill. diff --git a/dist/STATUS.md b/dist/STATUS.md index 15fcdcd..8aedf81 100644 --- a/dist/STATUS.md +++ b/dist/STATUS.md @@ -5,9 +5,10 @@ thinking-off déterministe. Modèle tranché = `q35-lmq4` (éval qualité ../eva Perf : prefill clos côté kernel GDN (pas le goulot), decode au plafond CPU. LIMITES / OUVERT : -1. **Bridge = CPU-only (ngl0)** : prefill sur CPU (~18-19 t/s), pas HTP. HTP (89.5) non câblé - (ping-pong mono-contexte). OK tours courts ; split HTP→CPU = non trivial, à faire si RAG/long. -2. **KV q8_0 à revérifier** sur batterie pleine (peut coûter au decode à court contexte). +1. **Prefill HTP = CASSÉ (sortie charabia), prefill OBLIGATOIREMENT CPU (~14 t/s).** Vérifié 27/05 + (cli -dev HTP0 + harness). Le débit HTP est réel mais l'état produit est corrompu. Dense sur HTP crashe + (0x2e). Bridge CPU-only (ngl0) = la seule config correcte. cf PERF.md / HANDOFF.md décision 2. +2. **KV = f16 (résolu, JNI corrigé)** : f16=10.9 vs q8_0=6.5 au decode. Rebuild .so requis (livré=q8_0). 3. `generate()` applique déjà ChatML + thinking-off. App fournit `sys`/`usr` (voir system_fr.txt). 4. TTS Talker GGUF chargeable mais **audio e2e non validé** (= intégration app). 5. STT inchangé (ORT-QAIRT). 9B/30B/35B écartés (lents/OOM). diff --git a/dist/jni/dual_ctx.cpp b/dist/jni/dual_ctx.cpp new file mode 100644 index 0000000..4228d9a --- /dev/null +++ b/dist/jni/dual_ctx.cpp @@ -0,0 +1,90 @@ +// dual_ctx.cpp — valide Path A: prefill HTP (ctx ngl99) -> transfert KV -> decode CPU (ctx ngl0). +// Mesure: RAM des 2 loads, débit prefill HTP, débit decode CPU, cohérence de la continuation. +// Build: NDK clang++ -Iinclude, link dist/lib. Run: GGML_HEXAGON_GDN_PREFILL=1 LD_LIBRARY_PATH=lib ./dual_ctx model.gguf +#include +#include +#include +#include +#include +#include +#include +#include "llama.h" +#include "ggml-backend.h" + +static int64_t usec(){ struct timespec ts; clock_gettime(CLOCK_MONOTONIC,&ts); return (int64_t)ts.tv_sec*1000000+ts.tv_nsec/1000; } +static long rss_mb(){ FILE*f=fopen("/proc/self/status","r"); if(!f)return -1; char l[256]; long k=0; + while(fgets(l,sizeof l,f)) if(sscanf(l,"VmRSS: %ld kB",&k)==1) break; fclose(f); return k/1024; } + +int main(int argc, char** argv){ + if(argc<2){ printf("usage: dual_ctx model.gguf\n"); return 1; } + llama_backend_init(); + fprintf(stderr,"RSS start : %ld MB\n", rss_mb()); + + const char* e=getenv("KZ_NGL_H"); int ngl_h = e? atoi(e):99; // control: KZ_NGL_H=0 => ctx_h aussi CPU + auto mp_h=llama_model_default_params(); mp_h.n_gpu_layers=ngl_h; + // sélection explicite du device HTP0 (comme -dev HTP0) pour ctx_h + static ggml_backend_dev_t devs[2]={nullptr,nullptr}; + for(size_t i=0;i0 && devs[0]){ mp_h.devices=devs; fprintf(stderr,"ctx_h: device HTP0 explicite\n"); } + auto m_h=llama_model_load_from_file(argv[1],mp_h); if(!m_h){printf("nomodel htp\n");return 1;} + fprintf(stderr,"RSS after HTP(ngl99) : %ld MB\n", rss_mb()); + auto mp_c=llama_model_default_params(); mp_c.n_gpu_layers=0; + auto m_c=llama_model_load_from_file(argv[1],mp_c); if(!m_c){printf("nomodel cpu\n");return 1;} + fprintf(stderr,"RSS after +CPU(ngl0) : %ld MB (delta = coût 2e instance)\n", rss_mb()); + + auto base=llama_context_default_params(); base.n_ctx=2048; base.n_batch=2048; + base.flash_attn_type=LLAMA_FLASH_ATTN_TYPE_ENABLED; base.type_k=GGML_TYPE_F16; base.type_v=GGML_TYPE_F16; + auto cph=base; cph.n_threads=8; auto ctx_h=llama_init_from_model(m_h,cph); + auto cpc=base; cpc.n_threads=4; auto ctx_c=llama_init_from_model(m_c,cpc); + auto vo=llama_model_get_vocab(m_h); + auto s=llama_sampler_init_greedy(); + fprintf(stderr,"RSS after 2 ctx : %ld MB\n", rss_mb()); + + int rep = (argc>2)? atoi(argv[2]) : 1; // argv[2]=N: répète la phrase (gonfle le prompt pour test perf) + std::string pr="<|im_start|>system\nTu es Kazeia, soutien psy bref en francais, tutoiement.<|im_end|>\n<|im_start|>user\n"; + for(int i=0;i t(n); + llama_tokenize(vo,pr.c_str(),pr.size(),t.data(),n,true,true); + fprintf(stderr,"prompt tokens : %d\n", n); + + int64_t a=usec(); auto b=llama_batch_get_one(t.data(),n); + if(llama_decode(ctx_h,b)!=0){printf("prefill fail\n");return 1;} + int64_t bb=usec(); fprintf(stderr,"PREFILL HTP : %d tok / %.0f ms = %.1f t/s\n", n,(bb-a)/1e3, n*1e6/(double)(bb-a)); + + size_t sz=llama_state_seq_get_size(ctx_h,0); std::vector buf(sz); + llama_state_seq_get_data(ctx_h, buf.data(), sz, 0); + size_t got=llama_state_seq_set_data(ctx_c, buf.data(), sz, 0); + fprintf(stderr,"KV transfer HTP->CPU : %zu bytes (set=%zu)\n", sz, got); + + // REF: decode quelques tokens sur le contexte HTP lui-même (sans transfert) -> isole prefill vs transfert + { llama_token id=llama_sampler_sample(s,ctx_h,-1); std::string ref; int p=n; + for(int i=0;i<12;i++){ char z[128]; int l=llama_token_to_piece(vo,id,z,sizeof z,0,true); if(l>0)ref.append(z,l); + if(llama_vocab_is_eog(vo,id))break; llama_token tk=id; llama_pos pp=p; int32_t ns=1; llama_seq_id sd=0,*sp=&sd; int8_t lg=1; + llama_batch sb; memset(&sb,0,sizeof sb); sb.n_tokens=1; sb.token=&tk; sb.pos=&pp; sb.n_seq_id=&ns; sb.seq_id=&sp; sb.logits=≶ + if(llama_decode(ctx_h,sb)!=0)break; p++; id=llama_sampler_sample(s,ctx_h,-1); } + fprintf(stderr,"REF decode sur ctx HTP (memes poids, pas de transfert): '%s'\n", ref.c_str()); } + // restaure le snapshot dans ctx_h (le self-decode l'a avancé) pour repartir propre + llama_state_seq_set_data(ctx_h, buf.data(), sz, 0); + + llama_token id=llama_sampler_sample(s,ctx_h,-1); + fprintf(stderr,"first id (from HTP prefill logits): %d\n", id); + std::string out; int pos=n, ndec=0; int64_t d0=usec(); + for(int i=0;i<64;i++){ + char z[128]; int l=llama_token_to_piece(vo,id,z,sizeof z,0,true); if(l>0) out.append(z,l); + if(i<8) fprintf(stderr," tok[%d]=%d piece='%.*s' eog=%d\n", i, id, l>0?l:0, l>0?z:"", llama_vocab_is_eog(vo,id)); + if(llama_vocab_is_eog(vo,id)) break; + llama_token tok=id; llama_pos p=pos; int32_t nsi=1; llama_seq_id sid=0, *sidp=&sid; int8_t lg=1; + llama_batch sb; memset(&sb,0,sizeof sb); + sb.n_tokens=1; sb.token=&tok; sb.pos=&p; sb.n_seq_id=&nsi; sb.seq_id=&sidp; sb.logits=≶ + if(llama_decode(ctx_c,sb)!=0){fprintf(stderr,"decode fail @%d\n",i);break;} + pos++; ndec++; id=llama_sampler_sample(s,ctx_c,-1); + } + int64_t d1=usec(); + fprintf(stderr,"DECODE CPU : %d tok = %.1f t/s\n", ndec, ndec*1e6/(double)(d1-d0)); + fprintf(stderr,"RSS peak : %ld MB\n", rss_mb()); + printf("OUT: %s\n", out.c_str()); + return 0; +}