28.9 миллионов параметров на ESP32-S3: как запустить LLM на микроконтроллере за 250 рублей

Цифры, которые цепляют

  • 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/s1757 мс
Output head в PSRAM + скалярные чистки4.61–4.77 tok/s194 мс
Точные dot/RoPE/attention + dual-core head5.67–6.22 tok/s139 мс
int8 head + int8 активации~9.5 tok/s102.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):

КонфигурацияPPLvs baseline
baseline12.58
ple11.41+0.098 nats (9.3%)
fatembed11.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

Ссылки

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *