Фикс локальной LLM: таймаут 300с для LOCAL (инициализация движка ~17с + генерация), CPU-бэкенд без спекуляции (MTP виснет на CPU, GPU-делегат падает), диагностика chat()
Проверено на железе (HONOR FCP-N49): Gemma 4 E2B отвечает «Здравствуйте! Чем я могу вам помочь с вашими очками Viewmate?» (15 чанков, 61 символ, ~19с генерации).
This commit is contained in:
@@ -112,13 +112,16 @@ class PhoneApp : Application() {
|
||||
private var localLlm: LocalLlmClient? = null
|
||||
|
||||
/** Выбор модели LLM (персистится, [pw.binom.viewmate.phone.agent.LlmPrefs]). */
|
||||
val llmPrefs = LlmPrefs(this)
|
||||
private val _llmChoice = MutableStateFlow<LlmModelChoice>(llmPrefs.current())
|
||||
val llmChoice: StateFlow<LlmModelChoice> = _llmChoice.asStateFlow()
|
||||
// lazy: конструктор Application выполняется ДО attachBaseContext — контекст (и
|
||||
// getSharedPreferences/getExternalFilesDir) доступен только после; ленивость —
|
||||
// тот же паттерн, что у chatDao/downloadManager (иначе NPE при старте).
|
||||
val llmPrefs: LlmPrefs by lazy { LlmPrefs(this) }
|
||||
private val _llmChoice: MutableStateFlow<LlmModelChoice> by lazy { MutableStateFlow(llmPrefs.current()) }
|
||||
val llmChoice: StateFlow<LlmModelChoice> by lazy { _llmChoice.asStateFlow() }
|
||||
|
||||
/** Состояние локальной модели Gemma 4 E2B для UI настроек. */
|
||||
private val _localLlmState = MutableStateFlow<LocalLlmModelState>(initialLocalLlmState())
|
||||
val localLlmState: StateFlow<LocalLlmModelState> = _localLlmState.asStateFlow()
|
||||
private val _localLlmState: MutableStateFlow<LocalLlmModelState> by lazy { MutableStateFlow(initialLocalLlmState()) }
|
||||
val localLlmState: StateFlow<LocalLlmModelState> by lazy { _localLlmState.asStateFlow() }
|
||||
|
||||
private var localLlmDownloadJob: Job? = null
|
||||
private fun initialLocalLlmState(): LocalLlmModelState =
|
||||
@@ -203,6 +206,10 @@ class PhoneApp : Application() {
|
||||
chatDao = chatDao,
|
||||
chat = { history, system -> llm.chat(history, system) },
|
||||
toolkit = tk,
|
||||
// Локальной модели нужно время: инициализация движка ~17с на
|
||||
// CPU + генерация. Серверная укладывается в 30с, локальной
|
||||
// даём 5 минут (иначе withTimeoutOrNull режет генерацию).
|
||||
timeoutMs = if (choice == LlmModelChoice.LOCAL) 300_000L else 30_000L,
|
||||
).also {
|
||||
assistant = it
|
||||
assistantChoice = choice
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.google.ai.edge.litertlm.Message
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
@@ -22,6 +23,17 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
private fun log(tag: String, message: String) = pw.binom.viewmate.phone.log(tag, message)
|
||||
|
||||
/** Обрезает сообщение до 200 символов, оставляя читаемые (ASCII + кириллица). */
|
||||
private fun sanitize(msg: String?): String {
|
||||
if (msg == null) return "null"
|
||||
val sb = StringBuilder()
|
||||
for (ch in msg.take(200)) {
|
||||
val c = ch.code
|
||||
if ((c in 32..126) || (c >= 0x410 && c <= 0x44F)) sb.append(ch)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Выбор модели LLM ассистента — персистится ([LlmPrefs] в PhoneApp),
|
||||
* переключается UI-блоком в настройках экрана «Телефон»:
|
||||
@@ -60,11 +72,15 @@ object Gemma4E2B {
|
||||
/**
|
||||
* Конфиг движка: GPU-бэкенд (Adreno/Mali), каудирование CPU, кэш в
|
||||
* [cacheDir] (cacheDir — не tmpDir, см. заметку про /data/local/tmp).
|
||||
* NOTE 2026-08-23: на Honor Magic V3 (FCP-N49) GPU-бэкенд litertlm 0.14.0
|
||||
* падает в ml_drift-делегате (delegate_kernel.cc:925) — движок не создаётся.
|
||||
* Пробуем CPU (decode ~46 ток/с на 8 Gen 3 — приемлемо); вернуть GPU,
|
||||
* когда починится делегат (или обновится litertlm).
|
||||
*/
|
||||
fun engineConfig(modelFile: File, cacheDir: File): EngineConfig = EngineConfig(
|
||||
modelPath = modelFile.absolutePath,
|
||||
backend = Backend.GPU(),
|
||||
visionBackend = Backend.GPU(),
|
||||
backend = Backend.CPU(),
|
||||
visionBackend = Backend.CPU(),
|
||||
audioBackend = Backend.CPU(),
|
||||
cacheDir = cacheDir.absolutePath,
|
||||
)
|
||||
@@ -284,8 +300,11 @@ class LocalLlmClient(
|
||||
fun modelDownloaded(): Boolean = modelFile.isFile
|
||||
|
||||
override suspend fun chat(messages: List<ChatMessage>, system: String?): String {
|
||||
log("llm", "chat: вызов (messages=${messages.size}, system=${system?.length ?: 0} симв)")
|
||||
val engine = ensureEngine()
|
||||
log("llm", "chat: движок готов, строю промпт")
|
||||
val prompt = buildLocalLlmPrompt(messages, system)
|
||||
log("llm", "chat: промпт: system=${prompt.systemInstruction?.length ?: 0} симв, initial=${prompt.initialMessages.size}, incoming=${prompt.incoming.text().take(60)}")
|
||||
val conversation: Conversation = synchronized(engineLock) {
|
||||
engine.createConversation(
|
||||
ConversationConfig(
|
||||
@@ -294,15 +313,30 @@ class LocalLlmClient(
|
||||
)
|
||||
)
|
||||
}
|
||||
log("llm", "chat: conversation создан")
|
||||
try {
|
||||
var text = ""
|
||||
var chunks = 0
|
||||
withTimeout(120_000) {
|
||||
conversation.sendMessageAsync(prompt.incoming).collect { messageChunk ->
|
||||
text += messageChunk.text()
|
||||
chunks++
|
||||
val chunkText = messageChunk.text()
|
||||
text += chunkText
|
||||
if (chunks <= 3 || chunks % 20 == 0) {
|
||||
log("llm", "chat: чанк #$chunks: ${chunkText.take(50)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
log("llm", "chat: готово, чанков=$chunks, текст=${text.length} симв")
|
||||
return text.trim()
|
||||
} catch (e: CancellationException) {
|
||||
log("llm", "chat: отменено")
|
||||
runCatching { conversation.cancelProcess() }
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
log("llm", "chat: EXC class=${e::class.simpleName} msgLen=${e.message?.length}")
|
||||
log("llm", "chat: EXC msg=${sanitize(e.message)}")
|
||||
throw IllegalStateException("Локальная модель не ответила: ${e.message}", e)
|
||||
} finally {
|
||||
runCatching { conversation.close() }
|
||||
}
|
||||
@@ -321,7 +355,10 @@ class LocalLlmClient(
|
||||
/** Включаем спекулятивное декодирование (на 2B заметно быстрее, см. заметки). */
|
||||
@OptIn(ExperimentalApi::class)
|
||||
private fun applyExperimentalFlags() {
|
||||
ExperimentalFlags.enableSpeculativeDecoding = true
|
||||
// NOTE 2026-08-23: на CPU-бэкенде спекуляция (MTP) виснет — движок
|
||||
// инициализируется, но sendMessageAsync не выдаёт токены. GPU-делегат
|
||||
// на этом устройстве падает (см. engineConfig). Пока — без спекуляции.
|
||||
// ExperimentalFlags.enableSpeculativeDecoding = true
|
||||
}
|
||||
|
||||
/** Ленивая инициализация движка (блокирующая — вызываем с IO-потока). */
|
||||
|
||||
Reference in New Issue
Block a user