Цифры, которые цепляют
- 28.9M параметров, из них 25M живут в lookup-таблице во flash
- Чип: ESP32-S3, 512KB SRAM, 8MB PSRAM, 16MB flash — стоит $3 на AliExpress
- Скорость: 9.88 токенов/с end-to-end (94.9 мс/токен чистых вычислений)
- Размер: 14.9MB в 4-bit квантизации
- Связь с внешним миром: нет, всё на устройстве
Это примерно в 110 раз больше параметров, чем у DaveBben/esp32-llm (260K) — предыдущего рекорда для ESP32. И всё это — на чипе, у которого суммарно 24.5MB памяти, причём 16MB из них — медленный flash.
Проблема: у микроконтроллера почти нет быстрой памяти
ESP32-S3 даёт тебе 512KB быстрой SRAM. Для нейросети это очень мало — обычно именно размер модели упирается в доступную RAM, а не в FLOPs.
В этой модели 28.9M параметров, но в SRAM живут только активации и нормы. Dense-ядро (558K) и output head (1.64MB) — в PSRAM (медленнее, но 8MB хватает). А 25-миллионная таблица эмбеддингов — во flash (16MB, ещё медленнее). То есть из 28.9M параметров в RAM в каждый момент времени находится менее 1%.
Трюк: Per-Layer Embeddings из Gemma 3n
Идея не новая — это Per-Layer Embeddings (PLE) от Google из Gemma 3n. Но здесь её адаптировали под иерархию памяти микроконтроллера, а не под мобильный NPU или GPU.
Идея в том, что большинство параметров сидят в таблице, из которой модель читает, а не вычисляет. На каждый токен нужно прочитать всего около 6 строк (~450 байт). То есть 25M-параметровая таблица никогда не загружается целиком — она сидит во flash и сэмплируется по чуть-чуть.
Три уровня памяти работают по принципу «как часто читаем»:
| Уровень | Что хранит | Объём |
|---|---|---|
| SRAM (быстрая, крошечная) | Активации, нормы (трогаются много раз за токен) | 512KB |
| PSRAM (средняя) | Dense-ядро, output head (один раз на позицию) | 8MB |
| Flash (медленная, огромная) | 25M-параметровая таблица (~6 строк на токен) | 16MB |
Что умеет и чего не умеет
Модель обучена на TinyStories — это синтетический датасет коротких детских историй (Ronen Eldan & Yuanzhi Li, Microsoft Research). Поэтому она умеет писать короткие простые рассказы и в целом остаётся связной. Но:
- ❌ Не отвечает на вопросы
- ❌ Не пишет код
- ❌ Не знает фактов
- ❌ Не умеет следовать инструкциям
Это ограничение маленького dense-ядра (558K параметров), а не трюка с памятью. Сам memory trick не добавляет «ума» — он позволяет впихнуть большую модель в маленький чип.
Скорость: как 0.57 tok/s превратили в 9.88
Это, на мой взгляд, самая интересная часть — инженерная итерация по выжиманию тактов из железа.
| Этап | Скорость | Время/токен |
|---|---|---|
| Первая корректная переносимая версия | 0.57 tok/s | 1757 мс |
| Output head в PSRAM + скалярные чистки | 4.61–4.77 tok/s | 194 мс |
| Точные dot/RoPE/attention + dual-core head | 5.67–6.22 tok/s | 139 мс |
| int8 head + int8 активации | ~9.5 tok/s | 102.9 мс |
Текущий рантайм использует int8-стадированный head: на старте int4-нибблы распаковываются в int8 один раз, активации квантизируются в int8 per-token, и каждый выходной dot product — это просто int8 × int8 → int32 без per-token распаковки. Качество проверено на хосте до прошивки: дельта перплексии ~0.
Распределение времени на текущей версии (94.9 мс/токен, dual-core LX7):
- Output head: 59.4 мс (66%)
- Attention: 20.5 мс
- PLE path: 6.4 мс
- FFN: 6.4 мс
- Input: 2.2 мс
Главный bottleneck — PSRAM bandwidth: head читает 2.43MB int8-весов на токен, при 60.7 MB/s это даёт ~40ms-пол. SIMD-векторизация сжала бы ещё ~15%, но основной рычаг для будущего ускорения — int4 head + SIMD-unpack или уменьшение размера head (это уже архитектурное изменение).
PLE реально работает, а не просто звучит красиво
Автор провёл аккуратные ablation-исследования (vocab 32768, 2 seed, fp32):
| Конфигурация | PPL | vs baseline |
|---|---|---|
| baseline | 12.58 | — |
| ple | 11.41 | +0.098 nats (9.3%) |
| fatembed | 11.94 | +0.052 nats |
PLE обгоняет baseline на 0.098 nats — это в 16 раз больше seed-noise (±0.006). И при 4-bit квантизации эффект сохраняется: edge остаётся на 124–128% от fp32-уровня. То есть часть модели, ради которой мы тратим flash-бюджет, оказывается ещё и самой устойчивой к квантизации. Никакого QAT не понадобилось.
Bandwidth на реальном кремнии
Главная ставка автора была: «flash-таблица почти ничего не стоит по bandwidth». Измерения на реальном ESP32-S3 (через Xtensa cycle counter):
- PSRAM seq read: 60.7 MB/s
- Internal SRAM seq read: 240 MB/s
- Flash random-read, 512B row: 20.3 мкс
- Per-token TABLE cost (6 random rows): ~0.12 мс
- Per-token HEAD cost (1.5MB PSRAM scan): ~17.3 мс
- Bandwidth-only ceiling: ~58 tok/s
Таблица стоит ~0.7% от per-token memory-времени. Ставка сыграла — это и есть ответ на вопрос «зачем вообще так делать».
Безопасность деплоя
Приятная мелочь для опенсорс-проекта с готовыми артефактами: fetch_model.sh проверяет артефакты по SHA-256 и размеру, прописанным в самом скрипте, и сверяет их с metadata.json релиза. Ничего не устанавливается, если проверки не прошли. deploy.sh не ходит в сеть вообще.
Артефакт 14,912,332 байт влезает в кастомный 15,597,568-байтовый flash-раздел. Прошивка 619KB — отдельный 1MB partition.
Пример сгенерированного текста
Стартовый промпт «Once upon a time», greedy generation на устройстве:
Once upon a time, there was a little girl named Lily. She loved to play outside in the sunshine. One day, she saw a big tree with a hole in it. She was curious and wanted to see what was inside.
Связно, в стиле TinyStories. На реальном железе, без единого запроса наружу.
Что это значит
Это не «убийца ChatGPT» — это proof of concept. Главная ценность проекта — демонстрация того, что memory hierarchy микроконтроллера можно использовать как feature, а не как ограничение. Если плотно проектировать архитектуру под silicon (где что лежит, как часто читается), то можно впихнуть на порядок больше параметров, чем позволяет SRAM.
Для embedded-разработчика это особенно интересно: ESP32 стоит $3, не требует радиатора, жрёт 200 мА в пике, и при этом способен генерировать связный текст без Wi-Fi. Handheld-игрушка, IoT-девайс с локальной NLU, offline-помощник в полевых условиях — варианты применения понятны.
Следующие очевидные шаги в репозитории:
- Интерактивный serial prompting + диалоговая fine-tune
- int4 head + SIMD unpack (ожидаемый большой speedup)
- Замер PLE vs baseline end-to-end, а не только bandwidth
Ссылки
- Репо: github.com/slvDev/esp32-ai
- Barista (вторая модель, Q&A про эспрессо): huggingface.co/slvDev/esp32-ai-barista
- TinyStories (HF): huggingface.co/slvDev/esp32-ai-tinystories
- TinyStories paper (Eldan & Li, Microsoft Research): arxiv.org/abs/2305.07759
- Gemma 3n Per-Layer Embeddings: ai.google.dev/gemma/docs/gemma-3n
- Karpathy llama2.c (референс тренировки на plain C): github.com/karpathy/llama2.c
