Когда читаешь проект 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.57 | 1757 |
| Head → PSRAM, базовые оптимизации | 4.6–4.8 | 194 |
| Точные dot/RoPE/attn, dual-core | 5.7–6.2 | 139 |
| int8 head + int8 активации | ~9.5 | 102.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:
| Конфигурация | PPL | vs baseline |
|---|---|---|
| baseline | 12.58 | — |
| ple | 11.41 | +0.098 nats |
| fatembed | 11.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 даже близко. Но я вижу несколько важных вещей:
- Доказано, что memory hierarchy микроконтроллера — это возможность, а не стена. Если плотно спроектировать архитектуру под silicon, можно впихнуть на порядок больше параметров, чем позволяет SRAM.
- Toolchain подтянулся. Хост-gates, верификация, SHA-256 пиннинг, FNV fingerprint, compile-before-flash — это всё уровень production-ready embedded, не hobby.
- Идея миграции 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
Ссылки
- Репозиторий: 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
