<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Microcontroller on RockB</title><link>https://baeseokjae.github.io/tags/microcontroller/</link><description>Recent content in Microcontroller on RockB</description><image><title>RockB</title><url>https://baeseokjae.github.io/images/og-default.png</url><link>https://baeseokjae.github.io/images/og-default.png</link></image><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 06 Aug 2026 20:12:35 +0000</lastBuildDate><atom:link href="https://baeseokjae.github.io/tags/microcontroller/index.xml" rel="self" type="application/rss+xml"/><item><title>SLM on ESP32-S3: Training a Small Language Model on an $8 Microcontroller 2026</title><link>https://baeseokjae.github.io/posts/slm-esp32-s3-microcontroller-training-2026/</link><pubDate>Thu, 06 Aug 2026 20:12:35 +0000</pubDate><guid>https://baeseokjae.github.io/posts/slm-esp32-s3-microcontroller-training-2026/</guid><description>Complete guide to training and deploying a small language model (SLM) on the ESP32-S3 microcontroller — from dataset preparation and quantization to firmware flashing and real-time inference.</description><content:encoded><![CDATA[<h2 id="introduction--why-run-a-language-model-on-an-8-microcontroller">Introduction — Why Run a Language Model on an $8 Microcontroller?</h2>
<p>The idea of running a language model on a microcontroller that costs less than a cup of coffee sounds improbable, but the ESP32-S3 makes it a reality. With dual-core Xtensa LX7 processors running at up to 240 MHz, 512 KB of internal SRAM, and support for up to 8 MB of external octal SPI PSRAM, this $6–8 chip can execute small language models (SLMs) entirely offline — no cloud, no WiFi dependency, no API keys required.</p>
<p>The TinyML market is projected to grow at a compound annual growth rate (CAGR) of 20–25% through 2030, driven by demand for privacy-preserving edge AI. Running SLMs on microcontrollers eliminates the latency, cost, and privacy risks of cloud-based inference. Your data never leaves the device. For applications like voice assistants, industrial control, and autonomous sensor nodes, this is transformative.</p>
<p>This guide walks you through the complete pipeline: selecting a model architecture that fits within the ESP32-S3&rsquo;s memory constraints, training on a suitable dataset, quantizing weights to INT8 or 4-bit, compiling with tools like SynapEdge or a custom C runtime, and finally flashing and running inference on real hardware. By the end, you will have a working SLM running on a $8 microcontroller.</p>
<h2 id="esp32-s3-hardware-overview--what-makes-it-suitable-for-slm">ESP32-S3 Hardware Overview — What Makes It Suitable for SLM</h2>
<p>The ESP32-S3 is not a general-purpose application processor, but it packs surprising capability for neural network inference. Understanding its hardware constraints is the first step to successful SLM deployment.</p>
<h3 id="processor-and-memory-architecture">Processor and Memory Architecture</h3>
<p>The ESP32-S3 features a dual-core Xtensa LX7 CPU clocked at up to 240 MHz. Critically, it includes <strong>AI acceleration vector instructions</strong> — SIMD extensions specifically designed for neural network operations like matrix multiplication and convolution. These instructions can provide 2–4x speedup for inference compared to scalar code.</p>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Specification</th>
          <th>Impact on SLM</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CPU</td>
          <td>Dual-core Xtensa LX7 @ 240 MHz</td>
          <td>Parallel inference scheduling</td>
      </tr>
      <tr>
          <td>Internal SRAM</td>
          <td>512 KB</td>
          <td>OS + runtime overhead (~100 KB)</td>
      </tr>
      <tr>
          <td>External PSRAM</td>
          <td>Up to 8 MB (octal SPI)</td>
          <td>Model weight storage</td>
      </tr>
      <tr>
          <td>External Flash</td>
          <td>Up to 16 MB (quad/octal SPI)</td>
          <td>Firmware + model partition</td>
      </tr>
      <tr>
          <td>AI Instructions</td>
          <td>Vector SIMD extensions</td>
          <td>2–4x matrix multiply speedup</td>
      </tr>
      <tr>
          <td>Connectivity</td>
          <td>WiFi + BLE 5.0</td>
          <td>Optional cloud sync, ESP-NOW</td>
      </tr>
  </tbody>
</table>
<h3 id="memory-budget-for-slm">Memory Budget for SLM</h3>
<p>The practical memory constraint is the PSRAM. With 8 MB of PSRAM, you can fit approximately:</p>
<ul>
<li><strong>1M parameters at FP32</strong>: ~4 MB — leaves room for runtime buffers</li>
<li><strong>1.16M parameters at INT8</strong>: ~1.17 MB — the sweet spot for circuitheroesLM</li>
<li><strong>312K parameters at FP32</strong>: ~1.2 MB — used by esp32-gpio-llm</li>
<li><strong>4-bit quantized models</strong>: 4x smaller than FP32, enabling ~4M parameters in 2 MB</li>
</ul>
<p>The flash memory stores the firmware and can also hold model weights that are read in place (memory-mapped flash), which is how many ESP32-S3 SLM deployments work — the model lives in the flash partition and is accessed directly without copying to RAM.</p>
<h2 id="choosing-your-model-architecture--parameter-budgets-and-trade-offs">Choosing Your Model Architecture — Parameter Budgets and Trade-offs</h2>
<p>Not every language model architecture is suitable for microcontroller deployment. The key constraint is the parameter budget, which directly determines model capacity and inference speed.</p>
<h3 id="architecture-options">Architecture Options</h3>
<p><strong>Tiny Transformer (Decoder-only)</strong>: The most popular choice for microcontroller SLMs. A 2-layer, 4-head transformer with embedding dimension 128 and hidden dimension 256 yields approximately 300K–500K parameters. This is the architecture behind esp32-gpio-llm (312K params).</p>
<p><strong>Engineering State Router (ESR)</strong>: Used by circuitheroesLM (1.16M params), this architecture is optimized for flash bandwidth. It uses a state-routing mechanism that minimizes random access to model weights, making it ideal for memory-mapped flash storage where sequential reads are much faster than random access.</p>
<p><strong>Per-Layer Embeddings (PLE)</strong>: Adapted from Google&rsquo;s Gemma architecture, PLE distributes embedding layers across devices. Used in the distributed 56M-parameter setup across 3 ESP32-S3 boards, this approach scales beyond single-chip limits.</p>
<h3 id="parameter-budget-guidelines">Parameter Budget Guidelines</h3>
<table>
  <thead>
      <tr>
          <th>Model Size</th>
          <th>Parameters</th>
          <th>Memory (INT8)</th>
          <th>Inference Speed</th>
          <th>Use Case</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ultra-tiny</td>
          <td>100K–312K</td>
          <td>0.4–1.2 MB</td>
          <td>150ms–500ms</td>
          <td>GPIO control, keyword classification</td>
      </tr>
      <tr>
          <td>Small</td>
          <td>500K–1.16M</td>
          <td>0.5–1.17 MB</td>
          <td>500ms–1.5s</td>
          <td>Text generation, simple dialogue</td>
      </tr>
      <tr>
          <td>Medium (distributed)</td>
          <td>1M–56M</td>
          <td>1–56 MB (multi-board)</td>
          <td>2–10s</td>
          <td>Complex reasoning, multi-turn agents</td>
      </tr>
  </tbody>
</table>
<p>For a first project, start with the 300K–500K parameter range. It fits comfortably in PSRAM, runs in under a second, and still produces coherent short-form text.</p>
<h2 id="setting-up-the-development-environment-esp-idf-synapedge-arduino">Setting Up the Development Environment (ESP-IDF, SynapEdge, Arduino)</h2>
<p>You have three main paths for developing SLM firmware on the ESP32-S3. Each has different trade-offs.</p>
<h3 id="option-1-esp-idf-recommended-for-full-control">Option 1: ESP-IDF (Recommended for Full Control)</h3>
<p>The Espressif IoT Development Framework (ESP-IDF) is the official SDK and gives you the most control over hardware features, including the AI vector instructions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install ESP-IDF on Ubuntu/Debian</span>
</span></span><span style="display:flex;"><span>sudo apt-get install git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>mkdir -p ~/esp
</span></span><span style="display:flex;"><span>cd ~/esp
</span></span><span style="display:flex;"><span>git clone --recursive https://github.com/espressif/esp-idf.git
</span></span><span style="display:flex;"><span>cd esp-idf
</span></span><span style="display:flex;"><span>./install.sh esp32s3
</span></span><span style="display:flex;"><span>source export.sh
</span></span></code></pre></div><h3 id="option-2-synapedge-compiler-simplest-path">Option 2: SynapEdge Compiler (Simplest Path)</h3>
<p>SynapEdge converts ONNX models to portable ANSI C code that runs on any microcontroller. This is the approach used by the asad-shafi project for running a TinyStoriesV2-trained model on ESP32-S3.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install SynapEdge</span>
</span></span><span style="display:flex;"><span>pip install synapedge
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Convert ONNX model to C</span>
</span></span><span style="display:flex;"><span>synapedge compile model.onnx --target esp32s3 --output ./esp32_model
</span></span></code></pre></div><p>The SynapEdge approach is hardware-agnostic — the same compiled C code can run on ARM Cortex-M, RISC-V, or Xtensa cores with minimal changes.</p>
<h3 id="option-3-arduino-esp32-fastest-prototyping">Option 3: Arduino ESP32 (Fastest Prototyping)</h3>
<p>For rapid prototyping, the Arduino core for ESP32 provides a familiar API with built-in PSRAM support.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install via Arduino CLI</span>
</span></span><span style="display:flex;"><span>arduino-cli core update-index
</span></span><span style="display:flex;"><span>arduino-cli core install esp32:esp32
</span></span><span style="display:flex;"><span>arduino-cli board attach esp32:esp32:esp32s3
</span></span></code></pre></div><p>The Arduino path is best for quick experiments but lacks direct access to the AI vector instructions, which means slower inference.</p>
<h2 id="step-1--preparing-and-quantizing-your-model-onnx-export-int84-bit">Step 1 — Preparing and Quantizing Your Model (ONNX Export, INT8/4-bit)</h2>
<p>Before you can deploy a model to the ESP32-S3, you need to train it, export it to ONNX, and quantize the weights to fit within the memory budget.</p>
<h3 id="training-a-tiny-transformer">Training a Tiny Transformer</h3>
<p>Here is a complete training script for a 312K-parameter transformer suitable for ESP32-S3 deployment:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> torch
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> torch.nn <span style="color:#66d9ef">as</span> nn
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> torch.optim <span style="color:#66d9ef">as</span> optim
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> torch.utils.data <span style="color:#f92672">import</span> Dataset, DataLoader
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TinyTransformer</span>(nn<span style="color:#f92672">.</span>Module):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> __init__(self, vocab_size<span style="color:#f92672">=</span><span style="color:#ae81ff">1000</span>, d_model<span style="color:#f92672">=</span><span style="color:#ae81ff">128</span>, nhead<span style="color:#f92672">=</span><span style="color:#ae81ff">4</span>, 
</span></span><span style="display:flex;"><span>                 num_layers<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>, dim_feedforward<span style="color:#f92672">=</span><span style="color:#ae81ff">256</span>, max_len<span style="color:#f92672">=</span><span style="color:#ae81ff">64</span>):
</span></span><span style="display:flex;"><span>        super()<span style="color:#f92672">.</span>__init__()
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>embedding <span style="color:#f92672">=</span> nn<span style="color:#f92672">.</span>Embedding(vocab_size, d_model)
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>pos_encoding <span style="color:#f92672">=</span> nn<span style="color:#f92672">.</span>Parameter(torch<span style="color:#f92672">.</span>randn(<span style="color:#ae81ff">1</span>, max_len, d_model))
</span></span><span style="display:flex;"><span>        encoder_layer <span style="color:#f92672">=</span> nn<span style="color:#f92672">.</span>TransformerEncoderLayer(
</span></span><span style="display:flex;"><span>            d_model<span style="color:#f92672">=</span>d_model, nhead<span style="color:#f92672">=</span>nhead, 
</span></span><span style="display:flex;"><span>            dim_feedforward<span style="color:#f92672">=</span>dim_feedforward, batch_first<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>transformer <span style="color:#f92672">=</span> nn<span style="color:#f92672">.</span>TransformerEncoder(encoder_layer, num_layers<span style="color:#f92672">=</span>num_layers)
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>output <span style="color:#f92672">=</span> nn<span style="color:#f92672">.</span>Linear(d_model, vocab_size)
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">forward</span>(self, x):
</span></span><span style="display:flex;"><span>        x <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>embedding(x) <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>pos_encoding[:, :x<span style="color:#f92672">.</span>size(<span style="color:#ae81ff">1</span>), :]
</span></span><span style="display:flex;"><span>        x <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>transformer(x)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>output(x)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Count parameters</span>
</span></span><span style="display:flex;"><span>model <span style="color:#f92672">=</span> TinyTransformer()
</span></span><span style="display:flex;"><span>params <span style="color:#f92672">=</span> sum(p<span style="color:#f92672">.</span>numel() <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> model<span style="color:#f92672">.</span>parameters())
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Model parameters: </span><span style="color:#e6db74">{</span>params<span style="color:#e6db74">:</span><span style="color:#e6db74">,</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)  <span style="color:#75715e"># ~312K</span>
</span></span></code></pre></div><p>Train this model on a domain-specific dataset. The TinyStoriesV2 dataset (used by the asad-shafi project) works well for general text generation. For domain-specific tasks like GPIO control, create a synthetic dataset of command-description pairs.</p>
<h3 id="exporting-to-onnx">Exporting to ONNX</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>dummy_input <span style="color:#f92672">=</span> torch<span style="color:#f92672">.</span>randint(<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">1000</span>, (<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">32</span>))
</span></span><span style="display:flex;"><span>torch<span style="color:#f92672">.</span>onnx<span style="color:#f92672">.</span>export(
</span></span><span style="display:flex;"><span>    model, dummy_input, <span style="color:#e6db74">&#34;model.onnx&#34;</span>,
</span></span><span style="display:flex;"><span>    input_names<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;input_ids&#34;</span>],
</span></span><span style="display:flex;"><span>    output_names<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;logits&#34;</span>],
</span></span><span style="display:flex;"><span>    dynamic_axes<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;input_ids&#34;</span>: {<span style="color:#ae81ff">0</span>: <span style="color:#e6db74">&#34;batch&#34;</span>, <span style="color:#ae81ff">1</span>: <span style="color:#e6db74">&#34;seq_len&#34;</span>}}
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><h3 id="quantization-strategies">Quantization Strategies</h3>
<table>
  <thead>
      <tr>
          <th>Method</th>
          <th>Size Reduction</th>
          <th>Accuracy Loss</th>
          <th>Tool</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FP32 → INT8</td>
          <td>4x</td>
          <td>1–3%</td>
          <td>ONNX Runtime quantization</td>
      </tr>
      <tr>
          <td>FP32 → 4-bit</td>
          <td>8x</td>
          <td>3–8%</td>
          <td>Custom quantization</td>
      </tr>
      <tr>
          <td>FP32 → binary</td>
          <td>32x</td>
          <td>15–25%</td>
          <td>Extreme edge cases</td>
      </tr>
  </tbody>
</table>
<p>For most SLM applications on ESP32-S3, <strong>INT8 quantization</strong> is the sweet spot. It reduces a 1.16M-parameter model from 4.6 MB (FP32) to 1.17 MB while retaining over 97% of accuracy.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> onnx
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> onnxruntime.quantization <span style="color:#f92672">import</span> quantize_dynamic, QuantType
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>model_fp32 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;model.onnx&#34;</span>
</span></span><span style="display:flex;"><span>model_int8 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;model_int8.onnx&#34;</span>
</span></span><span style="display:flex;"><span>quantize_dynamic(model_fp32, model_int8, weight_type<span style="color:#f92672">=</span>QuantType<span style="color:#f92672">.</span>QInt8)
</span></span></code></pre></div><h2 id="step-2--compiling-for-esp32-s3-with-synapedge-or-custom-c-runtime">Step 2 — Compiling for ESP32-S3 with SynapEdge or Custom C Runtime</h2>
<p>Once you have a quantized ONNX model, the next step is compiling it into code that runs on the ESP32-S3.</p>
<h3 id="method-a-synapedge-compilation-recommended-for-beginners">Method A: SynapEdge Compilation (Recommended for Beginners)</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Compile ONNX to portable C</span>
</span></span><span style="display:flex;"><span>synapedge compile model_int8.onnx <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --target esp32s3 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --output ./esp32_model <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --optimize flash_bandwidth <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --quantize int8
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This generates:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - esp32_model/model.c       (inference engine)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - esp32_model/model.h       (API header)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - esp32_model/weights.bin   (quantized weights)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - esp32_model/README.md     (integration guide)</span>
</span></span></code></pre></div><p>The <code>--optimize flash_bandwidth</code> flag is critical — it restructures memory access patterns for sequential flash reads, which is 10–50x faster than random access on SPI flash.</p>
<h3 id="method-b-custom-c-runtime-maximum-performance">Method B: Custom C Runtime (Maximum Performance)</h3>
<p>For projects that need every cycle of performance, write a custom C inference engine. The circuitheroesLM project uses this approach with its Engineering State Router architecture.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#75715e">// Simplified inference loop for ESP32-S3
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">#include</span> <span style="color:#75715e">&#34;esp32_model.h&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Model weights stored in flash partition
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">extern</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">uint8_t</span> model_weights[];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">int32_t</span> <span style="color:#a6e22e">slm_infer</span>(<span style="color:#66d9ef">int32_t</span><span style="color:#f92672">*</span> input_ids, <span style="color:#66d9ef">int</span> num_tokens) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int32_t</span> hidden[<span style="color:#ae81ff">256</span>];  <span style="color:#75715e">// PSRAM buffer
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">int32_t</span> logits[<span style="color:#ae81ff">1000</span>]; <span style="color:#75715e">// output buffer
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Embedding lookup (flash-resident)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">embed_lookup</span>(input_ids, num_tokens, hidden);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Transformer layers with AI vector instructions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> layer <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; layer <span style="color:#f92672">&lt;</span> NUM_LAYERS; layer<span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Use Xtensa LX7 vector SIMD for attention
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#a6e22e">xtensa_simd_mul</span>(hidden, model_weights <span style="color:#f92672">+</span> layer <span style="color:#f92672">*</span> WEIGHT_OFFSET, 
</span></span><span style="display:flex;"><span>                       HIDDEN_DIM, HIDDEN_DIM);
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ReLU activation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#a6e22e">relu_inplace</span>(hidden, HIDDEN_DIM);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Output projection
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">output_proj</span>(hidden, logits);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">argmax</span>(logits, VOCAB_SIZE);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="method-c-esp-nn-library">Method C: ESP-NN Library</h3>
<p>Espressif provides the ESP-NN library with optimized neural network kernels that leverage the AI vector instructions. It includes optimized implementations for fully connected layers, activation functions, and softmax.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&#34;esp_nn.h&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ESP-NN optimized matrix multiply
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">esp_nn_fully_connected_s8</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">int8_t</span> <span style="color:#f92672">*</span>input_data,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">int8_t</span> <span style="color:#f92672">*</span>weight_data,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">int32_t</span> <span style="color:#f92672">*</span>bias_data,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int8_t</span> <span style="color:#f92672">*</span>output_data,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> input_size, <span style="color:#66d9ef">int</span> output_size, <span style="color:#66d9ef">int</span> batch_size
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><h2 id="step-3--flashing-and-running-inference-on-the-esp32-s3">Step 3 — Flashing and Running Inference on the ESP32-S3</h2>
<p>With the compiled model and firmware ready, it is time to flash the ESP32-S3 and run your first inference.</p>
<h3 id="building-the-firmware-with-esp-idf">Building the Firmware with ESP-IDF</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Create a new ESP-IDF project</span>
</span></span><span style="display:flex;"><span>cp -r ./esp32_model ~/esp/projects/slm_inference
</span></span><span style="display:flex;"><span>cd ~/esp/projects/slm_inference
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Configure PSRAM and flash settings</span>
</span></span><span style="display:flex;"><span>idf.py set-target esp32s3
</span></span><span style="display:flex;"><span>idf.py menuconfig
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Navigate to:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   Component config → ESP32S3-specific → Support for external, SPI-connected RAM → Enable</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   Set PSRAM clock to 80 MHz, Quad mode</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Configure flash partition for model weights</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Add to partitions.csv:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># model, data, spi_flash, , 4M,</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Build and flash</span>
</span></span><span style="display:flex;"><span>idf.py build
</span></span><span style="display:flex;"><span>idf.py -p /dev/ttyUSB0 flash monitor
</span></span></code></pre></div><h3 id="running-inference">Running Inference</h3>
<p>Once flashed, the ESP32-S3 runs the SLM inference loop. Here is a minimal main application:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">app_main</span>(<span style="color:#66d9ef">void</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Initialize PSRAM
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">esp_psram_init</span>();
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Load model weights from flash partition
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">esp_partition_t</span> <span style="color:#f92672">*</span>model_part <span style="color:#f92672">=</span> <span style="color:#a6e22e">esp_partition_find_first</span>(
</span></span><span style="display:flex;"><span>        ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, <span style="color:#e6db74">&#34;model&#34;</span>);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Allocate input/output buffers in PSRAM
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">int32_t</span> <span style="color:#f92672">*</span>input_ids <span style="color:#f92672">=</span> <span style="color:#a6e22e">heap_caps_malloc</span>(<span style="color:#ae81ff">64</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">sizeof</span>(<span style="color:#66d9ef">int32_t</span>), MALLOC_CAP_SPIRAM);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int32_t</span> <span style="color:#f92672">*</span>output <span style="color:#f92672">=</span> <span style="color:#a6e22e">heap_caps_malloc</span>(<span style="color:#ae81ff">1000</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">sizeof</span>(<span style="color:#66d9ef">int32_t</span>), MALLOC_CAP_SPIRAM);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Tokenize input string
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">int</span> num_tokens <span style="color:#f92672">=</span> <span style="color:#a6e22e">tokenize</span>(<span style="color:#e6db74">&#34;turn on the light&#34;</span>, input_ids, <span style="color:#ae81ff">64</span>);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Run inference
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">uint64_t</span> start <span style="color:#f92672">=</span> <span style="color:#a6e22e">esp_timer_get_time</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">slm_infer</span>(input_ids, num_tokens, output);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">uint64_t</span> elapsed <span style="color:#f92672">=</span> <span style="color:#a6e22e">esp_timer_get_time</span>() <span style="color:#f92672">-</span> start;
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">printf</span>(<span style="color:#e6db74">&#34;Inference completed in %lld ms</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, elapsed <span style="color:#f92672">/</span> <span style="color:#ae81ff">1000</span>);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Decode output token
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">char</span> response[<span style="color:#ae81ff">64</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">detokenize</span>(output, response, <span style="color:#ae81ff">64</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">printf</span>(<span style="color:#e6db74">&#34;Response: %s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, response);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="verifying-inference-on-real-hardware">Verifying Inference on Real Hardware</h3>
<p>The circuitheroesLM project reports <strong>25.35 model steps per second</strong> with 1.16M parameters on an ESP32-S3 N16R8 module. The esp32-gpio-llm project achieves <strong>150ms–1.5s inference latency</strong> with 312K parameters. Your results will vary based on model size, quantization, and optimization level.</p>
<h2 id="performance-benchmarks--latency-memory-and-accuracy-results">Performance Benchmarks — Latency, Memory, and Accuracy Results</h2>
<p>Here is a consolidated benchmark table based on real ESP32-S3 deployments:</p>
<table>
  <thead>
      <tr>
          <th>Project</th>
          <th>Parameters</th>
          <th>Quantization</th>
          <th>Memory Used</th>
          <th>Inference Time</th>
          <th>Accuracy</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>esp32-gpio-llm</td>
          <td>312K</td>
          <td>FP32</td>
          <td>1.2 MB</td>
          <td>150ms–1.5s</td>
          <td>84.4% exact match</td>
      </tr>
      <tr>
          <td>circuitheroesLM</td>
          <td>1.16M</td>
          <td>INT8</td>
          <td>1.17 MB</td>
          <td>39.5ms/step</td>
          <td>Not reported</td>
      </tr>
      <tr>
          <td>asad-shafi TinyStories</td>
          <td>~500K</td>
          <td>INT8</td>
          <td>~2 MB</td>
          <td>~500ms/token</td>
          <td>Perplexity ~8.5</td>
      </tr>
      <tr>
          <td>Distributed PLE</td>
          <td>56M (3 boards)</td>
          <td>4-bit</td>
          <td>16 MB/board</td>
          <td>~5s/token</td>
          <td>Perplexity ~6.2</td>
      </tr>
  </tbody>
</table>
<h3 id="key-performance-factors">Key Performance Factors</h3>
<ol>
<li>
<p><strong>Flash bandwidth is the bottleneck</strong>: SPI flash reads at 40–80 MB/s, but random access adds significant overhead. Models optimized for sequential flash reads (like ESR) perform 3–5x faster than naive implementations.</p>
</li>
<li>
<p><strong>PSRAM vs. Flash for weights</strong>: Reading weights from PSRAM is faster but consumes precious RAM. Most deployments read weights directly from flash (memory-mapped) and only keep the current layer&rsquo;s activations in PSRAM.</p>
</li>
<li>
<p><strong>AI vector instructions</strong>: Using the Xtensa LX7 SIMD instructions for matrix multiplication provides 2–4x speedup over scalar code. Enable them in ESP-IDF with <code>CONFIG_ESP32S3_AI_VECTOR=y</code>.</p>
</li>
</ol>
<h2 id="advanced-distributed-inference-across-multiple-esp32-s3-boards">Advanced: Distributed Inference Across Multiple ESP32-S3 Boards</h2>
<p>When a single ESP32-S3 cannot fit your model, you can distribute inference across multiple boards using ESP-NOW, Espressif&rsquo;s connectionless wireless protocol.</p>
<h3 id="architecture">Architecture</h3>
<p>The wladimiravila project demonstrates a 56M-parameter model distributed across 3 ESP32-S3 boards:</p>
<ul>
<li><strong>Board 0 (Coordinator)</strong>: Input tokenization, first embedding layers, output decoding</li>
<li><strong>Board 1 (Worker)</strong>: Middle transformer layers (layers 2–4)</li>
<li><strong>Board 2 (Worker)</strong>: Final transformer layers (layers 5–6), output projection</li>
</ul>
<p>Each board holds a portion of the model weights in its 16 MB flash. Intermediate activations are transmitted via ESP-NOW, which has a typical latency of 2–5ms per packet.</p>
<h3 id="implementation-sketch">Implementation Sketch</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#75715e">// Coordinator board
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">coordinator_infer</span>(<span style="color:#66d9ef">int32_t</span><span style="color:#f92672">*</span> input_ids) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int32_t</span> embeddings[<span style="color:#ae81ff">256</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">embed_lookup</span>(input_ids, <span style="color:#ae81ff">4</span>, embeddings);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Send embeddings to Board 1 via ESP-NOW
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">esp_now_send</span>(board1_mac, (<span style="color:#66d9ef">uint8_t</span><span style="color:#f92672">*</span>)embeddings, <span style="color:#66d9ef">sizeof</span>(embeddings));
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Wait for final logits from Board 2
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">int32_t</span> logits[<span style="color:#ae81ff">1000</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">esp_now_receive</span>(board2_mac, (<span style="color:#66d9ef">uint8_t</span><span style="color:#f92672">*</span>)logits, <span style="color:#66d9ef">sizeof</span>(logits));
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">int</span> token <span style="color:#f92672">=</span> <span style="color:#a6e22e">argmax</span>(logits, <span style="color:#ae81ff">1000</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">printf</span>(<span style="color:#e6db74">&#34;Generated: %s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, <span style="color:#a6e22e">detokenize</span>(token));
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This approach scales to much larger models but introduces wireless latency. The distributed setup achieves approximately 5 seconds per token for the 56M-parameter model, which is usable for non-real-time applications like autonomous agents.</p>
<h2 id="real-world-applications--gpio-control-voice-assistants-autonomous-agents">Real-World Applications — GPIO Control, Voice Assistants, Autonomous Agents</h2>
<p>The ESP32-S3 SLM ecosystem has produced several compelling real-world applications.</p>
<h3 id="natural-language-gpio-control">Natural Language GPIO Control</h3>
<p>The esp32-gpio-llm project translates natural language commands directly to GPIO actions. A user says &ldquo;turn on the light and set fan to medium speed,&rdquo; and the SLM outputs the corresponding GPIO register values. With 84.4% exact-match accuracy, this is already practical for home automation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#75715e">// GPIO command from SLM output
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">execute_gpio_command</span>(<span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> command) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">strcmp</span>(command, <span style="color:#e6db74">&#34;GPIO_SET_2_HIGH&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gpio_set_level</span>(GPIO_NUM_2, <span style="color:#ae81ff">1</span>);
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">strcmp</span>(command, <span style="color:#e6db74">&#34;GPIO_SET_2_LOW&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gpio_set_level</span>(GPIO_NUM_2, <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ... additional commands
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><h3 id="voice-activated-local-ai-assistant">Voice-Activated Local AI Assistant</h3>
<p>The xiaoclaw project combines SLM inference with voice I/O on a single ESP32-S3 board. It runs a ReAct (Reasoning + Acting) agent loop entirely on-device, with a self-learning system that crystallizes multi-step tasks into reusable skills stored in a memory hierarchy (L0–L4).</p>
<p>This requires the larger 32 MB Flash + 8 MB PSRAM configuration but demonstrates the full potential of on-device AI agents.</p>
<h3 id="autonomous-sensor-nodes">Autonomous Sensor Nodes</h3>
<p>ESP32-S3 SLMs can act as autonomous decision-makers in sensor networks. Instead of sending raw sensor data to the cloud for processing, the microcontroller runs local inference to classify events, generate alerts, or trigger actuators — all while consuming milliwatts of power.</p>
<h2 id="troubleshooting-common-issues-psram-flash-size-inference-speed">Troubleshooting Common Issues (PSRAM, Flash Size, Inference Speed)</h2>
<h3 id="psram-not-detected">PSRAM Not Detected</h3>
<p><strong>Symptom</strong>: <code>esp_psram_init()</code> returns ESP_FAIL or the system crashes when allocating PSRAM.</p>
<p><strong>Solution</strong>: Ensure your board has PSRAM populated (N16R8 variant has 8 MB PSRAM, N16R2 has 2 MB). Verify the PSRAM configuration in menuconfig:</p>
<ul>
<li>Enable <code>SPIRAM</code> and set to <code>Octal SPI PSRAM</code></li>
<li>Set PSRAM clock to 80 MHz (not 120 MHz, which can cause instability)</li>
<li>Enable <code>SPIRAM_USE_CAPS_ALLOC</code> for dynamic allocation</li>
</ul>
<h3 id="model-too-large-for-flash">Model Too Large for Flash</h3>
<p><strong>Symptom</strong>: Build fails with partition overflow, or the device crashes during model loading.</p>
<p><strong>Solution</strong>:</p>
<ul>
<li>Reduce model size with more aggressive quantization (4-bit instead of INT8)</li>
<li>Prune less important weights (structured pruning can remove 30–50% of parameters with minimal accuracy loss)</li>
<li>Use a larger flash module (N16R8 has 16 MB flash; custom boards can use up to 64 MB)</li>
</ul>
<h3 id="slow-inference-speed">Slow Inference Speed</h3>
<p><strong>Symptom</strong>: Inference takes 5+ seconds per token.</p>
<p><strong>Solution</strong>:</p>
<ul>
<li>Enable AI vector instructions in ESP-IDF (<code>CONFIG_ESP32S3_AI_VECTOR=y</code>)</li>
<li>Optimize for sequential flash reads (restructure weight access patterns)</li>
<li>Reduce sequence length (shorter context = faster inference)</li>
<li>Use INT8 or 4-bit quantization (smaller weights = less flash bandwidth)</li>
<li>Consider distributed inference for models over 2M parameters</li>
</ul>
<h3 id="random-crashes-during-inference">Random Crashes During Inference</h3>
<p><strong>Symptom</strong>: ESP32-S3 resets or throws a panic during model inference.</p>
<p><strong>Solution</strong>:</p>
<ul>
<li>Check stack size: increase <code>CONFIG_ESP_MAIN_TASK_STACK_SIZE</code> to 8192 or higher</li>
<li>Ensure all large buffers are allocated in PSRAM, not internal SRAM</li>
<li>Verify flash timing: reduce SPI flash frequency if using long PCB traces</li>
<li>Add cache prefetch hints for sequential weight access</li>
</ul>
<h2 id="conclusion--the-future-of-edge-ai-with-esp32-s3">Conclusion — The Future of Edge AI with ESP32-S3</h2>
<p>Training and deploying a small language model on an $8 ESP32-S3 microcontroller is not just possible — it is practical today. With model sizes ranging from 312K to 1.16M parameters fitting comfortably in PSRAM, inference times under 1.5 seconds, and accuracy exceeding 84% for domain-specific tasks, the ESP32-S3 has become the most accessible platform for on-device language AI.</p>
<p>The ecosystem is growing rapidly. Tools like SynapEdge simplify the ONNX-to-firmware pipeline. Open-source projects like circuitheroesLM, esp32-gpio-llm, and xiaoclaw provide complete reference implementations. Distributed inference across multiple boards pushes the parameter ceiling to 56M and beyond.</p>
<p>For developers, the path is clear: start with a 300K-parameter transformer on a single ESP32-S3, quantize to INT8, optimize for flash bandwidth, and expand from there. The $8 microcontroller is no longer just for blinking LEDs — it is running language models, and the technology is only getting better.</p>
<h2 id="faq">FAQ</h2>
<h3 id="what-is-the-smallest-language-model-that-can-run-on-esp32-s3">What is the smallest language model that can run on ESP32-S3?</h3>
<p>The smallest practical SLM for ESP32-S3 has around 100K–312K parameters. The esp32-gpio-llm project runs a 312K-parameter model in 1.2 MB of FP32 weights, achieving 150ms–1.5s inference latency. Even smaller models (50K–100K parameters) are possible for highly constrained tasks like binary classification or keyword spotting.</p>
<h3 id="do-i-need-external-psram-to-run-an-slm-on-esp32-s3">Do I need external PSRAM to run an SLM on ESP32-S3?</h3>
<p>Yes, for any model larger than about 50K parameters. The ESP32-S3 has only 512 KB of internal SRAM, and the operating system and runtime consume roughly 100 KB of that. External PSRAM (2–8 MB) is required for model weights and activation buffers. Use the N16R8 module variant which includes 8 MB PSRAM and 16 MB flash.</p>
<h3 id="can-i-train-the-model-directly-on-the-esp32-s3">Can I train the model directly on the ESP32-S3?</h3>
<p>No — the ESP32-S3 lacks the memory and compute to train even a small language model. Training is done on a PC or cloud GPU using PyTorch or TensorFlow, then the trained model is exported to ONNX, quantized, and compiled for the ESP32-S3. The microcontroller handles inference only.</p>
<h3 id="how-does-distributed-inference-work-across-multiple-esp32-s3-boards">How does distributed inference work across multiple ESP32-S3 boards?</h3>
<p>Distributed inference splits the model layers across multiple ESP32-S3 boards connected via ESP-NOW (Espressif&rsquo;s wireless protocol). Each board holds a portion of the model weights in its flash. Intermediate activations are transmitted wirelessly between boards. The wladimiravila project demonstrates a 56M-parameter model across 3 boards with ~5s per token latency.</p>
<h3 id="what-is-the-power-consumption-of-an-esp32-s3-running-slm-inference">What is the power consumption of an ESP32-S3 running SLM inference?</h3>
<p>An ESP32-S3 running SLM inference consumes approximately 80–160 mA at 3.3V (264–528 mW) during active inference, and can drop to deep sleep mode at ~10 µA between inference events. This makes battery-powered SLM deployments feasible for applications that run inference intermittently, such as sensor nodes or voice-triggered assistants.</p>
]]></content:encoded></item></channel></rss>