Создание локальной LLM с реальным вводом на ESP32S3 и 16MB Flash
Если вы решите запустить языковую модель с 28,9 миллионами параметров на плате ESP32-S3 за 8 долларов, вы быстро столкнетесь с тупиком. Модель, которая отлично работает на презентационных видео, внезапно зависает при соприкосновении с вашими аппаратными периферийными устройствами. SRAM составляет всего 512 КБ, а необходимость прошивать новый раздел размером 15 МБ при каждом изменении предложения быстро начинает раздражать. Чтобы преодолеть эти ограничения и создать устройство, которое работает с реальным вводом с клавиатуры, вам придется полностью перепроектировать кэширование памяти и буферы ввода-вывода с нуля.
Внедрение промптов через Serial без необходимости полной перепрошивки
Перепрошивка платы каждый раз ради изменения одного тестового предложения — пустая трата времени. Связав асинхронные прерывания UART и семафоры FreeRTOS, вы избавитесь от необходимости перезагружать плату. Модель будет мгновенно обрабатывать предложения по мере их ввода через Serial-монитор или клавиатуру.
- Назначьте пины GPIO 16/17 порту UART_NUM_2, установите порог аппаратного FIFO в 120 байт и включите прерывание по приему.
- Создайте кольцевой буфер на 2048 байт во встроенной SRAM. Это предотвратит переполнение данных и сбои во время выполнения инференса.
- Подключите потоковый парсер, определяющий символы переноса строки, и мгновенно конвертируйте входной текст в массив ID токенов с помощью библиотеки токенизатора BTK1 размером 43 056 байт.
- Установите двойной семафор xPromptSemaphore. Когда задача приема завершит запись токенов и вызовет xSemaphoreGive, ожидающая задача инференса получит блокировку памяти через xSemaphoreTake и запустит вычисления.
Такая конфигурация сокращает время на доработку и тестирование промптов более чем на 80%.
`
+------------------+ +-------------------+ +--------------------+
| External UART | ---> | HW FIFO Buffer | ---> | SW Ring Buffer |
| (Keypad/Monitor) | | (120 Bytes) | | (2048 Bytes) |
+------------------+ +-------------------+ +--------------------+
|
v
+------------------+ +-------------------+ +--------------------+
| LLM Forward Pass | <--- | xPromptSemaphore | <--- | BTK1 Tokenizer |
| (Inference Task) | | (Binary Lock) | | (43,056 B Library) |
+------------------+ +-------------------+ +--------------------+
`
Достижение скорости в 14 токенов с помощью послойного распределения эмбеддингов и кэширования в SRAM
Использование архитектуры Per-Layer Embeddings (PLE), предложенной исследователями Google Gemma 3n, позволяет разделить и разместить 4-битную модель размером 14,9 МБ. Таблица эмбеддингов объемом 25 миллионов параметров размещается во флеш-памяти SPI емкостью 16 МБ, а веса Output Head (3,1 миллиона параметров) и кэш KV помещаются в PSRAM емкостью 8 МБ. Самое часто используемое плотное вычислительное ядро (Dense Compute Core) на 559 КБ параметров и буфер HotActivation сохраняются прямо в SRAM на 512 КБ.
Поднять скорость до 14 токенов в секунду и выше оказалось сложнее, чем предполагалось.
- Добавьте атрибут
__attribute__((noinline)) к функции вычислений matvec_i8_range. Если компилятор выполнит инлайнинг автоматически, возникнут промахи кэша I-RAM, и время вычислений не ускорится, а упадет с 94,9 мс до 155.2 мс.
- Разделите операции ple_model_proj и qkv между двумя ядрами Dual Xtensa LX7 процессора ESP32-S3. Нужная скорость достигается только при одновременной работе обоих ядер.
- Расширьте буфер предвыборки (prefetch), который заранее загружает данные эмбеддингов начальных слоев в свободную память SRAM, чтобы преодолеть узкое место флеш-памяти.
Простой перенос кода на язык C дает удручающую скорость в 0,57 токена в секунду. Однако после применения квантования INT8 и предвыборки в SRAM скорость превышает 14,0 токенов в секунду.
| Этап оптимизации |
Задержка вычислений на токен |
Генерация токенов в секунду |
Основные применяемые технологии |
| Чистый порт на C (Baseline) |
1757,2 мс |
0,57 tok/s |
Один корок, вычисления FP32 |
| PSRAM Head и скалярная оптимизация |
193,9 мс |
4,61 tok/s |
Размещение Output Head в PSRAM |
| Применение двухъядерного FP32 |
139,4 мс |
6,22 tok/s |
Двухъядерные вычисления послойного разделения |
| INT8 Staging + оптимизация SRAM |
94,9 мс |
9.88 tok/s |
Квантование INT8, применение noinline |
| Расширение буфера предвыборки SRAM |
~71,4 мс |
14,0+ tok/s |
Предвыборка начальных слоев в SRAM |
Управление током потребления на уровне 210 мА с помощью Light Sleep
При непрерывной работе двух ядер на частоте 240 МГц потребляемый ток взмывает до 210 мА (777 мВт). Помимо того, что плата сильно нагревается, батарея разряжается в мгновение ока. Команда esp_pm_configure должна использоваться для снижения частоты до 40 МГц и включения автоматического режима легкого сна (Light Sleep) в периоды отсутствия инференса.
- В структуре esp_pm_config_esp32s3_t установите max_freq_mhz на 240, min_freq_mhz на 40 и включите light_sleep_enable.
- Выполните команду
esp_sleep_pd_config(ESP_PD_DOMAIN_VDDSDIO, ESP_PD_OPTION_ON). Питание флеш-памяти SPI и RAM должно сохраняться даже в состоянии сна, иначе данные будут потеряны.
- Используйте uart_set_wakeup_threshold и esp_sleep_enable_uart_wakeup, чтобы настроить прерывание пробуждения, которое выведет CPU из спящего режима в момент поступления сигнала по последовательному порту.
При непрерывных вычислениях от литий-полимерного аккумулятора 3,7 В емкостью 1000 мАч устройство проработает всего 4,7 часа. Однако в режиме ожидания Light Sleep ток падает до 0,24 мА (0,88 мВт). В реальных сценариях использования с чередованием периодов ожидания средний ток удерживается на уровне 15–30 мА, что увеличивает время автономной работы более чем в 3 раза.