chantier RAG #1 : entrypoint embeddings (texte -> vecteur poolé) pour le RAG mobile
Spec /opt/Kazeia/docs/RAG_EMBEDDINGS_ENGINE_SPEC.md implémentée. Préflight (point #2 du spec = LE bloqueur potentiel) : - Arch BERT/nomic-bert/gemma-embedding TOUJOURS présentes dans le fork ql (llama-arch.cpp + src/models/bert.cpp), malgré les mods Qwen/TTS. - Outil de référence examples/embedding/embedding.cpp dispo = source de vérité. - Confirmé : BERT encoder-only routé via llama_decode (PAS llama_encode) dans ce fork ; le code ne bascule sur erreur que pour encoder+decoder (T5). Ajouts kazeia_engine_jni.cpp (handle KEmbedder SÉPARÉ du KEngine, CPU pur) : - loadEmbedder(path, nThreads, pooling) : embeddings=true, pooling MEAN/CLS/auto, n_ctx=n_batch=n_ubatch=512 (contrainte pooling = séquence dans 1 ubatch), n_gpu_layers=0, refus explicite des modèles encoder-decoder. - embedText(h, text) : tokenize add_special + batch logits=1 + llama_decode + llama_get_embeddings_seq + normalisation L2 -> float[n_embd] ou null. - freeEmbedder(h). - Reproduit À L'IDENTIQUE examples/embedding/embedding.cpp. Bindings Kotlin (EngineLlmEngine.kt) : loadEmbedder/embedText/freeEmbedder + wrapper EmbedderEngine(model, nThreads, pooling).embed(text). Validation sur tablette SM8750 (adb shell, nomic-embed Q4_K_M, CPU) : - Déterminisme : max|diff| = 0.0 bit-identique sur 2 runs - Norme L2 = 1.0000 - PARITÉ RÉFÉRENCE : mon embedText vs llama-embedding = max|diff| 5e-7, cos 1.000000 (probe rag_probe.cpp répliquant le chemin exact) - Cohérence sémantique : OK avec bons préfixes FINDING modèle (point #1 du spec confirmé par mesure) : nomic-embed est anglo-centré, marge FR fine (cos insomnie 0.556 vs tarte 0.517 = 0.04, et ordre s'inverse avec mauvais préfixe). => SHIPPER multilingual-e5-small comme le spec l'exige, pas nomic. Le code est agnostique au modèle (n_embd auto). ⚠ Bloqueur SELinux identique LLM/TTS : l'embedder linke libllama->libggml-hexagon qui ouvre /dev/fastrpc-cdsp au backend_init -> crash untrusted_app. DOIT tourner sur le build CPU-only (GGML_HEXAGON=OFF). Documenté dans le code + RAG_INTEGRATION.md. Doc : dist/RAG_INTEGRATION.md (format des 3 autres) + overview à jour (4 sous-systèmes).
This commit is contained in:
parent
f4c6e1290c
commit
5cc8c22fc9
|
|
@ -12,7 +12,8 @@ Pour intégrer dans l'app Kazeia, lis ces 3 docs (un par sous-système) :
|
|||
|---|---|---|---|
|
||||
| **STT** (Whisper-Small NPU) | [`STT_INTEGRATION.md`](STT_INTEGRATION.md) | `libkazeia_stt.so` 183 KB | `SttEngine.kt` |
|
||||
| **TTS** (Qwen3-TTS + clonage vocal) | [`TTS_INTEGRATION.md`](TTS_INTEGRATION.md) | `libkazeia_tts.so` 929 KB | `TtsEngine.kt` |
|
||||
| **LLM** (Qwen3.5 hybride / Qwen3 dense) | [`LLM_INTEGRATION.md`](LLM_INTEGRATION.md) | `libkazeia_engine.so` 60 KB | `EngineLlmEngine.kt` |
|
||||
| **LLM** (Qwen3.5 hybride / Qwen3 dense) | [`LLM_INTEGRATION.md`](LLM_INTEGRATION.md) | `libkazeia_engine.so` 64 KB | `EngineLlmEngine.kt` |
|
||||
| **RAG** (embeddings e5/bge, CPU) | [`RAG_INTEGRATION.md`](RAG_INTEGRATION.md) | `libkazeia_engine.so` (mêmes symboles) | `EmbedderEngine` |
|
||||
|
||||
Chaque doc a la même structure : libs à pousser, façade Kotlin, exemples, codes d'erreur, bench standalone, checklist d'intégration, pièges connus, perf détaillée.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
# Intégration RAG (embeddings) Kazeia-Engine — Guide développeur
|
||||
|
||||
**Date** : 09/06/2026
|
||||
**Cible** : exposer `texte → vecteur de phrase poolé` pour le RAG de Kazeia mobile, sans toucher au chemin Speaker/TTS.
|
||||
**Statut** : entrypoint JNI livré + validé (parité bit-identique vs `llama-embedding` du fork).
|
||||
|
||||
> ⚠️ **Même bloqueur SELinux que LLM/TTS** (cf `REBUILD_CPU_ONLY.md`). L'embedder linke `libllama.so`
|
||||
> qui dépend de `libggml-hexagon.so` ouvrant `/dev/fastrpc-cdsp` au `llama_backend_init()` → SIGABRT
|
||||
> dans un APK `untrusted_app`. `n_gpu_layers=0` ne suffit PAS (c'est l'énumération des devices qui
|
||||
> plante). **L'embedder DOIT rouler sur le `libllama` CPU-only** (`GGML_HEXAGON=OFF`), comme LLM/TTS in-app.
|
||||
> Les tests ci-dessous passent en `adb shell` (uid 2000, SELinux OK) mais c'est un FAUX positif pour l'app.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```kotlin
|
||||
// EngineJni (dist/jni/EngineLlmEngine.kt)
|
||||
external fun loadEmbedder(ggufPath: String, nThreads: Int, pooling: Int): Long
|
||||
external fun embedText(handle: Long, text: String): FloatArray? // float[n_embd] L2-normalisé, ou null
|
||||
external fun freeEmbedder(handle: Long)
|
||||
|
||||
// Wrapper fourni
|
||||
val emb = EmbedderEngine("$dir/multilingual-e5-small.gguf", nThreads = 4, pooling = -1)
|
||||
val v = emb.embed("query: j'ai du mal à dormir") // FloatArray(384), norme 1.0
|
||||
emb.release()
|
||||
```
|
||||
|
||||
`pooling` : **-1 = défaut du modèle (recommandé)** ; 1 = MEAN (e5) ; 2 = CLS (bge).
|
||||
|
||||
Handle **séparé** du LLM Speaker/TTS. CPU pur, aucun HTP. Déterministe (pas d'échantillonnage).
|
||||
|
||||
---
|
||||
|
||||
## Modèle d'embedding — décision critique (point #1 du spec)
|
||||
|
||||
| | Statut |
|
||||
|---|---|
|
||||
| **Recommandé** | `multilingual-e5-small` (384-dim, FR-capable, pooling MEAN) — **à sourcer/convertir + versionner** |
|
||||
| Testé localement | `nomic-embed-text-v1.5` (768-dim) — **anglo-centré, marge FR fine** |
|
||||
|
||||
**Mesure FR (nomic, sur tablette)** : `cos("mal à dormir","insomnie")=0.556` vs `cos("mal à dormir","recette de tarte")=0.517` — ordre correct mais marge de **0.04 seulement**. Avec un mauvais préfixe l'ordre s'inverse (0.477 vs 0.611). **Conclusion : nomic n'est pas assez robuste en FR pour un RAG thérapeutique. Shipper `multilingual-e5-small` comme le spec l'exige.**
|
||||
|
||||
Impératifs (contrat de distribution) :
|
||||
- **Le MÊME GGUF à l'ingestion ET à la requête** (vecteurs incompatibles sinon). Versionner le n° de modèle.
|
||||
- **Préfixes e5 (`query:` / `passage:`) = côté appelant**, jamais dans le moteur.
|
||||
|
||||
---
|
||||
|
||||
## Validation effectuée (tests d'acceptation du spec)
|
||||
|
||||
Sur tablette SM8750, `adb shell`, nomic-embed-text-v1.5 Q4_K_M, CPU :
|
||||
|
||||
| Test | Résultat |
|
||||
|---|---|
|
||||
| Arch BERT/nomic-bert charge end-to-end | ✅ vecteur produit |
|
||||
| `embedText` renvoie `float[n_embd]` | ✅ 768 (nomic) / 384 attendu (e5-small) |
|
||||
| **Déterminisme** (2 runs, même texte) | ✅ **max\|diff\| = 0.0 bit-identique** |
|
||||
| **Norme L2 ≈ 1.0** | ✅ 1.0000 |
|
||||
| Cohérence sémantique (proche > loin) | ✅ avec bons préfixes (model-dépendant, cf ci-dessus) |
|
||||
| **Parité référence** vs `llama-embedding` | ✅ **max\|diff\| = 5e-7, cos = 1.000000** |
|
||||
|
||||
La parité prouve que le chemin C++ (`tokenize add_special` + batch manuel `logits=1` + `llama_decode` + `llama_get_embeddings_seq` + L2) est **bit-identique** à l'outil de référence du fork. Confirmé : BERT encoder-only est routé via `llama_decode` (pas `llama_encode`) dans ce fork — `examples/embedding/embedding.cpp` ne bascule sur erreur que pour les modèles encoder **ET** decoder (T5).
|
||||
|
||||
---
|
||||
|
||||
## Contraintes techniques (impératifs)
|
||||
|
||||
1. **`n_ctx = n_batch = n_ubatch = 512`** : le pooling MEAN/CLS exige toute la séquence dans un seul ubatch. Cap à 512 tokens (chunker plus court en amont).
|
||||
2. **CPU only** (`n_gpu_layers=0`) + **build CPU-only** (cf bloqueur SELinux).
|
||||
3. **Pas de réutilisation** de `prefillEmbeds`/`decodeEmbed` (I/O float du Talker TTS, pas un embedding de phrase poolé).
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
Rebuild `libkazeia_engine.so` (les 3 symboles `loadEmbedder`/`embedText`/`freeEmbedder` y sont exportés).
|
||||
**Aucune nouvelle dépendance** (tout est dans libllama). Pour l'in-app : build CPU-only (`REBUILD_CPU_ONLY.md`).
|
||||
Livrer la `.so` + un GGUF d'embedding **multilingual-e5-small** versionné.
|
||||
|
||||
---
|
||||
|
||||
## Reste côté app (hors moteur, pour info)
|
||||
|
||||
Schéma SQLite (`chunk_text`, `source`, `embedding BLOB`) dans SQLCipher → ingestion admin (chunk
|
||||
sémantique + `embedText`) → runtime : matrice en RAM → brute-force cosinus NEON top-k 3-5 + seuil →
|
||||
bloc « CONTEXTE » budgété en tokens dans le system prompt. Pas de vector-DB, pas d'ANN, pas d'ONNX.
|
||||
Le RAG ne porte jamais la gestion de crise.
|
||||
|
|
@ -24,9 +24,32 @@ class EngineJni {
|
|||
external fun prefillEmbeds(h: Long, embdFlat: FloatArray, t: Int, outHidden: FloatArray): Int
|
||||
external fun decodeEmbed(h: Long, embd: FloatArray, outLogits: FloatArray, outHidden: FloatArray): Int
|
||||
|
||||
// -- Embeddings (RAG) : modèle dédié BERT-like (e5/bge), handle SÉPARÉ du LLM/TTS, CPU pur.
|
||||
// pooling: -1 = défaut du modèle (recommandé) ; 1 = MEAN (e5) ; 2 = CLS (bge).
|
||||
// embedText renvoie un float[n_embd] L2-normalisé (cosinus = dot product), ou null si échec.
|
||||
// Préfixes e5 ("query:" / "passage:") = côté appelant, PAS ici.
|
||||
external fun loadEmbedder(ggufPath: String, nThreads: Int, pooling: Int): Long
|
||||
external fun embedText(handle: Long, text: String): FloatArray?
|
||||
external fun freeEmbedder(handle: Long)
|
||||
|
||||
companion object { init { System.loadLibrary("kazeia_engine") } }
|
||||
}
|
||||
|
||||
// Wrapper RAG-friendly. Un seul embedder par process (le modèle est petit ~120 MB).
|
||||
// Le MÊME GGUF doit servir à l'ingestion ET à la requête (vecteurs incompatibles sinon) :
|
||||
// versionne le modèle dans le contrat de distribution.
|
||||
class EmbedderEngine(model: String, nThreads: Int = 4, pooling: Int = -1) {
|
||||
private val jni = EngineJni()
|
||||
private val h = jni.loadEmbedder(model, nThreads, pooling)
|
||||
init { require(h != 0L) { "Kazeia-Engine: échec du chargement de l'embedder ($model)" } }
|
||||
|
||||
/** Texte -> vecteur L2-normalisé (float[n_embd]). Déterministe. */
|
||||
fun embed(text: String): FloatArray =
|
||||
jni.embedText(h, text) ?: error("embedText a échoué pour: \"${text.take(40)}\"")
|
||||
|
||||
fun release() = jni.freeEmbedder(h)
|
||||
}
|
||||
|
||||
class EngineLlmEngine(model: String, ctx: Int = 4096, nThreads: Int = 6) {
|
||||
private val jni = EngineJni()
|
||||
private val h = jni.load(model, ctx, nThreads)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include "llama.h"
|
||||
#include "ggml-backend.h"
|
||||
#include "gguf.h"
|
||||
|
|
@ -326,3 +327,111 @@ Java_com_kazeia_llm_EngineJni_decodeEmbed(JNIEnv* e, jobject, jlong h,
|
|||
e->SetFloatArrayRegion(out_hidden, 0, n_embd, hidden.data());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EMBEDDINGS (RAG) : modèle dédié BERT-like (e5/bge/gemma-embedding), CPU pur,
|
||||
// pooling de séquence. Handle SÉPARÉ du KEngine Speaker/TTS — aucun HTP/HMX.
|
||||
//
|
||||
// ⚠ Bloqueur SELinux connu (cf REBUILD_CPU_ONLY.md / project_engine_selinux_fastrpc_blocker) :
|
||||
// libllama linke libggml-hexagon qui ouvre /dev/fastrpc-cdsp à l'init backend ->
|
||||
// SIGABRT dans un APK untrusted_app. L'embedder DOIT rouler sur le libllama
|
||||
// CPU-only (GGML_HEXAGON=OFF), comme LLM/TTS in-app. n_gpu_layers=0 ne suffit pas
|
||||
// à éviter le crash : c'est l'ÉNUMÉRATION des devices au backend_init qui plante.
|
||||
//
|
||||
// Chemin reproduit à l'identique de examples/embedding/embedding.cpp (source de
|
||||
// vérité du fork) : llama_decode (PAS llama_encode — BERT encoder-only y est routé
|
||||
// via decode), pooling != NONE -> llama_get_embeddings_seq, normalisation L2.
|
||||
// ============================================================================
|
||||
struct KEmbedder {
|
||||
llama_model* m;
|
||||
llama_context* c;
|
||||
const llama_vocab* v;
|
||||
int n_embd;
|
||||
};
|
||||
|
||||
// pooling: -1 = défaut du modèle (recommandé) ; 1 = MEAN (e5) ; 2 = CLS (bge)
|
||||
extern "C" JNIEXPORT jlong JNICALL
|
||||
Java_com_kazeia_llm_EngineJni_loadEmbedder(JNIEnv* e, jobject, jstring path, jint nThreads, jint pooling) {
|
||||
const char* pc = e->GetStringUTFChars(path, 0);
|
||||
std::string p(pc); e->ReleaseStringUTFChars(path, pc);
|
||||
|
||||
llama_backend_init(); // idempotent
|
||||
auto mp = llama_model_default_params(); mp.n_gpu_layers = 0; // CPU only
|
||||
llama_model* m = llama_model_load_from_file(p.c_str(), mp);
|
||||
if (!m) { fprintf(stderr, "kazeia-engine: EMBEDDER load FAIL (%s)\n", p.c_str()); return 0; }
|
||||
|
||||
// Encoder-decoder (T5) non supporté pour l'embedding (cf embedding.cpp).
|
||||
// BERT pur (encoder-only) est routé via llama_decode dans ce fork -> OK.
|
||||
if (llama_model_has_encoder(m) && llama_model_has_decoder(m)) {
|
||||
fprintf(stderr, "kazeia-engine: EMBEDDER refuse un modèle encoder-decoder (%s)\n", p.c_str());
|
||||
llama_model_free(m); return 0;
|
||||
}
|
||||
|
||||
auto cp = llama_context_default_params();
|
||||
cp.n_threads = (nThreads > 0) ? nThreads : 4;
|
||||
cp.n_threads_batch = cp.n_threads;
|
||||
cp.embeddings = true; // <-- clé
|
||||
cp.pooling_type = (pooling == 1) ? LLAMA_POOLING_TYPE_MEAN
|
||||
: (pooling == 2) ? LLAMA_POOLING_TYPE_CLS
|
||||
: LLAMA_POOLING_TYPE_UNSPECIFIED; // défaut = métadonnées du modèle
|
||||
// Pooling MEAN/CLS exige que TOUTE la séquence tienne dans un seul ubatch :
|
||||
cp.n_ctx = 512; cp.n_batch = 512; cp.n_ubatch = 512; // chunks <= 512 tokens
|
||||
// (ne PAS réutiliser make_ctx : pas de flash_attn ni de KV f16 ici)
|
||||
llama_context* c = llama_init_from_model(m, cp);
|
||||
if (!c) { fprintf(stderr, "kazeia-engine: EMBEDDER ctx FAIL (%s)\n", p.c_str()); llama_model_free(m); return 0; }
|
||||
|
||||
auto* k = new KEmbedder{ m, c, llama_model_get_vocab(m), llama_model_n_embd(m) };
|
||||
fprintf(stderr, "kazeia-engine: EMBEDDER chargé (%s, n_embd=%d, pooling=%d, threads=%d)\n",
|
||||
p.c_str(), k->n_embd, pooling, cp.n_threads);
|
||||
return (jlong) k;
|
||||
}
|
||||
|
||||
// texte -> float[n_embd] L2-normalisé. nullptr en cas d'échec.
|
||||
extern "C" JNIEXPORT jfloatArray JNICALL
|
||||
Java_com_kazeia_llm_EngineJni_embedText(JNIEnv* e, jobject, jlong h, jstring text) {
|
||||
auto* k = (KEmbedder*) h;
|
||||
if (!k) return nullptr;
|
||||
const char* tc = e->GetStringUTFChars(text, 0);
|
||||
std::string s(tc ? tc : ""); e->ReleaseStringUTFChars(text, tc);
|
||||
|
||||
// 1) tokenize (add_special=true -> CLS/BOS + SEP/EOS selon le modèle)
|
||||
int n = -llama_tokenize(k->v, s.c_str(), (int)s.size(), nullptr, 0, true, true);
|
||||
if (n <= 0) return nullptr;
|
||||
if (n > 512) n = 512; // garde-fou ubatch (chunks plus courts en amont)
|
||||
std::vector<llama_token> toks(n);
|
||||
if (llama_tokenize(k->v, s.c_str(), (int)s.size(), toks.data(), n, true, true) < 0) return nullptr;
|
||||
|
||||
// 2) batch : tous les tokens, seq 0, sortie activée sur chaque token (requis pour le pooling)
|
||||
llama_memory_clear(llama_get_memory(k->c), true);
|
||||
llama_batch b = llama_batch_init(n, 0, 1);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
b.token[i] = toks[i]; b.pos[i] = i;
|
||||
b.n_seq_id[i] = 1; b.seq_id[i][0] = 0; b.logits[i] = 1;
|
||||
}
|
||||
b.n_tokens = n;
|
||||
if (llama_decode(k->c, b) != 0) { llama_batch_free(b); return nullptr; }
|
||||
|
||||
// 3) embedding poolé de la séquence 0
|
||||
const float* emb = llama_get_embeddings_seq(k->c, 0);
|
||||
if (!emb) { llama_batch_free(b); return nullptr; }
|
||||
|
||||
// 4) normalisation L2 (cosinus = produit scalaire côté Android)
|
||||
std::vector<float> out(k->n_embd);
|
||||
double norm = 0.0; for (int i = 0; i < k->n_embd; ++i) norm += (double)emb[i] * emb[i];
|
||||
norm = norm > 0 ? 1.0 / std::sqrt(norm) : 0.0;
|
||||
for (int i = 0; i < k->n_embd; ++i) out[i] = (float)(emb[i] * norm);
|
||||
llama_batch_free(b);
|
||||
|
||||
jfloatArray arr = e->NewFloatArray(k->n_embd);
|
||||
e->SetFloatArrayRegion(arr, 0, k->n_embd, out.data());
|
||||
return arr;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_kazeia_llm_EngineJni_freeEmbedder(JNIEnv*, jobject, jlong h) {
|
||||
auto* k = (KEmbedder*) h;
|
||||
if (!k) return;
|
||||
if (k->c) llama_free(k->c);
|
||||
if (k->m) llama_model_free(k->m);
|
||||
delete k;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue