One Shot Build Script For Running 28M AI Model On ESP32-S3 A developer has released a one-shot build script that automates the entire pipeline for running a 28M parameter AI model on an ESP32-S3 microcontroller, from cloning the repository to training, exporting, compiling, and flashing the firmware. The script, based on the esp32-ai project, trains a TinyStories model on Apple silicon in about 13 minutes and verifies the exported C implementation against a PyTorch golden reference before deployment. | /usr/bin/env bash | | | esp32-ai: clone, train, export, build, and flash — end to end | | | Reproduces github.com/slvDev/esp32-ai from a bare board. | | | Requires: ESP32-S3 N16R8 16MB flash, 8MB PSRAM . | | | | | | Usage: | | | ./build and flash.sh full run: data prep, train, export, build, flash | | | ./build and flash.sh --skip-train reuse an existing firmware/model/model.bin, just build + flash | | | set -euo pipefail | | | SKIP TRAIN=0 | | | for arg in "$@"; do | | | case "$arg" in | | | --skip-train SKIP TRAIN=1 ;; | | | echo "Unknown option: $arg"; exit 1 ;; | | | esac | | | done | | | REPO URL="https://github.com/slvDev/esp32-ai.git" | | | REPO DIR="esp32-ai" | | | ---- -1. Clone the repo if we're not already inside it ---- | | | if -f "pyproject.toml" && -f "$REPO DIR/pyproject.toml" ; then | | | echo " Cloning $REPO URL ..." | | | git clone "$REPO URL" | | | fi | | | if -f "$REPO DIR/pyproject.toml" ; then | | | cd "$REPO DIR" | | | fi | | | echo " Working in $ pwd " | | | ---- 0. Auto-detect the board's port ---- | | | echo " Looking for a connected ESP32-S3..." | | | CANDIDATE PORTS= /dev/cu.usbmodem | | | if -e "${CANDIDATE PORTS 0 }" ; then | | | echo "No /dev/cu.usbmodem device found. Is the board plugged in?" | | | exit 1 | | | elif "${ CANDIDATE PORTS @ }" -eq 1 ; then | | | ESP32 PORT="${CANDIDATE PORTS 0 }" | | | echo " Found one port: $ESP32 PORT" | | | else | | | echo " Found multiple candidate ports:" | | | select choice in "${CANDIDATE PORTS @ }"; do | | | if -n "$choice" ; then | | | ESP32 PORT="$choice" | | | break | | | fi | | | done | | | fi | | | export ESP32 PORT | | | echo " Using port: $ESP32 PORT" | | | uvx --from esptool esptool.py --port "$ESP32 PORT" flash-id | head -20 | | | Expect: Detected flash size: 16MB, Embedded PSRAM 8MB | | | ---- 1. Toolchain ---- | | | echo " Installing toolchain..." | | | brew install arduino-cli | | | arduino-cli config init || echo "Config already exists, continuing..." | | | arduino-cli config add board manager.additional urls \ | | | https://espressif.github.io/arduino-esp32/package esp32 index.json | | | arduino-cli core update-index | | | arduino-cli core install esp32:esp32@3.3.10 | | | arduino-cli lib install "Adafruit SH110X" | | | uv sync | | | ---- 2. Data ---- | | | if "$SKIP TRAIN" -eq 1 ; then | | | echo " --skip-train set, skipping data prep..." | | | else | | | echo " Preparing TinyStories data..." | | | uv run python data/prepare.py --vocab 32768 | | | uv run python src/gen assets.py | | | fi | | | ---- 3. Train ~13 min on Apple silicon MPS ---- | | | if "$SKIP TRAIN" -eq 1 ; then | | | echo " --skip-train set, skipping training..." | | | else | | | echo " Training model this takes a while ..." | | | uv run python src/train.py --arm ple --vocab 32768 --d-model 96 --n-layers 6 \ | | | --ple-dim 128 --target-core 560000 --batch-size 16 --seq-len 256 \ | | | --steps 5000 --seed 0 --tag cleandeploy | | | fi | | | ---- 4. Export and verify on host ---- | | | if "$SKIP TRAIN" -eq 1 ; then | | | if -f "firmware/model/model.bin" ; then | | | echo "No firmware/model/model.bin found. Run once without --skip-train first." | | | exit 1 | | | fi | | | echo " --skip-train set, reusing existing firmware/model/model.bin" | | | else | | | echo " Exporting and verifying against PyTorch golden..." | | | uv run python src/export.py | | | cc -O3 -o /tmp/esp32-llm-verify firmware/host verify/verify.c -lm | | | /tmp/esp32-llm-verify firmware/model/model.bin firmware/model/golden.txt | | | Expect: PASS: C matches PyTorch golden | | | fi | | | ---- 5. Build firmware ---- | | | echo " Compiling firmware..." | | | SKETCH FILE="firmware/esp32 llm/esp32 llm.ino" | | | echo " Disabling USE DISPLAY no OLED wired up in this setup ..." | | | sed -i.bak -E "s/ define :space: +USE DISPLAY :space: + 1/\10/" "$SKETCH FILE" | | | FQBN='esp32:esp32:esp32s3:UploadSpeed=921600,USBMode=hwcdc,CDCOnBoot=default,UploadMode=default,CPUFreq=240,FlashMode=qio,FlashSize=16M,PartitionScheme=custom,PSRAM=opi,DebugLevel=info' | | | arduino-cli compile \ | | | --fqbn "$FQBN" \ | | | --build-property compiler.optimization flags=-O3 \ | | | --build-path /tmp/esp32-llm-uart \ | | | firmware/esp32 llm | | | ---- 6. Flash: app, then model model only needs to happen once ---- | | | echo " Flashing app..." | | | arduino-cli upload -p "$ESP32 PORT" \ | | | --fqbn "$FQBN" \ | | | --input-dir /tmp/esp32-llm-uart firmware/esp32 llm | | | echo " Flashing model into custom partition at 0x110000 ~1m45s ..." | | | if "$SKIP TRAIN" -eq 1 ; then | | | echo " --skip-train set, assuming model partition is already flashed. Skipping." | | | else | | | uvx --from esptool esptool.py --chip esp32s3 --port "$ESP32 PORT" --baud 921600 \ | | | write-flash 0x110000 firmware/model/model.bin | | | fi | | | ---- 7. Run ---- | | | echo " Opening serial monitor. Press the board's RESET button now to see generation from the start." | | | arduino-cli monitor -p "$ESP32 PORT" --config baudrate=115200 |