EngineeringIngénierie

Running an LLM on ESP32 and STM32 with a bit-exact C engineFaire tourner un LLM sur ESP32 et STM32 avec un moteur C bit-exact

2026-06-03 · Tilelli Lab · 6 min read6 min de lecture

Plenty of projects claim they “ported the model to C.” Far fewer test the one thing that matters: does the C output match the Python reference, token for token? Atome makes that a test rather than a promise, and that guarantee is what turns a microcontroller demo into something you can actually certify and ship.Beaucoup de projets affirment avoir « porté le modèle en C ». Bien peu testent la seule chose qui compte : la sortie C correspond-elle à la référence Python, token par token ? Atome en fait un test plutôt qu'une promesse, et cette garantie est ce qui transforme une démonstration sur microcontrôleur en quelque chose que l'on peut réellement certifier et livrer.

One engine, two chipsUn moteur, deux puces

The Atome inference engine is plain C99 with a fixed-shape block: a LayerNorm, a ternary depthwise convolution, a diagonal state-space model, a top-k attention, and a router, in that order. It targets ESP32 (the dual-core Xtensa LX7 with on-chip SRAM) and STM32 / Cortex-M parts identically because it makes no assumptions about an FPU, a cache, or an operating system. The same source compiles for a $2 STM32F103 and a $5 ESP32-S3; only the compile-time configuration changes.Le moteur d'inférence d'Atome est du C99 pur, avec un bloc à forme fixe : une LayerNorm, une convolution ternaire depthwise, un modèle d'état diagonal (SSM), une attention top-k et un routeur, dans cet ordre. Il vise l'ESP32 (le double cœur Xtensa LX7 avec SRAM interne) et les STM32 / Cortex-M de façon identique, car il ne suppose ni FPU, ni cache, ni système d'exploitation. Le même code se compile pour un STM32F103 à 2 $ et un ESP32-S3 à 5 $ ; seule la configuration à la compilation change.

The parity contractLe contrat de parité

Because the PyTorch model and the C engine are built to the same fixed block shape, the test suite can require that they produce identical numbers. End-to-end Python-to-Cortex-M3 parity under QEMU is max |Δ| = 3.7×10⁻⁷, which is floating-point epsilon. Multi-token generation parity is exact: 48 of 48 tokens on the 60K model and 16 of 16 on the 944K model. The full suite is 146 tests, green at HEAD.Parce que le modèle PyTorch et le moteur C sont construits sur la même forme de bloc fixe, la suite de tests peut exiger qu'ils produisent des nombres identiques. La parité de bout en bout entre Python et Cortex-M3 sous QEMU est de max |Δ| = 3,7×10⁻⁷, soit l'epsilon de la virgule flottante. La parité de génération multi-token est exacte : 48 tokens sur 48 pour le modèle 60K et 16 sur 16 pour le modèle 944K. La suite complète compte 146 tests, tous au vert sur HEAD.

Why bit-exactness matters for shippingPourquoi la bit-exactitude compte pour livrer

If the chip can silently diverge from your reference implementation, you cannot certify the device's behavior. Bit-exact parity means the behavior you validated in Python is the behavior that runs on the board — which is exactly what regulated domains (medical, industrial, automotive) require, where “probably the same” is not an acceptable answer. It also makes debugging tractable: a mismatch is a located bug, not a floating-point mystery to chase across two languages.Si la puce peut diverger silencieusement de votre implémentation de référence, vous ne pouvez pas certifier le comportement de l'appareil. La parité bit-exacte signifie que le comportement validé en Python est celui qui s'exécute sur la carte — précisément ce qu'exigent les domaines réglementés (médical, industriel, automobile), où « probablement pareil » n'est pas une réponse acceptable. Elle rend aussi le débogage tractable : un écart est un bug localisé, pas un mystère flottant à poursuivre entre deux langages.

Memory and flash, per chipMémoire et flash, par puce

The configuration you can run depends on the part's SRAM. From the repository's measured table: an STM32F103 (20 KB SRAM) runs the small classifier configs; an RP2040 (264 KB) runs the 64-dimension story model at about 104 KB of RAM; the 944K “prod” configuration needs a 512 KB part such as an STM32F7 or an ESP32-S3. Flash is rarely the limit — the packed weights plus the engine are tens to hundreds of kilobytes, well within a typical 512 KB to 4 MB flash.La configuration exécutable dépend de la SRAM du composant. D'après la table mesurée du dépôt : un STM32F103 (20 Ko de SRAM) exécute les petites configs de classifieur ; un RP2040 (264 Ko) exécute le modèle d'histoires en dimension 64 à environ 104 Ko de RAM ; la configuration « prod » 944K nécessite un composant de 512 Ko comme un STM32F7 ou un ESP32-S3. Le flash est rarement la limite — les poids empaquetés plus le moteur font de quelques dizaines à quelques centaines de kilooctets, largement dans un flash typique de 512 Ko à 4 Mo.

One honest boundaryUne limite honnête

The parity guarantee holds between the Python reference and the C engine. The in-browser playground on this site runs a separate JavaScript reimplementation of the forward pass — same weights, but not covered by the bit-exact guarantee, since floating-point order can differ in JavaScript. And all Cortex-M3 numbers are measured under QEMU; we have not yet flashed a physical board and measured joules per token. When we do, we will publish it with the same candor as everything else.La garantie de parité tient entre la référence Python et le moteur C. Le playground intégré au site exécute une réimplémentation JavaScript distincte de la passe avant — mêmes poids, mais non couverte par la garantie bit-exacte, car l'ordre des opérations flottantes peut différer en JavaScript. Et tous les chiffres Cortex-M3 sont mesurés sous QEMU ; nous n'avons pas encore flashé de carte physique ni mesuré les joules par token. Quand ce sera fait, nous le publierons avec la même franchise que le reste.

From PyTorch to a flashable blobDe PyTorch à un blob flashable

The path from a trained model to something a microcontroller runs is short and explicit. You train in PyTorch, export the ternary weights to the ATOME01 packed format — four trits per byte in base-3 — and the C engine loads that blob directly with no conversion at boot. Because the engine's block structure mirrors the PyTorch module exactly, there is no translation layer that could introduce a discrepancy; the same operations happen in the same order. That is what makes the bit-exact parity guarantee possible in the first place: the two implementations are not approximations of each other, they are the same computation expressed twice.Le chemin d'un modèle entraîné à quelque chose qu'un microcontrôleur exécute est court et explicite. Vous entraînez dans PyTorch, exportez les poids ternaires vers le format empaqueté ATOME01 — quatre trits par octet en base 3 — et le moteur C charge ce blob directement, sans conversion au démarrage. Parce que la structure de bloc du moteur reflète exactement le module PyTorch, il n'y a pas de couche de traduction susceptible d'introduire un écart ; les mêmes opérations se produisent dans le même ordre. C'est ce qui rend possible, en premier lieu, la garantie de parité bit-exacte : les deux implémentations ne sont pas des approximations l'une de l'autre, elles sont le même calcul exprimé deux fois.

Choosing between ESP32 and STM32Choisir entre ESP32 et STM32

The two families suit different products. STM32 parts span a huge range, from the 20 KB Blue Pill that runs only the smallest classifier configs to the 512 KB STM32F7 that hosts the full 944K model, so they are ideal when you want to pick exactly the right cost and size point. The ESP32-S3 brings 512 KB of SRAM plus built-in wireless, which is useful when you want an on-device model for the privacy-sensitive, low-latency path and optional connectivity for everything else. Either way the engine code is the same; you choose the chip by the configuration your task needs and the peripherals your product wants, then confirm the fit against the measured RAM table before committing.Les deux familles conviennent à des produits différents. Les STM32 couvrent une gamme énorme, de la Blue Pill à 20 Ko qui n'exécute que les plus petites configs de classifieur au STM32F7 à 512 Ko qui héberge le modèle complet 944K, ce qui les rend idéaux quand vous voulez choisir exactement le bon point de coût et de taille. L'ESP32-S3 apporte 512 Ko de SRAM plus du sans-fil intégré, utile quand vous voulez un modèle embarqué pour le chemin sensible à la confidentialité et à faible latence, et une connectivité optionnelle pour le reste. Dans les deux cas, le code du moteur est le même ; vous choisissez la puce selon la configuration dont votre tâche a besoin et les périphériques que votre produit veut, puis vous confirmez la compatibilité face à la table RAM mesurée avant de vous engager.

Testing the port before you trust itTester le portage avant de lui faire confiance

Bit-exact parity is only worth anything if you actually run the checks, so the workflow ends with verification rather than assumption. The repository ships a parity test that compares the C engine's output against the PyTorch reference, a multi-token test that confirms generation stays identical across a sequence, and a QEMU test that runs the Cortex-M3 build itself; all of it is part of the 146-test suite that must pass before a change is considered done. When you bring the engine up on a new part, the right move is to reproduce these checks in your environment first, confirm the numbers match, and only then build features on top. A mismatch at that stage is a located bug you can fix; a mismatch discovered in the field, after you assumed the port was faithful, is far more expensive. The discipline is simple: verify parity on the chip before you trust the chip.La parité bit-exacte ne vaut quelque chose que si vous lancez réellement les vérifications, le flux se termine donc par la vérification plutôt que par la supposition. Le dépôt fournit un test de parité qui compare la sortie du moteur C à la référence PyTorch, un test multi-token qui confirme que la génération reste identique sur une séquence, et un test QEMU qui exécute la compilation Cortex-M3 elle-même ; tout cela fait partie de la suite de 146 tests qui doit passer avant qu'un changement soit considéré comme terminé. Quand vous démarrez le moteur sur un nouveau composant, le bon réflexe est de reproduire ces vérifications dans votre environnement d'abord, de confirmer que les chiffres correspondent, et seulement ensuite de bâtir des fonctionnalités par-dessus. Un écart à ce stade est un bug localisé que vous pouvez corriger ; un écart découvert sur le terrain, après avoir supposé le portage fidèle, coûte bien plus cher. La discipline est simple : vérifiez la parité sur la puce avant de faire confiance à la puce.

Bottom lineEn résumé

The same portable C99 engine runs on an ESP32 and across the STM32 range because it assumes no FPU, cache or operating system; you choose the chip by the configuration your task needs and confirm the fit against the measured RAM table. The export path from PyTorch to a flashable ATOME01 blob is direct, and the bit-exact parity tests are what let you trust that the chip behaves exactly like your reference. Bring the engine up, reproduce the parity checks in your environment, and only then build features on top. Verify parity on the chip before you trust the chip.Le même moteur C99 portable tourne sur un ESP32 et sur toute la gamme STM32 parce qu'il ne suppose ni FPU, ni cache, ni système d'exploitation ; vous choisissez la puce selon la configuration dont votre tâche a besoin et confirmez la compatibilité face à la table RAM mesurée. Le chemin d'export de PyTorch vers un blob ATOME01 flashable est direct, et les tests de parité bit-exacte sont ce qui vous permet de faire confiance au fait que la puce se comporte exactement comme votre référence. Démarrez le moteur, reproduisez les vérifications de parité dans votre environnement, et seulement ensuite bâtissez des fonctionnalités par-dessus. Vérifiez la parité sur la puce avant de faire confiance à la puce.

Frequently asked questionsQuestions fréquentes

Can an ESP32 run a language model?Un ESP32 peut-il faire tourner un modèle de langue ?

Yes — an ESP32-S3 has 512 KB of SRAM, enough for Atome's larger configurations, and the heap-free C99 engine runs without an OS. Smaller configs also run on STM32 parts down to about 20 KB of SRAM.Oui — un ESP32-S3 a 512 Ko de SRAM, assez pour les configurations plus grandes d'Atome, et le moteur C99 sans tas fonctionne sans système d'exploitation. Les petites configs tournent aussi sur des STM32 jusqu'à environ 20 Ko de SRAM.

What does bit-exact Python-to-C parity mean?Que signifie la parité bit-exacte entre Python et C ?

It means the C engine produces the same outputs as the PyTorch reference, verified by tests — max |Δ| = 3.7×10⁻⁷ end-to-end and exact multi-token generation. The behavior you validate is the behavior that ships.Cela signifie que le moteur C produit les mêmes sorties que la référence PyTorch, vérifiées par des tests — max |Δ| = 3,7×10⁻⁷ de bout en bout et génération multi-token exacte. Le comportement validé est celui qui est livré.

← All posts← Tous les articles Source & data on GitHubCode & données sur GitHub