ESP32 LLMの実装として「$8のマイコンで28.9Mパラメータが動く」と話題になった slvDev/esp32-ai は、2026年7月25日のHacker News投稿で281ポイントを集め、英語圏のメディアが一斉に取り上げました。ただしこの見出しは、リポジトリの中身を読むとそのままの意味では受け取れないことが分かります。作者自身がRESULTS.mdに「能力の倍率として引用するな」と書いているからです。この記事では、公式リポジトリのコードとRESULTS.mdの実測記録だけを材料に、28.9Mという数字の内訳Per-Layer Embeddingsが効く条件実際に何が生成速度を決めているのかを順に分解します。

ESP32-S3上で28.9MパラメータのモデルがTinyStories風の物語を1トークンずつ生成していくデモ。画面には28.9M STORED PARAMETERS、~9.5 tok/s end to end、ESP32-S3の表示がある
公式リポジトリの media/esp32-ple-demo.gif(1200×350・138フレーム)。GIF内に「captured on-microcontroller output (replayed)」と表示されているとおり、実機のシリアル出力を再生した映像です。出典: slvDev/esp32-ai
30秒でわかる esp32-ai(2026年7月28日時点)
  • 正体:slvDev氏が単独開発するマイコン向けLLM実験リポジトリ。★1,735・MIT・コミット31件がすべて1人・コントリビューター1名
  • 何ができる:ESP32-S3の上で外部通信なしにTinyStories風の物語を生成。約9.5 tok/s、小さな画面へ1語ずつ書き出す
  • 28.9Mの内訳:SRAM常駐のコア 558K / PSRAM上の出力ヘッド 3.15M / フラッシュ上の参照表 25.17M の3層合計
  • 律速はフラッシュではない:1トークン102.9msのうち出力ヘッドが57.6ms。25M件の表を引くコストは0.12msで全体の1%未満
  • できないこと:質問応答・指示追従・コード生成・事実の知識。学習データがTinyStoriesなので物語しか書けない
  • 試すには:Release 0件・model.bin もチェックポイントも同梱なし。学習から始める必要がある

この記事はマイコン上でのLLM実行という極端なローカル実行事例として esp32-ai を扱います。LLMの仕組みやローカル実行・量子化の全体像は LLMとは?仕組み・主要モデル比較・ローカル実行・量子化を一気にまとめる2026年版 をご覧ください。

esp32-aiとは:ESP32 LLMを$8のESP32-S3でオンチップ実行する実験リポジトリ

esp32-aiは、マイコン1個の中だけで言語モデルを動かすことを目的にした実験リポジトリです。推論中はWi-Fiを使わず、サーバーへも何も送りません(READMEのスペック表でも Connectivity は none と書かれています)。GitHub APIとリポジトリの中身を確認すると、2026年7月28日時点で以下の状態でした。

項目 実測値(2026-07-28時点)
リポジトリ slvDev/esp32-ai
スター数 1,735
フォーク 183
ライセンス MIT
主要言語 Python(学習側)+ C / Arduino(実機側)
初回コミット 2026-07-20 15:12 JST
リポジトリ公開 2026-07-23 23:53 JST
最終push 2026-07-26 17:48 JST
コントリビューター 1名(slvDev・31コミット)
GitHub Release / タグ 0件 / 0件
Open Issues 7件
対象ハードウェア ESP32-S3 N16R8(SRAM 512KB/PSRAM 8MB/フラッシュ16MB)

READMEはボードの価格を「約$8」としています。英語メディアの一部は$10と表記していますが、本記事は一次情報であるREADMEの表記に従います。ボード価格はモデルや流通経路で変動する数字なので、厳密な単価というより「安価な汎用マイコンで成立する」という主張の目安として読むのが妥当です。

コミット履歴が示す「7日間」

31件のコミットは2026年7月20日から26日までの7日間に収まっています。並べ替えると、この期間に何が起きたかがそのまま読み取れます。

日時(JST) 出来事
07-20 15:12 初回コミット。uvプロジェクト初期化+TinyStoriesのデータ準備
07-20 5アーム(baseline / ple / ple_notable / fatembed / bigcore)の学習ハーネス、3層メモリ会計、4bit量子化チェックを追加
07-21 実機の帯域ベンチ、int4エクスポート、可搬C推論ランタイム、ESP32-S3スケッチを一気に追加
07-21 23:40 RESULTS.mdに実験設計と検証結果を記録
07-22 04:15 int8ヘッド+int8活性で9.72 tok/sへ到達したと記録
07-22 I2C OLED(SSD1306 / SH1106)対応を追加
07-23 23:53 GitHub上でリポジトリを公開。デモGIFをREADMEへ追加
07-26 03:59 Hacker Newsに投稿。281ポイント・70コメント
07-26 17:48 MIT LICENSEを追加(コミット 74744182

ここで注意したいのはMIT LICENSEの追加がHacker News投稿の約14時間後である点です。公開時点のコミット(9c4a214b)の中身を確認すると、LICENSE ファイルが無いだけでなく pyproject.tomllicense の記載もなく、READMEにもライセンスへの言及がありませんでした。つまり公開からHN掲載までの数日間、このリポジトリには明示的なライセンス表示が存在しなかったことになります。その時期にクローンした場合は、再配布や派生物を検討する前に現在の LICENSE(MIT)を基準として確認しておくのが安全です。

何ができて、何ができないか

READMEは能力の限界を先に書いています。モデルはTinyStories——子ども向けの短い物語だけを集めた合成データセット(Eldan & Li, Microsoft Research)で学習されているため、書けるのは短い物語だけです。

できる:短く単純な物語を、おおむね一貫性を保って生成する
できない:質問に答える/指示に従う/コードを書く/事実を知っている

RESULTS.mdは、この天井が密なコア側で決まっており「メモリの工夫では動かない」と明記しています。つまりesp32-aiが示したのは「マイコンで賢いAIが動く」ことではなく、大きなモデルを小さなチップへ載せるためのメモリ設計です。読む側もそこを取り違えないほうが、この成果を正しく評価できます。

「28.9Mパラメータ」の正体:3層メモリ会計をbudget.pyから再計算する

この記事で最初に確かめたいのが、28.9Mという数字の内訳です。リポジトリには src/budget.py という、報告専用の3層メモリ会計モジュールが独立して置かれています。コメントには「このファイルは報告するだけ」と書かれ、学習側の予算計算(model.pyparam_budget())とは意図的に切り離されています。

3層メモリ会計の図。SRAMにcore 558K、PSRAMにstream=出力ヘッド3.15M、FLASHにtable=PLE埋め込み表25.17Mが配置される
budget.py が定義する3層。区別しているのは速度ではなくアクセスパターンで、そこが設計の要点です

budget.py の分類は「速いか遅いか」ではなく、毎トークンどう読むかで切られています。

core:密で、毎トークン全体をランダムアクセスする。SRAMに置くしかない。本当に希少な予算
stream:密だが、毎トークン先頭から末尾へ1回だけシーケンシャルに走査する。出力ヘッドがこれに当たる。消費するのはSRAM容量ではなく帯域
table:疎。毎トークン1行だけ引く。メモリマップされたフラッシュに最適

budget.py のコメントは、この分類が生まれた理由をこう書いています——streamをcoreと同じものとして扱ってきたことが、大きな語彙を「使えない」ように見せていた。実際には単に遅いだけだった、と。

実際に電卓を入れて確かめる

budget.pytiers() 関数の式に、firmwareのREADMEが示す起動時診断のパラメータ(V=32768 D=96 L=6 H=4 F=66 P=128)を代入すると、次のようになります。

階層 計算式 パラメータ数 4bit時のサイズ 毎トークンの読み出し
core(SRAM) L × (4D² + 3DF + 2D·P) + D·L·P 556,416 約272KB 全体
stream(PSRAM) V × D 3,145,728 1.5MB 全体を1回走査
table(FLASH) V × L × P 25,165,824 12.0MB 6行=384バイト
合計 28,867,968 約14.4MB

合計28,867,968=28.87M。READMEの「28.9M」と一致します。表の12.0MBもRESULTS.mdの「25M-param PLE table (12MB flash)」と一致し、エクスポートされた実ファイル(14,912,332バイト=14.9MB)とも整合します。

coreだけはRESULTS.mdの表記(ple = 558K)とわずかにずれますが、これは budget.py の式がRMSNormの重みを数えていないためです。model.py を見ると、各ブロックに attn_norm / ffn_norm / ple_norm の3つ(各96要素)、モデル側に ple_proj_norm(128要素)と out_norm(96要素)があります。6ブロック×288+224=1,952を足すと556,416+1,952=558,368となり、RESULTS.mdの558Kに一致します。

この記事のポイント:作者自身が指定している引用の作法
RESULTS.mdは「28.9Mパラメータ」の扱いについて、「メモリ階層の分割によって常駐するパラメータ数」として引用せよ、能力の倍率としては決して引用するなと明記しています。これは謙遜ではなく定義の問題です。毎トークン実際に演算に使われる密な部分は558Kであり、25Mの表は「その都度6行だけ引かれる条件付きの記憶」だからです。

「フラッシュに大きな表を置く」という発想自体は、モデル圧縮の文脈でも繰り返し登場します。量子化でモデル本体を小さくするアプローチとの対比は BitNet|Microsoftの1ビットLLM量子化フレームワーク — CPUだけで100Bモデルが動く仕組み が詳しく、こちらは「重み自体を細くする」方向の代表例です。

Per-Layer Embeddings(PLE)とは:Gemma由来の仕組みをマイコンのメモリ階層へ移す

Per-Layer Embeddings(PLE)はGoogleがGemma 3n・Gemma 4で採用した設計です。通常の埋め込みが「入力の一番下で1回だけトークンをベクトル化する」のに対し、PLEは各層に対して別々の埋め込みを持ち、層ごとに注入する形をとります。

flowchart LR T["トークンID"] --> TB["FLASH: PLE表
32768行 × 6層 × 128次元
毎トークン6行だけ読む・約384B"] T --> EM["入力埋め込み
出力ヘッドと重み共有"] EM --> CORE["SRAM: dense core 558K
6層 × Attention + SwiGLU"] TB --> CORE CORE --> HEAD["PSRAM: 出力ヘッド 3.15M
32768語彙ぶんを毎トークン全走査"] HEAD --> NX["次のトークン"] NX --> T

重要なのは、この形がマイコンのメモリ階層と噛み合うことです。層ごとの埋め込み表は行数が語彙サイズ、列が層数×表の幅になるため巨大になりますが、1トークンで必要になるのはそのトークンに対応する行だけです。esp32-aiの構成では6層×128次元=768要素、4bitなら384バイトを読むだけで済みます。

model.py のコメントによれば、この注入は単なる「トークンごとのバイアス加算」ではありません。各ブロックが ple_gate(コアの状態から作るゲート)と ple_proj(表の行を戻す射影)を持ち、ゲートと表の行を掛け合わせてから残差へ足す構造になっています。掛け算があることで「条件付け」として働く、というのが作者の設計意図です。

「SSDから重みをストリーミングすれば同じでは?」への答え
Hacker Newsでも「CPUとフラッシュで同じことをすれば大きなモデルが動くのでは」という質問が出ています。ここは混同しやすい点ですが、PLEはそのように学習された場合にのみ成立する構造です。既存のモデルの重みを後からSSDへ追い出す手法(重みストリーミング)とは別物で、学習時から「層ごとに引く表」を持つ設計になっている必要があります。またフラッシュがアドレス空間へメモリマップ(XIP)されているため、NVMeのようにコマンド発行と完了待ちが挟まらないことも、マイコン側に有利に働いています。

「速いメモリに収まらない部分を、遅いが大きいメモリへ逃がす」という発想自体は、サーバー側の推論でも同じ形で現れます。MoEモデルの専門家をCPU側メモリへオフロードして24GB VRAMに載せる手法は KTransformers解説|24GB VRAMでDeepSeek-R1級MoEを動かすCPU/GPUヘテロ推論 が扱っており、規模はまったく違うものの「疎にアクセスされる巨大な部分だけを外へ出す」という骨格は共通です。

アブレーションが示すPLEの適用条件:語彙32768で+0.098nats、4096では4分の1

esp32-aiが単なるデモに留まらないのは、PLEが効く条件を対照実験で切り分けている点です。RESULTS.mdには5つのアーム(実験条件)による比較が、いずれも2シードで記録されています。

語彙4096と語彙32768でのPLEの優位を比較した図。4096では+0.025natsどまりだが32768では+0.098natsに広がる
PLEの優位は語彙サイズに強く依存する。小語彙の結果は対照実験であって製品構成ではない、とRESULTS.mdは明記している

デプロイ構成(語彙32768・コアを約559Kに揃えた条件)の結果は次のとおりです。

アーム コア 合計パラメータ perplexity baseline比
baseline 559K 3.7M 12.58
ple 558K 28.9M 11.41 +0.098 nats(ppl 9.3%改善)
fatembed 559K 28.9M 11.94 +0.052 nats

fatembed は「同じ量のパラメータを入力の一番下にまとめて置く」条件です。層ごとに注入する ple がこれを0.046nats上回っているので、パラメータをどこへ置くかではなく、どこで注入するかが効いていることになります。RESULTS.mdはこの差を「シードノイズ±0.006の約16倍」と表現しています。

対照実験が示す3つの境界条件

小語彙(4096)での比較は、PLEが万能ではないことを示す対照実験として置かれています。

アーム perplexity baseline比
baseline 8.21
ple_notable(表を外し配管だけ) 8.35 -0.017(悪化)
fatembed 8.26 -0.006(ほぼ変化なし)
ple 8.00 +0.025
bigcore(コア2倍) 6.93 +0.170

ここから読み取れることは3つあります。

効いているのは表であって配管ではないple_notable は層ごとのアダプタと射影を全部持ちながら参照表だけ外した条件で、baselineより悪化します。表がなければ、機構に割いたコアのパラメータが丸損になるということです
行の幅は飽和し、行数は飽和しない:表の幅(ple_dim)を増やすスイープでは+0.045(d64)→+0.094(d256)→+0.087(d512)と頭打ちになります。一方、語彙を32768にして行数で表を大きくした場合は+0.098がそのまま出ています
PLEはタダの容量ではない:コアを2倍にする bigcore は語彙4096で+0.170と圧勝します。デスクトップならコアを買えばいい話で、コアが固定のシリコンでフラッシュだけが潤沢という制約があって初めてPLEが選ばれる、というのがRESULTS.mdの整理です

つまりesp32-aiの結果は「PLEが常に強い」ではなく、語彙が大きく、かつ速いメモリを増やせない環境でだけ強いという限定付きの主張です。この限定を自分で書き残しているところが、この実験の信頼性を支えています。

実測プロファイルの逆説:遅いはずのフラッシュではなく出力ヘッドが律速

多くの人が直感的に「フラッシュは遅いのだから、そこを読むPLE表がボトルネックだろう」と考えます。実機の計測結果はその逆でした。

1トークンあたりの処理時間内訳。出力ヘッド57.6ms、Attention 25.6ms、PLE経路8.5ms、FFN 6.9ms、入力処理4.4ms
1トークン102.9msの内訳。フラッシュ上のPLE表を引くコスト自体は0.12msで、全体の1%にも満たない

まず、実機(ESP32-S3 N16R8)でXtensaのサイクルカウンタを使って測った帯域の実測値がこちらです。

計測項目 実測値
PSRAM シーケンシャル読み出し 60.7 MB/s
内部SRAM シーケンシャル読み出し 240 MB/s
フラッシュ ランダム読み出し(512Bの1行) 20.3 µs
PLE表の1トークンあたりコスト(6行) 約0.12 ms
出力ヘッドの1トークンあたりコスト(1.5MBのPSRAM走査) 約17.3 ms
帯域だけで決まる理論上限 約58 tok/s

25Mパラメータの表を引くコストは、この合成ベンチマークにおける1トークンあたりメモリ時間の約0.7%でした。RESULTS.mdはこの0.7%を「推論全体のオーバーヘッド」ではなく「合成ベンチにおける表の帯域シェア」として引用するよう指定しています。同様に58 tok/sも「帯域の上限であって観測されたスループットではない」と釘を刺しています。

40msの床は自分で計算し直せる

現在のランタイムのプロファイルは、出力ヘッド57.6ms/Attention 25.6ms/PLE経路8.5ms/FFN 6.9ms/入力処理4.4msで、合計102.9ms=9.72 tok/s(計算のみ)です。RESULTS.mdは出力ヘッドについて「計算律速ではなくPSRAM帯域律速」と書いていますが、これは手元で検算できます。

出力ヘッドはint8で常駐し、パディングされた到達不能な語彙行7,415行を飛ばして走査します。実際に読む行数は 32,768 − 7,415 = 25,353行。1行はd_model=96要素なので、25,353 × 96 = 2,433,888バイト=約2.43MB。これをPSRAMの実測帯域60.7 MB/sで割ると 約40.1ms となり、RESULTS.mdの「57.6msのうち約40msは読み出しの床」という記述と一致します。残る約17msだけが演算時間ということです。

この分解が意味するのは、SIMD命令を使っても改善幅は限定的ということです。RESULTS.mdはこの点を「SIMDで削れるのは17ms側だけで、上限は約15%」と見積もり、次の大きなレバーは「読むバイト数を減らす(int4ヘッド+SIMDでの展開)」か「出力ヘッド自体を小さく/分解する(モデル側の変更)」であって、ベクトル化を頑張ることではない、と結論づけています。

0.57 tok/sから9.5 tok/sまで

到達までの経過も5段階で記録されています。

最適化の履歴。最初の移植版0.57 tok/sから、PSRAM常駐4.61、厳密整理5.67、デュアルコア6.22、int8ヘッド9.50 tok/sへ
すべてスカラー演算のままでの改善。ESP32-S3のSIMD命令は現時点で未使用

最初に正しく動いた可搬版は1ステップ1,757.2ms(0.57 tok/s)でした。そこからヘッドのPSRAM常駐化とスカラー処理の整理で193.9ms、dot積・RoPE・Attentionの厳密な整理で172.9ms、ヘッドの2コア分割で139.4ms、int8ヘッド+int8活性で102.9msという経過です。

int8化については、ホスト側の検証perplexityで劣化がほぼゼロであることを確認してから実機へ載せたとRESULTS.mdは記録しています(firmware/host_verify/ppl.c)。fp32の厳密版(139.4ms)は正解基準として残され、ホスト側のC実装は32,768個すべてのロジットについてPyTorchの出力と最大絶対誤差0.00001で一致しています。

「速いメモリが足りないなら、台数を増やして帯域ごと確保する」という別方向のアプローチもあります。複数デバイスへモデルを分割する Distributed Llama:家庭用デバイスを繋ぐだけでLLMローカル実行を高速化する分散フレームワーク はその代表で、esp32-aiとは「1台のメモリ階層を使い切る」か「台数で殴る」かという対照的な選択になっています。

4bit量子化とESP32 LLM先行実装との比較:PLEの優位は量子化後にむしろ広がる

フラッシュに置く以上、重みは4bitで格納されます。「学習時のfp32で見えた差が、量子化で消えてしまわないか」は当然の疑問で、esp32-aiはこれも2シードで検証しています。

src/quantize.py はグループ単位の対称int4量子化(グループ64、GGUFのQ4形式に相当)を行い、25Mの参照表を含むすべての大きな重みを量子化してから検証損失を測り直します。

アーム fp32 → 4bit での劣化
baseline +0.079 / +0.088 nats
ple +0.055 / +0.061 nats
fatembed +0.046 / +0.050 nats

PLEとbaselineの差は、fp32で+0.101/+0.095だったものが4bit後には+0.125/+0.121になります。つまり優位は124〜128%が保たれる——正確には広がるという結果です。RESULTS.mdはこれを「すべてのアームが劣化するがPLEの劣化が小さい。グループごとのスケールを持つ巨大で冗長な参照表は、1つ1つの重みが重要な小さな密モデルより本質的に量子化に強い」と説明しています。フラッシュ予算を賭けた部分が、同時に最も4bit耐性の高い部分だったわけです。

実際にフラッシュへ書き込まれる形式(グループ128・可変長行・fp16スケール)でも同じ検証を行い、優位の保持率は126%/121%でした。実際に焼くバイト列でも2シードの結論は変わらないことまで確認されています。

先行するesp32-llmとの比較

ESP32上でLLMを動かした先行例として、2024年9月に公開された DaveBben/esp32-llm があります。両者の位置づけを整理すると次のようになります。

項目 slvDev/esp32-ai(2026-07) DaveBben/esp32-llm(2024-09)
保存パラメータ数 28.9M(3層に分散) 260K
モデルの出自 TinyStoriesで自前学習 karpathy/tinyllamas の stories260K
実装の系譜 独立実装(Gemma PLEを再現) llama2.c を移植・最適化
モデルの置き場所 フラッシュ12MB+PSRAM+SRAM PSRAM(約1MB)
実測速度 約9.5 tok/s(エンドツーエンド) 19.13 tok/s
SIMD利用 未使用(スカラーのみ) ESP-DSPのdot積で利用
対象ボード ESP32-S3 N16R8 ESP32-S3FH4R2

数値だけ見れば先行実装のほうが2倍速いことになりますが、条件が違います。260Kのモデルは全体で約1MBなので毎トークン全部読んでも安く、さらにESP-DSPのSIMD命令を使っています。一方esp32-aiは約110倍の保存パラメータを抱えながらスカラー演算のみで9.5 tok/sに到達しており、RESULTS.mdは両者を「同程度の実用速度帯」と表現しています。優劣というより、モデルを大きくしたときに速度がどこまで持ちこたえるかという別の問いに答えたのがesp32-aiです。

なおRESULTS.mdは来歴についても明示的で、依存しているのはTinyStoriesデータセットとGoogleが公開したGemmaのPLE設計だけであり、llama2.cやesp32-llmからコード・モデル・手法を引き継いではいない、と書いています。READMEでkarpathy氏のllama2.cに触れているのは「小さな言語モデルを学習してCで動かせると信じられるようになった理由」という影響の記述であって、実装上の依存ではありません。

ESP32 LLMを自分で動かす手順と、再現できる範囲

最後に実務的な話です。このリポジトリを試すハードルは、記事の見出しから受ける印象よりかなり高いというのが結論です。

自分で動かすまでの4ステップ。データ準備、学習、エクスポート+検証、実機へ書き込み
Release 0件・model.bin もチェックポイントも同梱されておらず、学習から始める必要がある

ステップ1:データを準備して学習する

data/prepare.py はHugging FaceからTinyStoriesの学習ファイルを先頭300MBぶんだけ取得し、BPEトークナイザーを学習してuint16のトークン列を書き出します。コメントによれば300MB=約7,500万トークンで、「1Mパラメータのコアを計算最適点よりかなり過学習させるのに十分な量」を意図した設計です。学習は src/train.py にアーム名を渡して実行します。デバイスはMPS(Apple Silicon)→CUDA→CPUの順に自動選択されます。

# 語彙32768でデータを準備(TinyStoriesの先頭300MBを取得しBPEを学習)
cd data && uv run python prepare.py --vocab 32768 && cd ..

# デプロイ構成のアームを学習(アームは baseline / ple / ple_notable / fatembed / bigcore)
cd src && uv run python train.py --arm ple --steps 4000 --batch-size 32 --seq-len 512

ステップ2:int4へエクスポートしてホスト側で照合する

実機に焼く前に、可搬C実装の出力がPyTorchと一致することをホストで確認します。RESULTS.mdによれば、この照合は32,768個すべてのロジットについて最大絶対誤差0.00001で通っています。

# group-128のラグドint4形式でエクスポート
cd src && uv run python export.py && cd ..

# 可搬C実装をゴールデン出力と照合
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

ステップ3:ビルドして実機へ書き込む

ファームウェアはArduino ESP32 core 3.3.10でビルドします。カスタムパーティション構成が前提で、モデル本体は 0x110000 に別途書き込みます。

# ファームウェアをビルド(PSRAM=opi・16MBフラッシュ・カスタムパーティション)
arduino-cli compile \
  --fqbn 'esp32:esp32:esp32s3:UploadSpeed=921600,USBMode=hwcdc,CDCOnBoot=cdc,UploadMode=default,CPUFreq=240,FlashMode=qio,FlashSize=16M,PartitionScheme=custom,PSRAM=opi,DebugLevel=info' \
  --build-property compiler.optimization_flags=-O3 \
  --build-path /tmp/esp32-llm-build \
  firmware/esp32_llm

# モデル本体を専用パーティションへ書き込み、シリアルで生成を確認
esptool.py --chip esp32s3 --port /dev/cu.usbmodem2101 --baud 921600 \
  write_flash 0x110000 firmware/model/model.bin
arduino-cli monitor -p /dev/cu.usbmodem2101 --config baudrate=115200

firmwareのREADMEには、計測に使ったモデルのSHA-256(21067f5d78113f6c64a8720b05ff7e5c774dab0276797a522f81a6797253d97c)と、起動時に出るはずの診断(model: V=32768 D=96 L=6 H=4 F=66 P=128 / head staged int8: 2.53 MB / PSRAM free after alloc: ~5100 KB)が書かれています。自分のエクスポート結果がこのハッシュと一致するかで、同じ成果物を作れたかを確認できます。

再現できる範囲には境界がある

一方で、RESULTS.mdの数値をすべて追試できるわけではありません

学習済み成果物が無い:GitHub Releaseは0件、firmware/model/model.bingolden.txt.gitignore されており、チェックポイントも含まれません。焼くところから始めることはできず、学習から始める必要があります
アブレーションの生データが無い:RESULTS.mdが参照する runs/_archive_old_accounting/runs/*fix-d*runs/ ごと .gitignore されています。5アーム×2シードの数値を検証するには、同じ学習を自分で回すことになります
新規性の主張は本人の限定付き:READMEは「これほど小さなチップで試した人はいないと自分の知る限り(as far as I can tell)」という書き方をしており、断定はしていません
ハードウェアが要る:PSRAM付きのESP32-S3 N16R8が前提で、READMEに載っている数値はこのボードでの計測です

検証環境
本記事の確認日は2026年7月28日。編集部で実際に確認したのは、GitHub API(スター・フォーク・ライセンス・コミット履歴・リリース数)、リポジトリのファイルツリーとサイズ、.gitignore の内容、README.md / RESULTS.md / firmware/esp32_llm/README.md / src/budget.py / src/model.py / src/quantize.py / data/prepare.py の記述、および budget.py の式からのパラメータ数の再計算です。ESP32-S3実機での再現は行っておらず、本文中のtok/s・ms・帯域・perplexityはすべてRESULTS.mdに記録された作者の計測値です。

何が持ち帰れるのか

esp32-aiは「マイコンで実用的なAIアシスタントが動く」という話ではありません。TinyStoriesの物語しか書けないモデルであることは作者が最初に書いています。それでも読む価値があるのは、メモリ階層をアクセスパターンで分類し、その分類に合わせてアーキテクチャを選ぶという考え方が、規模を問わず使える形で提示されているからです。

エッジAIの実装では「速いメモリに載る大きさまでモデルを削る」ことが定石でしたが、esp32-aiが示したのは削らずに、毎トークン読む量だけを削るという別の解き方です。マイコン LLM という極端な制約下で成立するなら、同じ考え方は「ストレージは潤沢だがRAMが足りない」他の環境にも応用できるかもしれません——ただしこれは編集部の見立てであって、RESULTS.mdはマイコン以外のターゲットについて何も検証していません。そして前述のとおり、これは学習時からその構造を持たせた場合にのみ成立する話であって、既存モデルへ後付けできる最適化ではない点にも注意が必要です。

そして数字の扱い方そのものが参考になります。パラメータ計上のバグを見つけて数値を修正した経緯を残し、帯域の0.7%は「合成ベンチでのシェア」と限定し、58 tok/sは「上限であって実測ではない」と断り、小語彙の結果は「対照実験であって製品構成ではない」と書く——この徹底ぶりが、7日間で書かれた個人プロジェクトの主張を検証可能なものにしています。

参照ソース

slvDev/esp32-ai — 公式リポジトリ。README・ソースコード・ファームウェア(2026-07-28確認)
slvDev/esp32-ai — RESULTS.md — アブレーション・帯域実測・オンチップ生成・4bit量子化の一次記録
slvDev/esp32-ai — src/budget.py — 3層メモリ会計の実装。本記事のパラメータ再計算はこの式に基づく
slvDev/esp32-ai — MIT LICENSE追加コミット 74744182 — 2026-07-26のライセンス追加
Hacker News — Running a 28.9M parameter LLM on an $8 microcontroller — 281ポイント・70コメント(2026-07-26 03:59 JST投稿)
DaveBben/esp32-llm — 先行する独立プロジェクト。260Kパラメータ・19.13 tok/sの記載はこのREADMEより
Eldan & Li, TinyStories: How Small Can Language Models Be and Still Speak Coherent English? — 学習に使われたデータセットの原論文