# Как запустить Text-to-Image на ESP32-S3: диффузионный трансформер на чипе за 500 рублей
Каноническая страница: https://plainews.ru/posts/kak-zapustit-text-to-image-na-esp32-s3-diffuzionnyy-transformer-na-chipe-za-500-
Опубликовано: 2026-10-17T07:02:59.187Z

Микроконтроллер ESP32 S3 способен генерировать реалистичные лица по текстовому описанию без подключения к интернету и облачным API. Инференс диффузионной модели
Микроконтроллер ESP32-S3 способен генерировать реалистичные лица по текстовому описанию без подключения к интернету и облачным API. Инференс диффузионной модели занимает 5,4 секунды при разрешении 64×64 пикселя. Для запуска полного пайплайна на чипе с 512 КБ оперативной памяти потребовалось пересобрать архитектуру: заменить U-Net на трансформер DiT, отказаться от тяжелого текстового энкодера CLIP и сжать вычисления до векторных инструкций int8.

В этом руководстве мы разберем инженерные решения, которые позволили уместить генеративную диффузию в микроконтроллер ценой в несколько сотен рублей, обучить модель и запустить локальный Text-to-Image.

## Зачем запускать диффузию на микроконтроллере

Полноразмерные диффузионные модели требуют колоссальных вычислительных мощностей и гигабайтов видеопамяти. Stable Diffusion запрашивает от 4 до 8 ГБ VRAM, а современный Flux требует минимум 12 ГБ, а лучше 24 ГБ оперативной памяти ускорителя. Вес одного только текстового энкодера CLIP составляет около 250 МБ, что в 500 раз превышает весь объем встроенной оперативной памяти стандартного микроконтроллера.

Автономный генеративный пайплайн на чипе решает задачу полной изоляции устройства от облака в умных гаджетах, игрушках или носимой электронике. Когда генерация происходит локально на микросхеме за 500 рублей, девайс не зависит от задержек сети, подписок на API и наличия Wi-Fi. Чтобы проект взлетел на кристалле без тензорных ядер, готовую модель приходится отбросить и проектировать архитектуру строго под аппаратные лимиты конкретного кремния.

| Ресурс ESP32-S3 | Доступный объем | Ограничение для нейросети |
| --- | --- | --- |
| Процессор | 2 × Xtensa LX7, 240 МГц | FPU одинарной точности, отсутствие NPU |
| SIMD PIE | 128-битные регистры | До 16 умножений int8 за одну инструкцию |
| Внутренняя SRAM | 512 КБ (свободно ~320 КБ) | Критически малый буфер для промежуточных активаций |
| Внешняя PSRAM | 2 МБ | Медленный доступ по quad SPI |
| Flash-память | 8 МБ | Отображается в адресное пространство через кэш |

## Шаг 1. Сужаем домен и пишем стеммер на 45 токенов

Универсальная генерация любых объектов в лимитах микроконтроллера невозможна из-за взрывного роста весов. Выбор лица человека в качестве целевого домена дает возможность использовать датасет CelebA (200 тысяч фотографий с 40 бинарными атрибутами) и получить четкие контуры даже в низком разрешении 64×64 пикселя.

Текстовый энкодер CLIP мы заменяем собственной компактной моделью на 300 тысяч параметров со словарем всего из 45 слов. У CelebA нет готовых подписей, поэтому скрипт обучения синтезирует текстовые фразы на лету, случайно комбинируя доступные атрибуты пола, возраста, мимики и прически.

```python
import random

def caption(a, rng=random):
    has = lambda n: a[I[n]] == 1
    male, young = has('Male'), has('Young')
    g = lambda m, f: m if male else f  # согласование по роду
    adj = []
    p = 0.75  # упоминаем признак с вероятностью, чтобы сеть видела неполные фразы

    if rng.random() < p:
        adj.append(g('молодой', 'молодая') if young else g('пожилой', 'пожилая'))
    if has('Smiling'):
        if rng.random() < p:
            adj.append(g('улыбающийся', 'улыбающаяся'))
        elif rng.random() < 0.3:
            adj.append(g('серьёзный', 'серьёзная'))
    return adj
```

Русский язык создает проблему вариативности окончаний: слова «улыбающийся», «улыбающаяся» и «улыбающимися» словарь считает разными сущностями. Запустить тяжелый стеммер или лемматизатор на микроконтроллере невозможно из-за дефицита памяти, поэтому токенизация решается грубой обрезкой слов до первых пяти символов.

```python
import re

def tokenize(text: str) -> list[str]:
    # Обрезаем слово до 5 символов: "улыбающаяся" -> "улыба", "светлыми" -> "светл"
    return [w[:5] for w in re.findall(r'[а-яa-z]+', text.lower().replace('ё', 'е'))]
```

Каждое слово ужимается до корня, что сокращает словарь прошивки до 45 токенов, занимающих по 16 байт на запись.

## Шаг 2. Собираем облегченный автоэнкодер

Генерировать пиксели напрямую слишком ресурсоемко, поэтому вычисления переносятся в латентное пространство. Автоэнкодер упаковывает изображение 64×64×3 в тензор 16×16×4. Это сокращает объем данных для диффузии ровно в 12 раз, а декодирование в исходные цвета выполняется всего один раз на финальном этапе генерации.

Энкодер используется только на этапе обучения на видеокарте и насчитывает 1,9 млн параметров. Декодер переносится в прошивку ESP32, поэтому его архитектура зачищена от тяжелых операций: исключена GroupNorm, которая медленно считается на CPU и плохо поддается int8-квантованию.

```python
import torch.nn as nn

class Decoder(nn.Module):
    CH = (64, 48, 32)

    def __init__(self, z=4):
        super().__init__()
        a, b, c = self.CH
        self.net = nn.Sequential(
            nn.Conv2d(z, a, 3, padding=1),
            Res(a),
            nn.Upsample(scale_factor=2),
            nn.Conv2d(a, b, 3, padding=1),
            Res(b),
            nn.Upsample(scale_factor=2),
            nn.Conv2d(b, c, 3, padding=1),
            Res(c),
            nn.ReLU(),
            nn.Conv2d(c, 3, 3, padding=1)
        )
```

Весь декодер весит 179 тысяч параметров и состоит исключительно из сверток 3×3, блоков ReLU и nearest-апсемплинга. Модель обучается на Google Colab с GPU Nvidia T4 за 20 минут (20 тысяч шагов) с комбинацией функций потерь L1, LPIPS и небольшим добавлением KL-дивергенции.

## Шаг 3. Строим DiT с оптимизацией adaLN-single

Классическая U-Net содержит ветвящиеся skip-соединения, требующие хранения промежуточных тензоров в памяти. Трансформер DiT (Diffusion Transformer) разбивает латент 16×16×4 на неперекрывающиеся патчи 2×2, превращая картинку в последовательность из 64 токенов по 16 признаков. Вся диффузия сводится к регулярным матричным умножениям одинакового размера.

> **Важно.** В базовой архитектуре DiT каждый блок модулируется через отдельный adaLN-слой с проекцией размерности D в 6D. Для скрытого слоя D=192 это потребовало бы 221 тысячу параметров на каждый блок, что превышает размер самого механизма внимания.

Для решения этой проблемы заимствуется трюк adaLN-single из модели PixArt-α: вектор условия шага и текста проецируется один раз в начале сети, а блоки трансформера лишь масштабируют полученные коэффициенты.

```plain
Параметры трансформера DiT:
  Ширина слоя (D): 192
  Количество блоков: 6
  Головы внимания: 6 (по 32 размерности на голову)
  Размерность MLP: x3 (576)
  Всего параметров: 2.9 млн (включая текстовый энкодер)
```

Благодаря сквозному сведению математики к однотипным матричным операциям вычислительное ядро прошивки оптимизируется под одну функцию перемножения матриц.

## Шаг 4. Оптимизируем диффузию под SIMD и память

Стандартный планировщик DDPM требует от 50 до 1000 итераций денойзинга, что неприменимо для микроконтроллера. Переход на Rectified Flow сокращает траекторию генерации всего до 8 шагов без потери качества лиц.

Алгоритм Classifier-Free Guidance (CFG) традиционно удваивает нагрузку, требуя два последовательных прогона трансформера на каждом шаге (с промптом и без). Чтобы исключить повторные чтения весов из медленной flash-памяти, условный и безусловный проходы склеиваются в один общий батч размером 2. Веса считываются из кэша один раз на два потока вычислений.

Вычисления переводятся в целочисленный формат: веса и активации квантуются в int8, а аккумуляция ведется в регистры int32. Расширение PIE (Processor Instruction Extension) в архитектуре Xtensa LX7 задействует 128-битные векторные регистры, выполняя 16 операций умножения int8 за один такт микроконтроллера.

## Где ломается система

Главным узким местом ESP32-S3 при инференсе трансформеров становится пропускная способность шины памяти. Внешняя PSRAM и Flash подключены по quad SPI, поэтому частый случайный доступ к весам вызывает просадки частоты конвейера CPU.

При квантовании функций внимания softmax в int8 без запаса точности активации переполняют аккумуляторы int32. Это приводит к появлению контрастных артефактов и «выбитых» белых пикселей на сгенерированных лицах.

Ограниченный словарь из 45 токенов и пятибуквенный стемминг приводят к коллизиям понятий. Слова со сходными приставками могут активировать одни и те же нейроны, меняя атрибуты лица вопреки смыслу запроса.

## Что попробовать дальше

Для развития автономной генерации на чипе стоит подключить компактный SPI-дисплей (например, ST7789 или ILI9341), настроив отрисовку картинки напрямую из буфера декодера без передачи по USB.

Второй вектор — добавление интерполяции в скрытом пространстве. Фиксируя шум и плавно смещая текстовый вектор между двумя промптами («улыбающийся» и «серьезный»), можно получить плавную анимацию мимики прямо на вычислительных ресурсах микроконтроллера.

## FAQ

### Можно ли сгенерировать пейзажи или технику вместо лиц?

Архитектура позволяет обучить модель на любом другом датасете с малым числом классов. Однако лица обладают выраженной геометрической симметрией, поэтому в разрешении 64×64 они выглядят узнаваемо, в то время как детализированные объекты со сложной сценой превратятся в кашу.

### Подойдет ли обычный чип ESP32 или ESP8266?

Нет, проект требует именно чип ESP32-S3. В классическом ESP32 и ESP8266 отсутствуют 128-битные векторные инструкции PIE, а объема оперативной памяти недостаточно для хранения даже сжатых активаций латентного пространства.

### Зачем нужен стеммер по первым пяти буквам?

Русский язык имеет развитую систему падежей и окончаний, из-за чего обычные словари разрастаются до десятков тысяч токенов. Обрезка до пяти символов сводит разные словоформы («молодой», «молодая», «молодому») к единому токену «молод», укладывая словарь в 45 записей.

### Почему отказались от нормализации GroupNorm в декодере?

Слой GroupNorm требует вычисления среднего и дисперсии по группам каналов в реальном времени, что плохо распараллеливается на процессоре микроконтроллера и дает высокую погрешность при целочисленном int8-квантовании. Замена на стандартные свертки с ReLU сохранила производительность.

## Источники

- Habr ML: Text‑to‑image на ESP32: диффузионный трансформер в микроконтроллере
