dist: métriques vérifiées batterie pleine — KV f16 (pas q8_0), q35-lmq4 HTP=189

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>
This commit is contained in:
Richard Loyer 2026-05-27 09:23:57 +02:00
parent fac50d69ce
commit c9d4e625eb
3 changed files with 36 additions and 30 deletions

29
dist/HANDOFF.md vendored
View File

@ -19,23 +19,24 @@ Config déjà correcte et alignée :
- API : `load(path,nCtx) → generate(h,sys,usr,maxTok) → reset(h) / free(h)`.
- **`n_gpu_layers = 0` → tout CPU** (pas de HTP). Choix après le ping-pong ngl99 (decode 0.2 t/s).
## ⚠ Les 2 décisions ouvertes (à toi, sur batterie pleine)
1. **Prefill : CPU-only vs HTP.** Le JNI est CPU-only → prefill ~18-19 t/s (sain). Le HTP fait
89.5 t/s mais n'est PAS câblé (le ping-pong vient du mono-contexte ngl99 qui renvoie le
decode sur NPU). Pour des tours psy courts, CPU suffit (prompt ~100-300 tok → 5-15 s). Pour
du RAG/long, il faudra un vrai split (prefill HTP → bascule decode CPU), non trivial en
mono-contexte llama.cpp. **Garder CPU-only tant que les prompts restent courts.**
2. **KV `q8_0` : à revérifier.** Le JNI met `type_k/v=q8_0`. Mesure (batterie faible, throttle)
a montré decode q8_0=6.6 vs KV-f16=10.8 plus tôt → le déquant KV pourrait coûter plus que
le gain BW à court contexte (même effet que la quant poids sous-Q4). **A/B f16 vs q8_0 sur
batterie pleine ; si f16 gagne, repasse `type_k/v=GGML_TYPE_F16`.** (q8_0 ne sert que la RAM
KV en long contexte.)
## Décisions (vérifiées 27/05, batterie 90%, device froid)
1. **KV = f16 (RÉSOLU, corrigé dans le JNI).** Mesuré : 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é. `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.**
## Perf (chiffres sains — la tablette throttlait à 11% ce soir, à reconfirmer)
## Perf (sains, 27/05, device froid)
| | prefill | decode | RAM |
|---|--:|--:|--:|
| q35-lmq4 (JNI CPU-only) | ~18-19 (CPU) | **~10.8** (t4+fa, KV f16) | 2.4 GB |
| capacité HTP (si câblé) | 89.5 | — | +~3.3 ION |
| 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 |
## Système (prompt) — `system_fr.txt` fourni
Inclut les garde-fous (3114, pas de prescription) + 2 correctifs issus de l'éval :

30
dist/PERF.md vendored
View File

@ -1,20 +1,22 @@
# Perf mesurée SM8750/V79 — MAJ 26/05 (fork ql=qualcomm/llama.cpp)
Decode optimum = **t4 / fa1** (PAS t8 : contention, 3.5-4B chute). Prefill HTP : t8 / b512.
# 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.
| Modèle | prefill HTP (t8/b512) | prefill CPU (t4) | decode CPU (t4+fa) | RAM |
| Modèle | prefill HTP | prefill CPU (t4) | decode CPU (t4+fa, KV f16) | RAM |
|---|--:|--:|--:|--:|
| Qwen3-4B dense | 285 | ~19 | ~11.7 | 2.2 |
| Qwen3.5-4B (q35-lmq4) | 89.5 | ~18-19 | **~10.8** | 2.4 |
| 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 |
Notes (chiffres sains ; reconfirmer hors throttle batterie) :
- **GDN-compute n'est PAS le goulot du prefill** : serial f32 / fp16 / WY chunkwise donnent le
même pp512 (~89.5). Piste kernel GDN **close** (RAPPORT_RD §7). Decode = plafond BW CPU ~25 GB/s.
- **Bridge JNI = CPU-only (ngl0)** → prefill ~18-19 (pas 89.5). Le 89.5 est la capacité HTP, non câblée.
- **KV q8_0 à revérifier** : mesure dégradée a donné decode q8_0 < f16 (déquant KV coûteux à court
contexte, comme la quant poids). A/B sur batterie pleine ; si f16 gagne, dropper q8_0 du JNI.
- `q35-lmq4` (embeds Q4) = +5% decode vs Q4_0, qualité ≈.
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.
Leviers MORTS (mesurés) : spec-decode net-négatif ; ngram idem (vérif GDN coûteuse) ; IQ4_NL =
mêmes octets ; **quant sous-Q4 = decode CPU plus lent** (Q3_K_M 20%) ; kernel GDN (chunkwise/HMX/fp16) = 0 sur prefill.
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.

View File

@ -17,8 +17,11 @@ Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx)
auto m = llama_model_load_from_file(p, mp); e->ReleaseStringUTFChars(path, p);
if (!m) return 0;
auto cp = llama_context_default_params(); cp.n_ctx = nctx; cp.n_threads = 4; cp.n_batch = 512;
cp.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_ENABLED; // t4+fa = 14t/s decode (t8=9.8 contention; fa coupe trafic KV)
cp.type_k = GGML_TYPE_Q8_0; cp.type_v = GGML_TYPE_Q8_0; // KV q8: 14->15.5 dense (>pte), 8.5->9.8 hyb, quasi-lossless
cp.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_ENABLED; // t4+fa optimum decode (t8=contention)
// KV f16 (PAS q8_0): mesuré 27/05 batterie pleine, q35-lmq4 decode f16=10.9 vs q8_0=6.5 (-40%,
// déquant KV en flash-attn coûte plus que le BW à court/moyen contexte; idem d=512). q8_0 ne
// servirait que la RAM KV en très long contexte. Repasser q8_0 seulement si OOM KV avéré.
cp.type_k = GGML_TYPE_F16; cp.type_v = GGML_TYPE_F16;
auto* k = new KEngine{m, llama_init_from_model(m, cp), llama_model_get_vocab(m),
llama_sampler_init_greedy()};
return (jlong)k;