# Spec — API d'échantillonnage natif (kazeia-engine) pour les presets Kazeia **Date** : 2026-06-18 **Demandeur** : app Kazeia (refonte presets admin) **Cible** : `kazeia-engine` (lib `libkazeia_engine`, JNI `EngineJni`) ## Contexte L'app admin permet désormais de régler, par modèle (Speaker / Thinker) et via des **presets** nommés, les paramètres d'échantillonnage : `temperature`, `top_p`, `top_k`, `repeat_penalty`, `presence_penalty`, `frequency_penalty`, `max_tokens`. Côté app, ces valeurs sont **persistées et transmises** jusqu'au pont JNI (`UnifiedLlmAdapter` → `EngineLlmEngine`/`LlmSession`). **MAIS le JNI actuel n'expose que `maxTok`** : ```kotlin external fun generate(h: Long, sys: String, usr: String, maxTok: Int): String external fun sessionAsk(h: Long, usr: String, maxTok: Int, cb: TokenCallback) external fun generateStream(h: Long, sys: String, usr: String, maxTok: Int, cb: TokenCallback) ``` Donc aujourd'hui **seul `max_tokens` agit** ; température/top-p/k/pénalités sont inertes (l'app l'indique honnêtement dans l'UI : « effectif après MAJ moteur »). ## Demande — exposer le sampling au JNI Ajouter une **variante échantillonnée** des entrées de génération (sans casser les signatures existantes), qui prend une struct de sampling et la câble sur la chaîne de samplers llama.cpp. Le GGUF Qwen3.5-4B (Speaker par défaut) est la cible prioritaire. ### Signatures proposées ```kotlin // Struct portée côté natif (ou 7 args primitifs si plus simple en JNI) : // temperature: Float, topP: Float, topK: Int, // repeatPenalty: Float, presencePenalty: Float, frequencyPenalty: Float, maxTokens: Int external fun generateSampled( h: Long, sys: String, usr: String, maxTokens: Int, temperature: Float, topP: Float, topK: Int, repeatPenalty: Float, presencePenalty: Float, frequencyPenalty: Float, cb: TokenCallback ) external fun sessionAskSampled( h: Long, usr: String, maxTokens: Int, temperature: Float, topP: Float, topK: Int, repeatPenalty: Float, presencePenalty: Float, frequencyPenalty: Float, cb: TokenCallback ) ``` ### Mapping llama.cpp (sampler chain) Construire la chaîne par requête (ou réutiliser un `llama_sampler` reconfiguré) : ```c auto * chain = llama_sampler_chain_init(llama_sampler_chain_default_params()); llama_sampler_chain_add(chain, llama_sampler_init_top_k(top_k)); llama_sampler_chain_add(chain, llama_sampler_init_top_p(top_p, 1)); llama_sampler_chain_add(chain, llama_sampler_init_penalties( /*penalty_last_n*/ 64, repeat_penalty, frequency_penalty, presence_penalty)); llama_sampler_chain_add(chain, llama_sampler_init_temp(temperature)); llama_sampler_chain_add(chain, llama_sampler_init_dist(/*seed*/ LLAMA_DEFAULT_SEED)); ``` - `temperature <= 0` ⇒ greedy (`llama_sampler_init_greedy`), ignorer les autres. - Conserver le **thinking-off** et le template ChatML actuels inchangés. - `presence/frequency_penalty = 0` ⇒ no-op (comportement neutre). ### Comportement attendu - Valeurs de référence (preset « Kaz chaleureux ») : temp 0.4, top_p 0.85, top_k 40, repeat 1.05, presence 0.2, frequency 0.2, max 256 — doit produire des sorties visiblement moins répétitives / plus chaleureuses que le greedy actuel. - Déterminisme : exposer un `seed` optionnel plus tard si besoin (pas requis v1). ### `.pte` (NPU) — hors scope v1 Le runner ExecuTorch `.pte` décode **greedy**. Le sampling y est une évolution distincte (échantillonnage post-logits côté runner). Pour l'instant les presets n'affectent que le chemin GGUF ; documenter la limite suffit. ## Côté app — déjà prêt - `SamplingParams` (com.kazeia.core) porte les 7 knobs. - `UnifiedLlmAdapter.generateWithSystem` reçoit les params ; il suffira de router vers `*Sampled` au lieu de `ask(max)` / `generateStream(max)` quand le JNI existe. - `ConfigStore.ModelConfig` + presets persistent les valeurs ; l'admin les édite. Quand le JNI échantillonné est livré : ~10 lignes à changer côté `UnifiedLlmAdapter` (brancher `sessionAskSampled` / `generateSampled`) et retirer la mention « effectif après MAJ moteur » de l'UI admin.