bytes-per-token · September 2026 · по-русски

A language model on an $8 microcontroller, 3.8–8.2x faster than the open reference port

We measured where the time goes on an ESP32-S3 and rewrote language-model inference for this chip. On the same board and the same weights our engine produces the same text as esp32-ai, several times faster. Our own models, designed for this chip, write better than theirs at the same speed.

8.2xfaster than esp32-ai: same model, same board, same text
3.8xwith exactly the same computation (no two-stage output layer)
0.63 / 0.86bits per byte of text (lower is better): our 8.6M-parameter model vs esp32-ai's 29M; 81 and 94 tok/s on our engine, averaged over the context
0bit differences between board and PC across all tests
ESP32-S3-N16R8 · 2 Xtensa LX7 cores at 240 MHz · 512 KB SRAM · 8 MB PSRAM · 16 MB flash

Same text, same board

Real speeds measured on the board: prompt "Once upon a time", 200 tokens

esp32-ai (their firmware)

10.6 tok/s
Once upon a time
0 / 200 tokens0.0 s

Our engine, same model

86.8 tok/s
Once upon a time
0 / 200 tokens0.0 s

The texts match character for character: 759 of 759. Only the time differs: 18.8 s vs 2.3 s.

Key observation

Speed is bound by memory, not by compute

To write one token the model has to read all of its weights. The chip has three kinds of memory whose speeds differ almost 10x, and the PIE vector unit computes 21x faster than plain code. Once PIE is in use, the processor mostly waits for data.

Memory read speed

MB/s measured on our board (sequential read, 1 core)

Cycles per multiply-accumulate (int4)

Lower is better. Weights in SRAM, C kernel vs PIE assembly kernel

That gives the rule behind all of the work: speedup = fewer bytes per token. Bytes per token = bits per weight × fraction of weights read ÷ tokens per weight pass. Each technique below shrinks one of these factors.

Path

What we did, in order

  1. Phase 0

    A speed calculator for the chip

    A per-operation cost model: reads from each memory plus compute. On third-party measurements it was not fitted to, the error is 3–5% (only two check points); on our engine it underestimates time by 10–14%. It let us drop ideas on paper before writing code.

  2. Phase 1

    Board measurements

    Our own benchmark firmware: SRAM, PSRAM and flash speed, PIE kernels, DMA. Findings: PIE is 21x faster than plain C (~13x faster than esp32-ai's kernel); after it the bottleneck is PSRAM (83 MB/s on two cores); the PSRAM bus gives ~90 MB/s in total, so DMA adds no bandwidth.

  3. Phase 2

    Model architecture experiments

    27 models trained on a laptop, every compared variant in two runs (except one repeated 2-bit run). Per-layer embeddings (PLE) make the model smarter for the same bytes read; ReLU lets 79% of neurons be skipped at no cost; training for 4 bits is almost free; 4–6 layers beat 12.

  4. Phase 3

    The engine on the board

    C code shared by PC and board, plus PIE assembly kernels. Three versions: 70 → 98 → 110 tokens/s on our model. Every change was measured on its own; failed ones were reverted and logged.

  5. Comparison

    esp32-ai's model on our engine

    Their published weights, not requantized. Their firmware and our engine on one board, on their own benchmark: 94.1 vs 11.5 ms per token, identical text.

  6. Phase 4

    Several tokens per weight pass

    Speculative decoding does not pay on this chip (negative result). Batching tokens known in advance (the prompt, unambiguous pieces of JSON) gives 1.5–2.8x.

  7. Phase 5

    Our own model, by the recipe

    The cost model, calibrated on the board, predicted the speed of the variants within 1–5%. The best variant (vocabulary 8192, PLE table twice as wide, 8.6M parameters), trained on 70M tokens, reaches 0.63 bits per byte vs 0.86 for esp32-ai's 29M model; our 3.4M model already beats it at almost the same speed.

Models

Making a model smarter without reading more

Quality at the same bytes read per token (~900 KB)

Loss on held-out text (val loss), lower is better. Two dots per model: two independent runs.
dense and trimmedmixture of experts (MoE)per-layer embeddings (PLE)
TechniqueQuality (val loss)Speed (board or cost model)Verdict
PLE: a per-token table in flash, one row read per token−0.022…−0.039+0.5 ms, boardworks
ReLU instead of SwiGLU, skipping silent neurons (79%)−0.0021.3x, boardworks
Training for 4 bits from the start (QAT)+0.024~7x fewer bytes than fp32 (incl. scales)works
Mixture of experts (4 experts, 1 read)−0.009≈, cost modelweak
2 bits at the same size (with twice the weights and the same bytes read: +0.13)+0.3051.5x, cost model (not run on the board)too costly in quality
12 layers instead of 6+0.069−15%, cost modelworse on both

The combined model combo = PLE + ReLU + 4 bits: val loss 2.010 vs 2.024 for a plain 4-bit model (mean of two runs, better in both), and 22% faster on the board (98 vs 80 tok/s, engine v2).

The recipe against someone else's model: same text, same board

Models with different vocabularies are compared in bits per byte of text: how much information the model is missing to predict each byte. First 5,000 held-out stories, each model with its own tokenizer and its own 4-bit weights, as the engine runs them. Speed is the mean over the whole 256-token context on our engine (two-stage output layer, PSRAM 80 MHz).

ModelParametersBits per bytetok/s (mean, board)
esp32-ai TinyStories (their open model)28.9M0.863~94
combo (ours: PLE + ReLU + 4 bits)3.4M0.732~91
PLE table twice as wide4.9M0.716~84 (previous engine version)
vocabulary 8192 + PLE twice as wide8.6M0.704~81
same, trained on 70M tokens instead of 20M8.6M0.625~81
vocabulary 16384 + PLE twice as wide16.0M0.706— (predicted ~74)

Fairness check: our score for their model (2.45 nats per token) matches what its authors publish (perplexity 11.4 before quantization, "~+1" after). Almost all of their parameters are a 32k-token vocabulary and the PLE table, with a trunk of only 0.56M; children's stories barely use such a vocabulary, and our 16384-vocabulary variant showed that the PLE table saturates. We run their model only on our engine: their firmware runs it at 10.6 tok/s and cannot run our models.

Engine

Where the time goes, theirs and ours

Time per token by stage, the same esp32-ai model

ms per token by stage, their benchmark (200 tokens). The esp32-ai profile is their own output on our board.
4 bits + PIE

Weights are stored in 4 bits and unpacked by vector instructions. esp32-ai uses 8 bits and plain C: twice the bytes and ~13x more cycles per operation (6.2 vs 0.47).

15% of rows

Two-stage output layer: 256 clusters of tokens, score the clusters first, then compute only the best ones in full. Agrees with the full output layer at 99.4–99.8% of positions; the rows that are read match the full computation bit for bit.

2 cores

Every matrix-vector product is split between the cores; the second core spins waiting for work, so handing it a job costs 83–93 cycles.

SRAM

Engine buffers first, then the most-read weights go to fast internal memory (~210 KB), the rest to PSRAM; the PLE table stays in flash.

48% of FFN

ReLU "gate first": compute the gate, then read the other two matrices only for live neurons. A new kernel on the QACC accumulator for the transposed matrix.

−1 ms

Work outside the matrix products: quantization, GELU by table, cluster selection. Guided by the profile, not guesses, it went from 2.2 to 1.2 ms per token.

Engine speed over versions, our combo model

Tokens per second, 2 cores, generation after a short prompt
A fair comparison

One board, one model, one benchmark

We built esp32-ai's firmware their way (arduino-cli, -O3, their flash partition) and measured it on our board. The model fingerprint a9bdd778 matched their metadata, and three runs gave the same value. Their own published figure is 94.9 ms; we measured 94.1 ms.

ms per token, esp32-ai model (29M parameters), positions 4–203

Lower is better
EngineOutput layerPSRAMms/tokentok/sSpeedup
esp32-aifull, 8-bit80 MHz94.1010.61x
oursfull, 4-bit80 MHz25.0040.03.8x
ourstwo-stage80 MHz11.5286.88.2x
oursfull120 MHz*18.9052.95.0x
ourstwo-stage120 MHz*9.17109.010.3x

* PSRAM at 120 MHz is marked experimental in ESP-IDF ("works at a stable temperature"). Measured at room temperature, no faults seen.

What did not work

Negative results are measured too

IdeaExpectedMeasuredWhy
Guess 2–4 next tokens and verify them in one weight pass1.3x by the calculator0.8–1.05xafter the optimizations a third of the time is per-token compute; the drafter yields only 1.4–1.6 tokens per pass
DMA prefetches weights while the core computes−30%no fasterthe PSRAM bus gives ~90 MB/s in total; DMA (77 MB/s) only shares it with the cores, and two cores already read 83
16-bit weight scales instead of 32-bit−6% bytes+5% timeconverting 8192 numbers per token costs more than the read it saves
Keep recent attention values in SRAM−1 ms−0.06 ms attentionthe writes were not the bottleneck, PSRAM read latency was (−0.39 ms in total, the rest unexplained)
Weights from a seed generator (SeedLM), weight compressiona win on GPUsa loss on paperthe chip has no spare cycles: decompression costs more than the read
Prior work

How this project differs from existing ones

ProjectWhere the model runsSpeed on ESP32-S3HasLacks
This projectESP32-S3, fully offline87–110 tok/s (29M), 110–134 (2M); ~72 at the end of the contextPIE kernels, 2 cores, two-stage output layer, sparse FFN, batching, cost model, bit-exact checksan applied model (children's stories so far)
slvDev/esp32-aiESP32-S310.6 tok/s (measured by us)TinyStories and Barista models, PLEPIE, 4-bit execution, work skipping
esp32-tinyLLM (Rust)ESP32-S3~10.5 tok/s (their figure)bit-exact test methodologyvector unit, large byte savings
Needle ports (tool calling)ESP32-S3~1.5 tok/s (their figure)schema-based tool callsspeed
Deeploy (compiler)RISC-V MCU with NPU—tiling, DMA, small LMsESP32 / Xtensa PIE

1. The fastest LLM inference on ESP32-S3 we know of

On the same weights and the same text: 3.8x with identical computation and 8.2x with the two-stage output layer. We found no other open LLM engine using PIE.

2. Models designed for the chip

Architectures compared at equal bytes read per token and checked on the board. Our 3.4M model writes better than esp32-ai's open 29M model at almost the same speed on our engine (0.73 vs 0.86 bits per byte); the best one reaches 0.63.

3. Server-side techniques tested on an MCU

The ideas come from server and mobile LLM inference: candidate screening for the output layer, ReLU activation sparsity and reading only active rows (LLM in a flash), per-layer embeddings. We did not find them measured on a microcontroller, and show which help here, which hurt, and why.

4. A method you can check

A cost model validated on data it was not fitted to, bit-exact agreement between PC and board, every kernel checked against a C reference, every experiment repeated.

"Not found" means a search of GitHub, Hugging Face, arXiv and patents; it is not proof of absence.

Verification

Why the numbers can be trusted

bit for bit

PC, the board with PIE on two cores, on one core, and in plain C give identical digests of all tokens and all logits in every run.

±0.003

The engine's loss on held-out text differs from PyTorch by at most 0.003. The chosen token matches at 98–99% of positions; differences only at near-ties.

<0.1%

Spread between repeats of the same run. This is not the spread across texts: speed drops 1.6x from the start to the end of the context. All measurements are kept as logs in the repository.

Live demo

Try it yourself

An ESP32-S3-N16R8 board connects to any computer over USB. A page in Chrome or Edge flashes the board and then talks to it directly, nothing to install. You type the start of a story, the board continues it in real time, and the page shows the time of every token.

  1. Connect the board with a cable to the COM (UART) USB port.
  2. Open alikup-ai.github.io/bytes-per-token in Chrome or Edge.
  3. Press a Flash button, wait about a minute, close the flasher and reload the page.
  4. "Connect board" → type the start of an English story → "Write".

All models were trained on children's stories. Two images are offered: our varC model (8.6M, 0.63 bits per byte) and esp32-ai's model on our engine. The serial protocol is in engine/demo/README.md.

PC    → G 120 700 40 <seed> 0 7 433 447 259 405 …
board → P 29.6
board → T 11 10320
board → T 612 10288
…
board → E 120 10.32

Limitations

The models are small (3–29M parameters) and trained on children's stories, 20M tokens per model (70M for the best one), one learning rate for all; the quality comparison with esp32-ai uses text from our distribution (their score on it matches their own); speed depends on context length ("110 tok/s" is positions 13–60; the esp32-ai comparison uses their benchmark, positions 4–203); PSRAM at 120 MHz is experimental; energy per token was not measured, and the second core busy-waits; the Arduino core version for the esp32-ai measurement was 3.3.3 (the authors used 3.3.10).