File size: 23,230 Bytes
9863dbd
a32665e
9863dbd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a32665e
 
 
 
9863dbd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a32665e
9863dbd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc577b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9863dbd
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
---
license: other
pipeline_tag: text-generation
language:
- ru
- en
tags:
- android
- gguf
- llama-cpp
- on-device
- bonsai-27b
---

# Bonsai Local for Android

**Русский** · [English](README_EN.md) · [Структурированные результаты](BENCHMARKS.md)

Исследовательское Android-приложение для полностью локального запуска
[Prism ML Bonsai-27B GGUF](https://huggingface.co/prism-ml/Bonsai-27B-gguf)
через JNI и специальный форк `llama.cpp`. После помещения GGUF на устройство
диалог не использует сервер, облачный API или интернет.

![Bonsai Local с загруженной моделью](screenshots/02-model-loaded.png)

## Что уже работает

- полноценная загрузка `Bonsai-27B-Q1_0.gguf` размером 3 803 452 480 байт;
- inference внутри Android-процесса, без сервера на ПК;
- потоковый чат и thinking-mode;
- автообнаружение GGUF в приватной и app-specific external папках;
- ручной импорт любого совместимого GGUF через Android Storage Access Framework;
- встроенный benchmark prompt processing / token generation;
- APK для `arm64-v8a` реального телефона и `x86_64` эмулятора;
- Android adaptive icon и отдельные raster mipmaps;
- debug-вход `prompt_b64` для воспроизводимых автоматических тестов Unicode prompts.

## Проверенная конфигурация

| Компонент | Значение |
|---|---|
| ОС хоста | Windows 11 |
| CPU хоста | Intel Core Ultra 5 125H |
| Android Studio | 2025.2 |
| Android Gradle Plugin | 8.13.2 |
| Gradle | 8.14.3 |
| JDK сборки | JetBrains Runtime 21.0.9 |
| compileSdk / targetSdk | 36 / 36 |
| minSdk | 30 (Android 11) |
| Android NDK | 28.2.13676358 |
| CMake | 3.22.1 |
| ABI | `arm64-v8a`, `x86_64` |
| Тестовый AVD | Android 17 preview, x86_64, 8 ГБ RAM, 16 ГБ data |
| PrismML llama.cpp commit | `62061f91088281e65071cc38c5f69ee95c39f14e` |

## Результаты локальных тестов

Все числа ниже получены внутри Android-эмулятора, а не настольным
`llama-cli`.

| Проверка | Результат |
|---|---|
| Сборка debug APK | успешно |
| Установка и запуск Android | успешно |
| Распознавание модели | `qwen35 27B Q1_0`, 26.9B params, 3.53 GiB |
| Backend | CPU, динамически выбранный x86_64 variant |
| Prompt processing, pp64 | **2.89 tok/s** |
| Token generation, tg32 | **1.79 tok/s** |
| Арифметика `37 × 19` | корректный результат **703** |
| Thinking trace | работает |
| Русский + Kotlin, лимит 512 токенов | prompt понят, но весь лимит ушёл на thinking; чистый финальный код не выведен |
| Полностью offline после импорта | да |
| Крэши / FATAL EXCEPTION | не обнаружены |

Скриншоты:

- [`02-model-loaded.png`](screenshots/02-model-loaded.png) — модель загружена;
- [`03-benchmark.png`](screenshots/03-benchmark.png) — benchmark;
- [`04-reasoning.png`](screenshots/04-reasoning.png) — реальная генерация и ответ 703;
- [`05-russian-kotlin.png`](screenshots/05-russian-kotlin.png) — негативный capability-тест:
  модель поняла русский Kotlin prompt, но исчерпала лимит в thinking-mode.

Скорость эмулятора нельзя переносить на физический телефон: виртуализированный
CPU, thermal policy и доступные SIMD-инструкции отличаются. В карточке модели
Prism ML публикует существенно более высокие результаты для нативного MLX на
современном iPhone; этот проект использует Android/llama.cpp CPU backend.

## Архитектура

```mermaid
flowchart TD
    UI["Android UI · MainActivity"] --> API["InferenceEngine Kotlin API"]
    API --> DISP["Single-thread coroutine dispatcher"]
    DISP --> JNI["JNI · libai-chat.so"]
    JNI --> COMMON["llama-common · chat template · sampler"]
    JNI --> LLAMA["PrismML llama.cpp"]
    LLAMA --> GGML["GGML CPU backend loader"]
    GGML --> ABI{"Device ABI / CPU features"}
    ABI --> ARM["ARM variants · NEON / DOTPROD / I8MM / SVE / SME"]
    ABI --> X86["x86_64 variants · SSE4 / AVX2 / AVX512 / AMX"]
    GGUF["Bonsai-27B-Q1_0.gguf · 3.80 GB"] --> LLAMA
```

### Модули

- `app/` — UI, импорт модели, чат, benchmark и тестовый Base64 intent.
- `lib/` — Android-friendly Kotlin API и JNI wrapper.
- `third_party/llama.cpp/` — форк PrismML с Q1_0 и hybrid-attention kernels;
  находится рядом с проектом, на уровень выше `BonsaiAndroid`.
- `models/` — локальная копия GGUF для исследования; модель не упаковывается
  в APK.
- `screenshots/` — фактические результаты запуска в эмуляторе.

### Поток загрузки

1. `MainActivity` ждёт состояния `InferenceEngine.State.Initialized`.
2. Ищет `.gguf` в `files/models` и `getExternalFilesDir()/models`.
3. Передаёт абсолютный путь в `InferenceEngine.loadModel()`.
4. Kotlin сериализует все вызовы через один `Dispatchers.IO` dispatcher.
5. JNI вызывает `llama_model_load_from_file()`, затем создаёт контекст 8192
   токенов, batch 512 и sampler.
6. GGML выбирает подходящую CPU-библиотеку из `nativeLibraryDir`.
7. Chat template формирует сообщения Qwen, а JNI возвращает token pieces как
   Kotlin `Flow<String>`.

Критический фрагмент JNI:

```cpp
llama_model_params model_params = llama_model_default_params();
g_model = llama_model_load_from_file(model_path, model_params);

llama_context_params ctx_params = llama_context_default_params();
ctx_params.n_ctx = 8192;
ctx_params.n_batch = 512;
ctx_params.n_threads = n_threads;
g_context = llama_init_from_model(g_model, ctx_params);
```

Сериализация native calls важна: глобальные `llama_model`, `llama_context`,
`llama_batch` и sampler не должны одновременно изменяться UI и benchmark
корутинами.

```kotlin
private val llamaDispatcher = Dispatchers.IO.limitedParallelism(1)

override suspend fun loadModel(pathToModel: String) =
    withContext(llamaDispatcher) {
        load(pathToModel)
        prepare()
    }
```

## Что подтверждено в native runtime

При фактической загрузке полного GGUF runtime сообщил:

- 64 transformer blocks;
- Q1_0 binary tensors;
- recurrent state около 149.62 MiB;
- CPU compute buffer около 523.02 MiB;
- Flash Attention включён автоматически;
- fused Gated Delta Net работает в autoregressive и chunked режимах;
- graph содержит около 3703 nodes и один split.

Bonsai-27B **не MoE-модель**. У неё нет набора экспертов и router, поэтому
проверка «всех экспертов» неприменима. Это dense 27B hybrid-attention model:
примерно 75% слоёв используют linear/recurrent attention и 25% — full
attention.

Текущий APK текстовый. Дополнительные компоненты из репозитория модели не
подключены:

- `Bonsai-27B-mmproj-Q8_0.gguf` — vision tower;
- `Bonsai-27B-dspark-Q4_1.gguf` — speculative decoding drafter.

## Требования к устройству

- Android 11 или новее;
- 64-bit ARM или x86_64;
- минимум 5 ГБ свободного места только под GGUF, комфортно 8+ ГБ;
- рекомендуется 8 ГБ RAM или больше;
- для импорта через picker временно может потребоваться место для копии;
- первый старт модели может занимать десятки секунд.

Вес модели не включён внутрь APK, потому что APK с asset размером 3,8 ГБ
непрактичен. Проверенная копия опубликована рядом с проектом как
`models/Bonsai-27B-Q1_0.gguf`, поэтому её можно скачать из этого репозитория
без отдельного поиска. SHA-256 модели:

```text
17EF842E47450CAEB8EAA3EBFBBAB5D2F2278B62B79BE107985FB69A2F819AA0
```

Официальная карточка указывает около 5.2 ГБ peak memory при 4K context без
KV-cache compression. Android `dumpsys meminfo` показывает для процесса
меньше, потому что memory-mapped страницы GGUF и page cache учитываются не так,
как private native heap.

## Сборка в Android Studio

1. Открыть папку `BonsaiAndroid` в Android Studio.
2. Убедиться, что рядом есть `third_party/llama.cpp`:

   ```text
   gpt/
   ├── BonsaiAndroid/
   └── third_party/llama.cpp/
   ```

3. Установить SDK 36, NDK `28.2.13676358` и CMake `3.22.1`.
4. Проверить путь SDK в `local.properties`.
5. Выполнить `Build > Make Project` или из PowerShell:

   ```powershell
   $env:JAVA_HOME = 'C:\Program Files\Android\Android Studio1\jbr'
   java -classpath gradle\wrapper\gradle-wrapper.jar `
     org.gradle.wrapper.GradleWrapperMain :app:assembleDebug
   ```

Debug APK появляется в:

```text
app/build/outputs/apk/debug/app-debug.apk
```

Проверенный итоговый артефакт дополнительно скопирован в:

```text
release/BonsaiLocal-debug.apk
```

Его размер — 120 762 843 байта, SHA-256:

```text
BDAF2D9EE7EE1BBB2A424242678D75AC35F2B770973E3F4C9E58639CE8F93E5C
```

Первая сборка долгая: генерируются сотни C/C++ объектов и несколько CPU
variants для двух ABI. Инкрементальные сборки значительно быстрее.

### Привязка llama.cpp в CMake

Проект был вынесен из `examples/llama.android`, поэтому относительный путь
исходников пришлось изменить:

```cmake
set(LLAMA_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../../../third_party/llama.cpp)
add_subdirectory(${LLAMA_SRC} build-llama)
```

Для Android компилируются обе ABI:

```kotlin
ndk {
    abiFilters += listOf("arm64-v8a", "x86_64")
}
```

## Установка модели

### Обычный пользовательский путь

1. Скопировать `Bonsai-27B-Q1_0.gguf` на телефон.
2. Открыть приложение.
3. Нажать **Выбрать GGUF** и выбрать файл.
4. Дождаться копирования и загрузки.

### Воспроизводимый путь для эмулятора

После первой установки APK:

```powershell
adb -s emulator-5556 shell mkdir -p `
  /sdcard/Android/data/com.prismml.bonsailocal/files/models

adb -s emulator-5556 push .\models\Bonsai-27B-Q1_0.gguf `
  /sdcard/Android/data/com.prismml.bonsailocal/files/models/
```

После перезапуска приложение найдёт модель автоматически.

## Воспроизводимые тесты Unicode

Debug Activity принимает Base64 UTF-8 prompt. Это обход нестабильного Unicode
input у ADB-клавиатуры preview-эмулятора и не участвует в обычном UI.

```powershell
$prompt = 'Ответь по-русски одним предложением.'
$b64 = [Convert]::ToBase64String(
  [Text.Encoding]::UTF8.GetBytes($prompt)
)

adb shell am start `
  -n com.prismml.bonsailocal/.MainActivity `
  --es prompt_b64 $b64
```

В коде extra декодируется без сетевых вызовов:

```kotlin
String(Base64.decode(encoded, Base64.NO_WRAP), Charsets.UTF_8)
```

## Проблемы, встреченные при разработке

### 1. Готового Android runtime у коллекции нет

Коллекция публикует GGUF и специальный PrismML fork, но не готовый AAR/APK.
Решение: официальный `examples/llama.android` из форка использован как база,
а CMake собирает runtime прямо внутри Gradle.

### 2. Стандартный llama.cpp недостаточен

Модель использует Q1_0 и Qwen 3.5/3.6 hybrid architecture. Решение: закреплён
именно PrismML fork commit `62061f9`, содержащий нужные quantization и Gated
Delta Net paths.

### 3. JDK 17 toolchain отсутствовал

Gradle запускался на JBR 21, а исходный sample требовал установленный JDK 17
через `jvmToolchain(17)`. Gradle не имел repository для автоматической загрузки.
Решение: убрать принудительный поиск отдельного toolchain и явно компилировать
Java/Kotlin bytecode target 17:

```kotlin
kotlin {
    compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
}
```

### 4. Android logging API

`__android_log_is_loggable()` доступен только с API 30, а ранняя конфигурация
использовала minSdk 28. Решение: локальный фильтр log level и итоговый minSdk
30.

### 5. Эмулятор был слишком маленьким

Существующий Pixel_7 имел 2 ГБ RAM и раздел data 6 ГБ, из которых свободно
около 1.1 ГБ. Модель туда не помещалась. Создан отдельный AVD
`Bonsai_27B_Test` с 8 ГБ RAM и 16 ГБ data, не затрагивающий пользовательский
Pixel_7.

### 6. 16 KB memory pages

Android 15+ требует 16 KB-compatible native libraries. Проект использует NDK
r28 и AGP 8.13.2. Проверено:

- ELF `LOAD` alignment: `2**14` для native `.so`;
- `zipalign -v -c -P 16 4 app-debug.apk`: `Verification successful`.

Android 17 preview system image дополнительно показывает экспериментальный
RELRO compatibility dialog для части динамически загружаемых CPU variants.
Неиспользуемый `androidx.datastore` был удалён из общего dependency bundle,
что исключило `libdatastore_shared_counter.so` из APK. После этого в APK
осталось 32 целевых native-библиотеки вместо 36.
APK продолжает работать в page-size compatibility mode. Для production перед
публикацией нужно повторить проверку на стабильном Android 15/16 16 KB image и
обновить NDK/PrismML fork, если preview-проверка станет обязательной.

### 7. Большой GGUF нельзя дублировать бездумно

Импорт в private storage может временно требовать две копии. Для тестового AVD
модель отправлялась сразу в app-specific external directory, которую приложение
сканирует при старте.

### 8. Thinking mode делает даже короткие тесты долгими

Запрос с требованием вернуть одно число всё равно сгенерировал подробный
`<think>` блок. Это ожидаемое поведение модели и полезная проверка reasoning,
но на CPU-эмуляторе занимает минуты. UI генерирует асинхронно и не блокирует
main thread. Более сложный русский Kotlin prompt подтвердил понимание задачи,
но за 512 токенов модель не вышла из thinking в финальный ответ. Это не падение
runtime, а ограничение текущей политики генерации; для прикладного UI нужны
отдельная панель reasoning, больший лимит или поддерживаемое моделью отключение
thinking-mode.

## Ограничения текущей версии

- только text-to-text; vision projection не загружается;
- только CPU backend в протестированном AVD;
- нет встроенного resumable downloader и проверки SHA-256 в UI;
- модель опубликована отдельно от APK в папке `models/`;
- история чата живёт в памяти Activity и не сохраняется после полного restart;
- контекст приложения ограничен 8192 токенами, хотя модель обучена на гораздо
  более длинный контекст;
- thinking-теги показываются как обычный текст;
- на устройствах с 6 ГБ RAM возможен LMK/OOM;
- DSpark speculative decoding не подключён.

## Куда развивать исследование

1. Отделить `<think>` в сворачиваемую UI-панель.
2. Добавить resumable WorkManager/foreground download с SHA-256.
3. Подключить `mmproj` и Android Photo Picker для vision.
4. Исследовать Vulkan backend на Android GPU и сравнить CPU/Vulkan.
5. Подключить Q4 KV cache и замерить RAM на 8K/32K/100K.
6. Проверить DSpark drafter, когда Android path будет поддержан форком.
7. Добавить Room для истории диалогов и export/import sessions.
8. Собрать per-ABI APK/AAB, чтобы не поставлять обе native архитектуры каждому
   устройству.
9. Добавить macrobenchmark: cold load, first-token latency, sustained tok/s,
   thermal throttling и energy usage.
10. Запустить тот же test suite на физическом Snapdragon/Dimensity и сравнить
    NEON, DOTPROD, I8MM и SVE variants.

## Ценность для сообщества и похожие проекты

Локальные LLM на Android уже не являются новой идеей. Официальный `llama.cpp`
содержит Android Studio sample и динамический выбор CPU kernels; PocketPal AI и
ChatterUI запускают разные GGUF на телефоне; MLC LLM, MNN и ExecuTorch предлагают
собственные Android runtime и demo-приложения.

| Проект | Что уже делает | Отличие этого исследования |
|---|---|---|
| [llama.cpp Android](https://github.com/ggml-org/llama.cpp/blob/master/docs/android.md) | официальный JNI/sample и CPU variants | проверена конкретная редкая Q1_0 hybrid-модель и собран готовый APK |
| [PocketPal AI](https://github.com/a-ghorbani/pocketpal-ai) | зрелый GGUF-клиент, загрузки HF и benchmark | универсальный продукт, а здесь узкий воспроизводимый Bonsai-27B test case |
| [ChatterUI](https://github.com/Vali-98/ChatterUI) | GGUF и API-чаты через React Native | здесь минимальный native Android/JNI путь без React Native |
| [MLC LLM](https://llm.mlc.ai/docs/deploy/android.html) | GPU-oriented Android SDK и demo | другой model format/toolchain; текущий проект использует исходный GGUF и CPU |
| [MNN](https://github.com/alibaba/MNN/tree/master/apps/Android/MnnLlmChat) | производительный мультимодальный Android-клиент | значительно шире и быстрее; этот репозиторий проще как regression fixture для Q1_0/GDN |
| [ExecuTorch](https://docs.pytorch.org/executorch/stable/llm/run-on-android.html) | AAR и experimental LLM Java API | требует export в `.pte`; здесь тестируется GGUF/llama.cpp путь |

Поиск по открытым Hugging Face и GitHub проектам на дату публикации не выявил
другой воспроизводимой сборки именно **Bonsai-27B Q1_0 внутри Android APK**.
Это не доказательство абсолютного первенства, но практическая новизна публикации
состоит в сочетании следующих элементов:

- полный проверенный GGUF, APK, исходники и checksum находятся вместе;
- зафиксированы реальные Android pp/tg замеры, runtime paths и screenshots;
- описаны 16 KB page-size и RELRO проблемы preview-Android;
- опубликован не только успешный ответ 703, но и отрицательный Kotlin-тест;
- документация дана на русском и английском языках.

Сейчас это полезный **engineering baseline и regression artifact**, а не новая
научная архитектура или production-конкурент PocketPal/MNN. Наибольшую ценность
следующий этап даст после ARM64-тестов на физических Snapdragon/Dimensity,
измерений RAM/энергии/first-token latency, CI-сборки и upstream PR с найденными
Android-исправлениями.

## Лицензии

- Bonsai-27B GGUF: Apache-2.0 согласно карточке модели.
- llama.cpp / PrismML fork: см. лицензии в `third_party/llama.cpp`.
- При распространении APK и модели необходимо сохранить соответствующие
  `LICENSE` и `NOTICE` файлов upstream-проектов.