На ESP32-S3 запустили 28.9M-параметровую LLM: разбор архитектуры и инженерных трюков

esp32 llm

Когда читаешь проект esp32-ai от slvDev, первая реакция — это «не сходится». 28.9 миллионов параметров на чипе с 512KB быстрой памяти. Без облака. Без Wi-Fi. Стоимость платы — три доллара. Тем не менее это работает в железе: прошивка кладётся на ESP32-S3 N16R8 и генерирует связный текст со скоростью ~10 токенов/с. Разбираем, что именно сделал автор и почему это интересно для edge AI в целом.

Формула не сходится — и это нормально

Возьмём ESP32-S3. Быстрая SRAM — 512KB. Вся остальная память — либо медленная (PSRAM, 8MB, 60.7 MB/s), либо очень медленная (flash, 16MB, random-read по 20.3 мкс на 512-байтную строку). В такой конфигурации стандартная dense-модель llama-стиля больше пары миллионов параметров не вытянет — просто не влезет в RAM на этапе инференса.

А в esp32-ai — 28.9M. Как?

Ответ — переосмысление того, что должно лежать в быстрой памяти. Не все параметры одинаково полезны, и не все используются с одинаковой частотой. Если спроектировать архитектуру под это наблюдение, можно впихнуть в чип на порядок больше, чем позволяет SRAM.

Gemma 3n: идея из мира телефонов, переехавшая в embedded

Per-Layer Embeddings (PLE) — техника, которую Google применила в Gemma 3n для мобильных устройств. Суть в том, что token embedding разворачивается в набор per-layer таблиц, из которых модель выбирает вектор по индексу. Получается, что большая часть весов фактически не матрица для умножения, а огромный «словарь» спрятанных представлений, из которого читается буквально несколько строк на каждый токен.

slvDev сделал неожиданный ход — перенёс эту идею на иерархию памяти микроконтроллера. Получилось вот что:

  • 558K dense-ядро (трансформер-блоки, attention, FFN) — в PSRAM, читается один раз на позицию
  • 25M-параметровая PLE-таблица — во flash, читается по 6 строк (~450 байт) на токен
  • Активации и нормы — в SRAM

В каждый момент времени в быстрой памяти находится менее 1% параметров. Остальные 99% живут во flash и никогда целиком не загружаются.

Почему именно flash — главная ставка проекта

Это ключевой момент, ради которого всё затевалось. Автор исходно делал ставку: «если таблица сэмплируется sparse, то её bandwidth-цена будет ниже, чем у любой другой части модели». И эта ставка подтвердилась на реальном железе, через Xtensa cycle counter:

  • PSRAM sequential read: 60.7 MB/s
  • Internal SRAM sequential 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-пол: ~58 tok/s

25M-параметровая таблица тратит меньше 0.7% от per-token memory-времени. Flash медленный random-access, но при правильном access pattern он становится практически бесплатным.

На десктопе или мобильнике мы бы сказали «просто добавим оперативки». На микроконтроллере оперативка фиксирована — её впаяли на заводе. А вот flash стоит копейки и его много.

Скорость: как 0.57 tok/s превратили в 9.88

Архитектурно всё уже звучит убедительно. Но перейти от «запускается» к «быстро запускается» — отдельный квест. В RESULTS.md задокументирована итерация по шагам:

Версияtok/sмс/токен
Первая корректная (скалярная)0.571757
Head → PSRAM, базовые оптимизации4.6–4.8194
Точные dot/RoPE/attn, dual-core5.7–6.2139
int8 head + int8 активации~9.5102.9

Главный трюк в финальной версии — перевод выходного head в int8. На старте int4-нибблы распаковываются в int8 один раз, активации квантизируются в int8 per-token, и каждый dot product — простой int8×int8 → int32. Стоимость — крошечная (val perplexity delta ~0.0003 nats), выигрыш — ×1.35 от fp32 dual-core версии.

Профиль на текущей версии (94.9 мс/токен, dual-core LX7):

  • Output head: 59.4 мс (63%)
  • Attention: 20.5 мс
  • PLE path: 6.4 мс
  • FFN: 6.4 мс
  • Input: 2.2 мс

Head — это 2.43MB int8-весов, читаемых на каждый токен. При 60.7 MB/s это даёт ~40ms пол. SIMD-векторизация сжала бы ~15%. Основной рычаг для ускорения — int4 head с SIMD-unpack, или же уменьшение размера head (это уже архитектурное изменение).

Что модель умеет и чего не умеет

Это важно проговорить заранее, иначе «28.9M параметров» звучит внушительнее, чем есть. Модель:

  • ✅ Пишет связные короткие рассказы
  • ❌ Не отвечает на вопросы
  • ❌ Не пишет код
  • ❌ Не знает фактов
  • ❌ Не следует инструкциям

Лимит задаётся dense-ядром (558K параметров). Никакой memory trick не добавляет «ума» — он даёт место для большего числа параметров, а «рассудок» определяется ядром, а не таблицей.

Цифры как доказательство, а не маркетинг

Один из недооценённых плюсов проекта — задокументированные ablation-исследования с двумя seed’ами. Для hardware-проекта это редкость. На vocab 32768, fp32:

КонфигурацияPPLvs baseline
baseline12.58
ple11.41+0.098 nats
fatembed11.94+0.052 nats

PLE выигрывает 0.098 nats — это 16× seed-noise. Не шум, а реальный эффект. И при 4-bit квантизации edge сохраняется на 124–128% от fp32. Забавный бонус: та самая часть модели, ради которой мы тратим flash-бюджет, оказалась самой устойчивой к квантизации. Никакого QAT не понадобилось.

Инженерные мелочи, которые видны только в коде

Несколько деталей, которые меня особенно порадовали — это уровень production-ready embedded, не hobby:

  • Атомарность деплоя. deploy.sh сначала компилирует, потом прошивает. Если сборка упала, новые веса не окажутся на плате со старой прошивкой. SHA-256 пиннинг артефактов на этапе скачивания.
  • Fingerprint at boot. На старте плата выводит FNV-1a-хэш образа. deploy.sh печатает тот же хэш для файла, который прошивал. Совпало — значит, прошито то, что собирались.
  • matvec_i8_range нельзя делать inline. В Arduino ESP32 3.3.10 @ -O3 inline’инг этой функции ломал скорость с 95 до 155 мс/токен. Разработчик обнаружил это и оставил предупреждение на будущее.
  • Skip unreachable padding. Словарь 32768, но реально используется ~25353 строки. 7415 padding-строк пропускаются — экономия bandwidth.

Контекст: это не первый tiny-LM на ESP32

Для понимания масштаба: DaveBben запускал esp32-llm на ESP32-S3 с 260K параметров. У slvDev — 28.9M. Это в 110 раз больше при более жёстком fast-memory бюджете. Принципиально нового инсайта здесь нет — это то, что технически давно можно было сделать, но почему-то не делали.

Karpathy llama2.c — reference impl для обучения маленьких LM на простом C. slvDev использовал его как starting point. Gemma 3n PLE — референс для архитектурной идеи. Всё остальное — плотная работа на стыке ML и embedded.

Что это значит для edge AI в 2026

Я не вижу здесь прямой коммерческой пользы — этот 28.9M не заменит GPT-4 даже близко. Но я вижу несколько важных вещей:

  1. Доказано, что memory hierarchy микроконтроллера — это возможность, а не стена. Если плотно спроектировать архитектуру под silicon, можно впихнуть на порядок больше параметров, чем позволяет SRAM.
  2. Toolchain подтянулся. Хост-gates, верификация, SHA-256 пиннинг, FNV fingerprint, compile-before-flash — это всё уровень production-ready embedded, не hobby.
  3. Идея миграции large-model техник в small-device. PLE — пример общего принципа: «что работает на телефоне, может работать на микроконтроллере, если подумать, как именно».

В каком-то смысле это proof of concept для будущих edge-LLM: голосовые ассистенты без облака, IoT-сенсоры с in-device NLU, хендхелды с LLM-интерфейсом. ESP32 за $3 — это не детская игрушка, а компонент, на котором можно делать вполне серьёзный edge AI.

Что дальше

Из открытой дорожной карты проекта:

  • int4 head + SIMD unpack — главный рычаг для ускорения
  • End-to-end PLE vs baseline замер (сейчас есть только bandwidth-числа)
  • Serial prompting + диалоговая fine-tune
  • Замер реального on-chip demo

Ссылки