int-llm / README.md
nmicic's picture
Publish Q16.48 tiny-GPT reference model and validation card
9d480ec verified
|
Raw
History Blame Contribute Delete
8.88 kB
---
license: apache-2.0
language: en
tags:
- tinyml
- embedded
- microcontroller
- cortex-m
- risc-v
- xtensa
- rp2040
- esp32
- nrf52840
- avr
- fixed-point
- integer-only
- deterministic
- reproducibility
- quantization
- gpt
- character-level
- custom-code
---
# int-llm `model.mgw` β€” an integer-only GPT that runs bit-identically from x86 servers down to 32-bit microcontrollers, its arithmetic verified down to an 8-bit AVR
This repo hosts the committed reference weights of
**[int-llm](https://github.com/nmicic/int-llm)**: a tiny character-level GPT
trained and sampled **entirely in Q16.48 fixed-point integer arithmetic** β€”
no `float`, no `double`, no `libm` anywhere in the compute path β€” with
**byte-reproducible** results across every tested platform and compiler.
> **Note:** this is a custom C artifact in the project's own `.mgw` container β€”
> **not** a Transformers/safetensors checkpoint. It cannot be loaded with
> `AutoModel` or run on hosted inference; it is consumed by the C programs in
> the GitHub repo below. It is also **not** the TinyLlama-derived inference
> weights discussed in the int-llm write-up β€” this repository publishes only
> the small character-level reference GPT used throughout the project.
Background and motivation: [the int-llm blog post](https://huggingface.co/blog/nmicic/int-llm).
## The file
| | |
|---|---|
| File | `model.mgw` β€” 115,576 bytes |
| sha256 | `466cfe9dba7b888cdaa23dedf4b10351826795793448c8e95dcb0f7a61ed33eb` |
| Model | character-level GPT: `N_EMBD=32, N_HEAD=4, N_LAYER=1`, 14,272 parameters (9 weight tensors) |
| Weights | MGW v1 indexed container; 9 `int64_t` Q16.48 weight tensors plus tokenizer alphabet and RNG state |
| Training | 5000 steps on the public *makemore* names dataset, integer-only (`./gpt_int --save model.mgw`) |
| Math contract | determinism-gate golden hash `c0d933ea340452ec` β€” reproduced by every backend and every validated target below, down to an 8-bit AVR |
| License | Apache-2.0 |
The `.mgw` format (MGW v1) is a small indexed container: a 64-byte header
(magic, version, endianness rejection tag), a 64-byte config block, a tensor
index, the nine weight matrices as raw `int64_t` Q16.48 values, and two
bookkeeping tensors β€” the tokenizer alphabet and the sampler's RNG state
(11 stored tensors in all: 9 learned + 2 bookkeeping).
There is nothing to dequantize: the model was **trained directly in Q16.48**,
so floating-point weights never existed for this model. It can be loaded
from a file (`--load model.mgw`) or **zero-copy from memory**
(`mgpt_load_mem()`), which is how microcontrollers run it straight out of
memory-mapped flash without ever copying the weights into RAM.
## Why host 115 KB of weights?
First, because the size is the point: at 115 KB the *entire model* β€” not a
distilled or re-quantized derivative β€” bakes into the flash of most
mainstream MCUs alongside the code that runs it. Even a 256 KB-flash part
holds both with room to spare (measured firmware sizes below), and on
XIP-capable chips the weights are read in place, occupying zero RAM.
Second, because the file itself is a claim. This *exact byte sequence* is
independently reproduced by running the full 5000-step training on four
different hosts β€” arm64 macOS (clang), x86-64 AMD (gcc), x86-64 Intel (gcc),
and a 2014 Raspberry Pi 1 B+ (32-bit ARMv6, a CPU with no `__int128`) β€” and
every validated inference run on the targets below reproduces the training
host's 20 sampled names **byte-for-byte, PRNG stream included**. Most model
files are "weights we happened to save"; this one is a fixed point (pun
intended) you can re-derive from source.
That makes it useful as an **oracle** for anyone working on quantization,
numerical drift, regression testing, or deterministic inference: any
deviation from it is an implementation bug, never rounding ambiguity β€”
within the documented arithmetic and file-format contract, there is no
floating-point tolerance to hide behind.
## Validated on real hardware
The train-big/run-small loop closes on microcontrollers: train on a laptop,
run inference-only from this weight file on a $5 board. Every target below
passed both checks (determinism-grid golden hash + byte-identical 20-sample
inference), except the 8-bit Mega 2560 β€” a board over a decade old β€” which
runs the determinism gate only (8 KB RAM), computing the same 64-bit hash
from 8-bit ALU instructions; raw provenance-stamped transcripts live in
[`validation/cpu/`](https://github.com/nmicic/int-llm/tree/main/validation/cpu).
| target | ISA | determinism | 20 samples |
|---|---|---|---|
| XIAO RP2040 (Cortex-M0+ @ 133 MHz) | Armv6-M | 10.9 s | 6.7 s |
| Raspberry Pi Pico 2 (RP2350, ARM mode, Cortex-M33) | Armv8-M | 4.1 s | 3.1 s |
| Raspberry Pi Pico 2 (RP2350, RISC-V mode, Hazard3) | rv32imac | 5.4 s | 3.8 s |
| ESP32-C6 | rv32imac | 5.6 s | 2.0 s |
| Heltec V3 (ESP32-S3, LX7 @ 240 MHz) | Xtensa | 3.9 s | 1.1 s |
| LILYGO T-Beam (ESP32, LX6 @ 240 MHz) | Xtensa | 4.2 s | 2.6 s |
| XIAO nRF52840 (Cortex-M4F @ 64 MHz) | Armv7E-M | 13.9 s | 3.4 s |
| Arduino MKR Zero (SAMD21, Cortex-M0+ @ 48 MHz, 256 KB flash / 32 KB RAM) | Armv6-M | 37.2 s | 26.4 s |
| Arduino Mega 2560 (ATmega2560, 8-bit AVR @ 16 MHz, 8 KB RAM) | AVR | 747.9 s | n/a (8 KB RAM) |
| Raspberry Pi 1 B+ (2014, ARMv6, 32-bit Linux) | ARMv6 | 0.7 s | 0.2 s |
| AMD Ryzen 7 7700 (Linux, gcc) | x86-64 | native + portable | byte-identical |
| Intel Core i7-7700 (Linux, gcc) | x86-64 | native + portable | byte-identical |
Same die, two ISAs: the Pico 2 reproduces the identical output in both its
ARM and RISC-V boot modes. The Linux rows additionally rerun the **full
training** and reproduce this repo's `model.mgw` byte-for-byte.
**Footprint** (XIAO RP2040): the complete firmware is
187,932 B of flash β€” *including* the whole 115 KB weight file baked into
rodata β€” and 15,268 B of static RAM (5.8% of the RP2040's 264 KB). Weights
are read in place over XIP flash; RAM holds only KV cache, activations, and
the USB stack. The 256 KB claim is measured, not extrapolated: on the
Arduino MKR Zero's SAMD21 (256 KB flash / 32 KB RAM), code plus the whole
weight file build to 133,472 B β€” 50.9% of flash β€” with 13,124 B of RAM.
## Run it
```bash
git clone https://github.com/nmicic/int-llm
cd int-llm
make gpt_int
./gpt_int --load model.mgw # 20 names, byte-identical on every validated target
```
The identical file is committed in the GitHub repo; to fetch just the
weights from here instead:
```bash
hf download nmicic/int-llm model.mgw --local-dir .
# for strict reproducibility, pin a revision once published:
# hf download nmicic/int-llm model.mgw --revision <commit-or-tag> --local-dir .
```
To retrain and verify the reproducibility claim yourself:
```bash
make input # fetch the names dataset
./gpt_int --save model2.mgw # ~2 s on a desktop, ~10 min on a Pi 1
cmp model2.mgw model.mgw # exit 0
```
The MCU harnesses (PlatformIO, one folder per board, flash + serial-capture
scripts) are in
[`validation/cpu/`](https://github.com/nmicic/int-llm/tree/main/validation/cpu).
## Intended use
This artifact is intended for: validating integer-only or fixed-point
implementations against an exact reference, regression testing, deterministic
inference research, and embedded/TinyML experiments. It is **not** intended
for production language generation, hosted inference, use with the
Transformers library, or capability comparisons against modern LLMs.
## Limitations & provenance
- **"Integer-only" refers to the model compute path**: training, inference,
sampling, and serialization are all Q16.48 integer arithmetic. Build
tooling, logging, and timing on the host are outside that claim.
- This is a **toy by design**: a character-level *name generator*, not a
general text LLM. `BLOCK_SIZE` is 8, so generated names are at most
8 characters. Its value is the methodology β€” exact integer arithmetic and
cross-platform byte-reproducibility β€” not the language modeling.
- Training data: [`names.txt` from Andrej Karpathy's makemore](https://github.com/karpathy/makemore)
(MIT license), fetched by `make input`;
sha256 `0a30b5557f192f32ab962680889aac5f6fda0f4cecf40a6d0b5694f58ea8cc4d`.
- MGW v1 is a **host-native-endian** format with an endianness rejection tag
in the header; this published file is **little-endian** (as are all
validated targets).
- The loader is a research-grade parser, not hardened against adversarial
inputs β€” verify the sha256 above and load only trusted `.mgw` files.
## Links
- Source, math library, and validation records: **https://github.com/nmicic/int-llm**
- Write-up: **https://huggingface.co/blog/nmicic/int-llm**
- The portable Q16.48 dual-backend math header matured in the sibling project **[astro-nav-int](https://github.com/nmicic/astro-nav-int)**