28.9M-parameter LLM on an ESP32-S3
A 28.9M-parameter language model writing stories on an ESP32-S3, fully offline

By slvDev (GitHub repository owner)
- GitHub stars
- 4.5K (GitHub)
Build sheet
- Board
- ESP32-S3 N16R8
- Other parts
- OLED
- AI model or service
- 28.9M-param TinyStories LLM, 4-bit, on-device
- Difficulty
- Advanced
Parts list
- ESP32-S3 board (512 KB SRAM, 8 MB PSRAM, 16 MB flash) [9]
- 128×64 I2C OLED (SH1106 or SSD1306) (1.3" usually SH1106, 0.96" usually SSD1306) [optional] [10]
- arduino-cli [11]
- uv [12]
Prices and notes: Parts and requirements
Steps
- Skill level
- Advanced
- scripts/fetch_model.sh tinystories (download, verify, install into artifacts/)
- scripts/deploy.sh tinystories (generate headers, run gates, compile, flash) [17]
Full instructions: View original
Like this build? Get the weekly builds
Get the best new AI hardware builds every Thursday. One email a week, free.
Built something? Submit a build
Overview
Uses Gemma-style Per-Layer Embeddings: the 25M-parameter embedding table stays in flash, so a 28.9M-parameter model generates text at 9.88 tokens/s on the ESP32-S3 itself. Covered by Tom’s Hardware.
What it does
- Runs a 28.9 million parameter language model on the ESP32-S3 itself, with nothing sent to a server, showing the text on a small screen at 9.88 tokens per second. [1]
- Fits because most of the model, a 25M-parameter embedding table, stays in flash and only about 450 bytes are read per token (Per-Layer Embeddings, from Google’s Gemma 3n). [2]
- The model is 14.9 MB at 4-bit; activations and norms sit in 512 KB SRAM, the core and output head in 8 MB PSRAM. [3]
- The same chip also runs part of the fruit fly connectome (48,311 neurons) escaping a spider on screen. [4]
The creator's config
Copied word for word from the creator's repository, at the commit linked in each source.
FQBN used by deploy.sh [5]
FQBN='esp32:esp32:esp32s3:UploadSpeed=921600,USBMode=hwcdc,CDCOnBoot=cdc,UploadMode=default,CPUFreq=240,FlashMode=qio,FlashSize=16M,PartitionScheme=custom,PSRAM=opi,DebugLevel=info'OLED wiring (display.h) [6]
// DISPLAY_OLED_I2C (default) -- 128x64 I2C mono OLED. 4 wires: // GND->GND, VCC->3V3, SCL->GPIO46, SDA->GPIO18. Set OLED_CONTROLLER to match // the panel: 1.3" is usually SH1106, 0.96" usually SSD1306. Wrong choice -> // top row correct, rest noise (SH1106-as-SSD1306) -> flip the define.
Use-case ideas
- Run the other published models on the same board: Barista (espresso question answering) or the fly connectome, with the same two commands. [7]
- A reference for Per-Layer Embeddings on microcontroller memory: the reusable architecture is in src/ and the training and quantization code in research/tinystories/. [8]
Parts and requirements
| Part | Spec / notes | Needed | Price |
|---|---|---|---|
| ESP32-S3 board [9] | 512 KB SRAM, 8 MB PSRAM, 16 MB flash | Required | |
| 128×64 I2C OLED (SH1106 or SSD1306) [10] | 1.3" usually SH1106, 0.96" usually SSD1306 · Optional screen; a 2.0" ST7789 SPI TFT is also supported | Optional | |
| arduino-cli [11] | deploy.sh stops if arduino-cli is missing | Required | |
| uv [12] | deploy.sh runs two of its header tools through uv | Required |
- Network: Only for fetch_model.sh, which downloads the model; deploy.sh downloads no model
- Model files from Hugging Face (slvDev/esp32-ai-tinystories), checked against a pinned SHA-256 by fetch_model.sh
- Requirements source: [13]
How it works
Per-Layer Embeddings keep the 25M-parameter embedding table in flash; only the rows each token needs are read, while the dense core sits in PSRAM. 9.88 tokens/s on a small screen.
Setup effort
- Skill level
- Advanced
Two commands: one downloads and verifies the model, the other compiles and flashes it. [17]
Limits and caveats
Why it's interesting
The trick here is memory layout, not raw speed. The ESP32-S3 has 512 KB of SRAM, 8 MB of PSRAM and 16 MB of flash, so activations and norms stay in SRAM, the dense core and output head sit in PSRAM, and a 25-million-parameter embedding table stays in flash, where only about 450 bytes are pulled per token. The README credits this Per-Layer Embeddings idea to Google's Gemma 3n. The result is 9.88 tokens per second end to end, a 14.9 MB model at 4-bit, and no network connection at all. It is also clear about limits: trained on TinyStories, it writes short simple stories and will not answer questions, follow instructions or know facts. The same chip and repo also run the fruit fly connectome demo listed separately on HardwareLab.
Hardware
- ESP32-S3 N16R8
- OLED
Credits and links
- slvDev (Viacheslav Sierbov): Creator
Sources
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L10-L14
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L38-L46
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L22-L35
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L16-L18
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/scripts/deploy.sh#L187
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/firmware/esp32_tinystories/display.h#L4-L7
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L108-L124
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L138-L144
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L25
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/firmware/esp32_tinystories/display.h#L1-L8
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/scripts/deploy.sh#L190-L191
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L130-L133
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L111-L133
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L22-L28
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L48-L54
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/firmware/esp32_tinystories/README.md?plain=1#L14-L18
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L119-L125
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L58-L63
- github.com/slvDev/esp32-ai/blob/52ebcbbd905270592c1d415efd1128d04d0fa365/README.md?plain=1#L124-L125
This page is a short summary written by HardwareLab. Build steps, code and photos belong to the creator; follow “View original” for the full project.
Featured? Add this badge
Built this? Paste the badge into your README. It links back to this page.
Markdown
[](https://hardwarelab.dev/projects/esp32-28m-llm/?utm_source=badge&utm_medium=referral&utm_campaign=launch_2w)HTML
<a href="https://hardwarelab.dev/projects/esp32-28m-llm/?utm_source=badge&utm_medium=referral&utm_campaign=launch_2w"><img src="https://hardwarelab.dev/badge.svg" alt="Featured on HardwareLab" width="188" height="20"></a>Related builds
All 22 in Local LLM Rigs & Edge Agents
PFor
A 180.9M-parameter MoE language model and agent on a $6.80 ESP32-P4.
- WT9932P4-Tiny (ESP32-P4 v1.3, 32MB PSRAM, 16MB flash)
Advanced$6.80 (Wireless-Tag store)8

Muse Gadgets SDK
Meta’s open SDKs for connecting Muse to ESP32 and Raspberry Pi gadgets.
- ESP32 boards
- Raspberry Pi (3B+, 4, 5 or Zero 2 W) or another Linux computer with Bluetooth LE
Intermediate2.2K