138 lines
5.6 KiB
Kotlin
138 lines
5.6 KiB
Kotlin
package com.kazeia.config
|
|
|
|
import java.io.File
|
|
|
|
/**
|
|
* Catalogue figé des modèles LLM qu'on sait charger via ExecuTorch QNN HTP.
|
|
* Filtrer par `availableOnDisk()` pour ne proposer dans l'UI admin que ceux
|
|
* réellement présents sur la tablette.
|
|
*
|
|
* L'admin app lit cette liste via le ContentProvider URI `/models` ;
|
|
* KazeiaService la consulte pour résoudre un `model_id` → (pte, tokenizer).
|
|
*/
|
|
object ModelRegistry {
|
|
|
|
// Racine .pte LLM — résolue par KazeiaPaths (legacy /data/local/tmp/kazeia-et
|
|
// sur tablette dev, stockage externe app sur tablette de production).
|
|
private val ET_DIR: String get() = com.kazeia.KazeiaApplication.LLM_DIR
|
|
|
|
// Racine GGUF (moteur kazeia-engine "lib"). Les GGUF (q35-lmq4, qwen2.5-7b…)
|
|
// vivent sous MODELS_DIR, pas LLM_DIR.
|
|
private val GGUF_DIR: String get() = com.kazeia.KazeiaApplication.MODELS_DIR
|
|
|
|
enum class Role { SPEAKER, THINKER, BOTH }
|
|
|
|
// PTE = runner ExecuTorch QNN HTP ; GGUF = moteur kazeia-engine "lib" (CPU).
|
|
enum class Backend { PTE, GGUF }
|
|
|
|
// Template ChatML du modèle. QWEN35_THINKOFF injecte le bloc <think></think>
|
|
// (thinking-off déterministe Qwen3.5). PLAIN_CHATML = ChatML standard sans
|
|
// <think> (Qwen2.5 et autres modèles dense sans raisonnement).
|
|
enum class ChatTemplate { QWEN35_THINKOFF, PLAIN_CHATML }
|
|
|
|
data class ModelInfo(
|
|
val id: String,
|
|
val displayName: String,
|
|
val pteFile: String,
|
|
val tokenizerFile: String,
|
|
val role: Role,
|
|
val sizeMb: Int,
|
|
val maxSeqLen: Int,
|
|
val notes: String = "",
|
|
val backend: Backend = Backend.PTE,
|
|
// Nom du fichier GGUF (sous GGUF_DIR) quand backend = GGUF. Le tokenizer
|
|
// est embarqué dans le GGUF, pas de fichier séparé.
|
|
val ggufFile: String = "",
|
|
val chatTemplate: ChatTemplate = ChatTemplate.QWEN35_THINKOFF
|
|
) {
|
|
fun ptePath() = "$ET_DIR/$pteFile"
|
|
fun tokenizerPath() = "$ET_DIR/$tokenizerFile"
|
|
fun ggufPath() = "$GGUF_DIR/$ggufFile"
|
|
fun fileExists(): Boolean = when (backend) {
|
|
Backend.PTE -> File(ptePath()).exists()
|
|
Backend.GGUF -> File(ggufPath()).exists()
|
|
}
|
|
}
|
|
|
|
// Catalogue refonte 2026-06-17 (cf dist/LLM_INTEGRATION.md §2). Chargement unifié
|
|
// via UnifiedLlmAdapter (magic-byte) : GGUF→CPU i8mm, .pte→NPU V79 (libkazeia_pte).
|
|
// .pte en SOUS-DOSSIER <id>/hybrid_llama_qnn.pte (+ tokenizer.json) : le nom du
|
|
// dossier porte l'auto-détection decoder_model_version (qwen3/qwen2_5) côté natif.
|
|
val ALL: List<ModelInfo> = listOf(
|
|
ModelInfo(
|
|
id = "qwen3.5-4b",
|
|
displayName = "Qwen3.5-4B (GGUF) ★",
|
|
pteFile = "",
|
|
tokenizerFile = "",
|
|
role = Role.SPEAKER,
|
|
sizeMb = 2380,
|
|
maxSeqLen = 4096,
|
|
notes = "Speaker PAR DÉFAUT — hybride GatedDeltaNet (non exportable .pte), GGUF CPU i8mm. Thinking-off.",
|
|
backend = Backend.GGUF,
|
|
ggufFile = "q35-lmq4.gguf",
|
|
chatTemplate = ChatTemplate.QWEN35_THINKOFF
|
|
),
|
|
ModelInfo(
|
|
id = "qwen3-4b",
|
|
displayName = "Qwen3-4B (.pte NPU)",
|
|
pteFile = "qwen3_4b_seq1024/hybrid_llama_qnn.pte",
|
|
tokenizerFile = "qwen3_4b_seq1024/tokenizer.json",
|
|
role = Role.SPEAKER,
|
|
sizeMb = 3100,
|
|
maxSeqLen = 1024,
|
|
notes = "Dense Qwen3-4B sur NPU. Prefill 313 tok/s, RAM ÷2, sans root. Decode 15.7.",
|
|
backend = Backend.PTE,
|
|
chatTemplate = ChatTemplate.QWEN35_THINKOFF
|
|
),
|
|
ModelInfo(
|
|
id = "qwen3-8b",
|
|
displayName = "Qwen3-8B (.pte NPU)",
|
|
pteFile = "qwen3_8b_seq512/hybrid_llama_qnn.pte",
|
|
tokenizerFile = "qwen3_8b_seq512/tokenizer.json",
|
|
role = Role.SPEAKER,
|
|
sizeMb = 5500,
|
|
maxSeqLen = 512,
|
|
notes = "Dense Qwen3-8B sur NPU. Decode 10.5, prefill 219, RAM ~3.1 G.",
|
|
backend = Backend.PTE,
|
|
chatTemplate = ChatTemplate.QWEN35_THINKOFF
|
|
),
|
|
ModelInfo(
|
|
id = "qwen2.5-7b",
|
|
displayName = "Qwen2.5-7B (.pte NPU)",
|
|
pteFile = "qwen2_5_7b_seq512/hybrid_llama_qnn.pte",
|
|
tokenizerFile = "qwen2_5_7b_seq512/tokenizer.json",
|
|
role = Role.SPEAKER,
|
|
sizeMb = 5000,
|
|
maxSeqLen = 512,
|
|
notes = "Dense Qwen2.5-7B sur NPU (decoder qwen2_5). Decode 8.0 (embeddings non-tied), RAM ~5.1 G.",
|
|
backend = Backend.PTE,
|
|
chatTemplate = ChatTemplate.PLAIN_CHATML
|
|
),
|
|
ModelInfo(
|
|
id = "guard4b",
|
|
displayName = "Qwen3Guard 4B (.pte NPU)",
|
|
pteFile = "guard4b/hybrid_llama_qnn.pte",
|
|
tokenizerFile = "guard4b/tokenizer.json",
|
|
role = Role.THINKER,
|
|
sizeMb = 3100,
|
|
maxSeqLen = 512,
|
|
notes = "Thinker cascade sur NPU. Decode 17.2, prefill 166. Guard-4B + Speaker tiennent en 16 G.",
|
|
backend = Backend.PTE,
|
|
chatTemplate = ChatTemplate.QWEN35_THINKOFF
|
|
)
|
|
)
|
|
|
|
fun availableOnDisk(): List<ModelInfo> = ALL.filter { it.fileExists() }
|
|
|
|
fun byId(id: String): ModelInfo? = ALL.firstOrNull { it.id == id }
|
|
|
|
// Défaut Speaker = Qwen3.5-4B GGUF (hybride, décision refonte 2026-06-17).
|
|
// Repli : tout autre Speaker présent sur disque.
|
|
fun defaultSpeaker(): ModelInfo = byId("qwen3.5-4b")
|
|
?: ALL.first { it.role == Role.SPEAKER && it.fileExists() }
|
|
|
|
// Défaut Thinker (cascade) = Guard-4B .pte NPU.
|
|
fun defaultThinker(): ModelInfo = byId("guard4b")
|
|
?: ALL.first { it.role == Role.THINKER && it.fileExists() }
|
|
}
|