Project Info
Inspiration
It's Valentines weekend and I'm not able to spend it with my girlfriend. Therefore, I decided to make her a funny little gift: a clone of myself (based on our 100k+ messages). I thought some kind of LLM would be a good place to start, and because she doesn't have much coding experience or access to hardware, I decided to make this LLM run on the cheapest computer around: the Raspberry-Pi Zero (512MB RAM, $15). I don't think you can get much smaller than this. On a broader scale, this project explores how small we can make language models so they can still learn domain specific knowledge, and what the bare minimum hardware is for serving these language models. I believe these to be important questions because they provide answers to how we can distribute LLMs at fraction of their cost today. This is a key step in democratizing AI technology.
What it does
I put myself in a Raspberry Pi! You plug in a Raspberry Pi containing an LLM to your computer and you can converse with it, not like a chatbot, but like an actual human: It will talk to you if you leave it alone for too long. It won't automatically respond to every message you send. This is achieved by: Training a 15M parameter LLM from-scratch on my text messages to mimic my conversations with my girlfriend. Creating the protocol to inference it (in C) on a bare-metal (no operating system) Raspberry-Pi Zero. Making a minimalistic messaging webapp (mostly just for demonstration purposes).
How we built it
1. Training the Model Data Preparation: Preparing 1.5 years of Facebook Messenger messages with my girlfriend. Synthetic Data: Generating synthetic conversation data using Gemini-2.0-Flash to make more training examples (~20k messages total). Pipeline: Using training pipelines from the llama2.c repository (Ack) to train a custom tokenizer and the model. Optimization: Quantizing the model to fp8 for inference speed. 2. Bare-Metal Inference Bootloader: Making bootloader code to quickly load the model into Pi RAM. Inference: Adapting inference code from llama2.c to be able to do forward passes on the Pi. Communication: Creating a UART communication protocol to actually simulate messaging someone with the model outputs.
Challenges we ran into
I can bucket the main challenge of this project into roughly 3 places: Model Goodness: Getting the model to produce coherent English and proper turn-based dialogue. Speed of Inference: Getting the model to run fast enough on the Pi. Connecting everything together: Handling hardware/software integration. Here are some things I found interesting for each: Skill Expression in Data: I tested extensively with training 15M and 42M parameter models with many different sweeps of hyperparameters. I ended up feeling like there was not much skill expression here. HOWEVER, I found a lot of skill expression in how the dataset was made. I cleaned 1.5 years of messenger data (80k messages). Initially, just sampling from true messages, I found the model was very incoherent until I refined the cleaning process. Inference Constraints: This was probably the biggest hurdle. I needed to throw out the idea of models larger than 15M parameters because it didn't make sense to inference them on the Pi. I ended up partially solving this by quantizing my models (into fp8) and training a custom tokenizer (vocab size 2048). One forward pass takes ~2.2s still D: Race Conditions: Many race conditions appeared regarding the user messages and how the Pi is supposed to respond, especially if user messages are sent while the Pi is in the middle of generating tokens. I solved this by developing a handshaking protocol that standardizes communication between the Pi and my computer.
Accomplishments we're proud of
Learning about LLMs and pulling everything together.
What we learned
This was my first time training an LLM. It was amazing to learn how developed the model training ecosystem really is, between having to write very little actual training code because PyTorch wraps everything so well, and having unbelievable insight into my training process with just a few lines of code using WandB. These tools really amazed me. Also, it was very cool to witness firsthand how my data turned randomly initialized weights into something with structure. On one hand, I understand the "black box" sentiment more now, because I literally can't comprehend that the model learned my data so well even though I intentionally chose the simplest training pipeline possible. On the other hand, it was cool to learn that I still have some control. For instance, it was really cool to see my validation loss curve invert itself when I added some dropout. I also learned a lot regarding how the Transformer actually works. One thing I learned that turns out to be of extreme significance to my project is how the KV cache is computed and saved. When turning an LLM into a chat application, I figured out that you can save a lot on inference time by checkpointing your KV cache (and not recomputing it needlessly). Ultimately there is probably a lot more hiding here in terms of unlocking faster inference. There are actually so many cool things I learned this weekend. Please come ask me about this if you want to hear more.
What's next
Model Training I was unable to download my Facebook Messenger data from the past year in time to train my model with this data. I feel like this is a significant chunk of conversational data I have that's real, so I'm eager to retrain once this finally downloads. I also wonder how training on a more general conversation dataset first and then fine-tuning with only certain layers unfrozen would change things. Also, I'm generally curious what the pitfalls were of my synthetic data generation and how much I can scale this. Speedier Inference There's definitely so much to unlock here. I did not rewrite much of Karpathy's original inference code to conform to my use case, and I am aware of the repo llama.cpp which has a lot of speedup tricks which I want to take a look at. In terms of Pi stuff, the Pi Zero does have a GPU (maybe not in the traditional sense but still), and maybe it will be faster at doing the matmuls. I feel like I still need a better understanding of how the transformer actually works, so it will be good to step through this after TreeHacks. Acknowledgements llama2.c repo - training codes, would not be possible without it. Eric Chen - guidance during model training process. Iris Nguyen - Idea + data :)
Ghost in the Machine
I put myself in a Raspberry Pi.
This is a 15M parameter LLM trained from scratch on my text messages with my girlfriend, running bare-metal (no operating system) on a Raspberry Pi Zero (512MB RAM, $15). You plug the Pi into your computer and chat with it through a messaging webapp. It'll message you if you leave it alone for too long, and it won't automatically respond to every message you send.
This is the public version of this repo (without my messages). If you want to see the full commit history or private data, come ask me.
Why
It's Valentine's weekend and I can't spend it with my girlfriend, so I made a clone of myself based on our 100k+ messages. I thought an LLM would be a good starting point, and because she doesn't have much coding experience or access to hardware, I wanted this to run on the cheapest computer around.
On a broader scale: how small can we make language models that still learn domain-specific knowledge, and what's the bare minimum hardware to serve them? These are important questions for distributing LLMs at a fraction of their current cost.
How It Works
Three pieces:
- Training: Fine-tune a 15M param Llama 2-style model on my text messages using PyTorch.
- Inference: Run the quantized (int8) model in C on a bare-metal Raspberry Pi Zero.
- Webapp: A messaging UI connected to the Pi over serial (UART) via a Python bridge.
Chat History → Tokenize → Fine-tune → Export → Bake into Pi kernel
↓
Raspberry Pi (bare metal)
↓ (serial @ 115200 baud)
Host PC (server.py)
↓ (WebSocket)
Browser (index.html)
Setup
Prerequisites
arm-none-eabi-gcc(for cross-compiling to Pi)- A Raspberry Pi Zero (or similar) with a serial connection to your host
1. Training the Model
Everything lives in llama2.c/.
Install dependencies:
cd llama2.c
pip install -r requirements.txt
Download a base model (pre-trained on TinyStories):
# 15M params (fits in Pi RAM after quantization)
wget https://huggingface.co/karpathy/tinyllamas/resolve/main/stories15M.pt
# 42M params (if you're not targeting the Pi)
wget https://huggingface.co/karpathy/tinyllamas/resolve/main/stories42M.pt -P finetunes
Prepare your data:
Put your cleaned chat history in private/chat_history_train.txt and private/chat_history_val.txt. Format is lines prefixed with I: (you) and M: (the other person):
I: hey what's up
M: not much just got home
I: nice want to get food
M: yes please I'm starving
Then tokenize and preprocess:
python chat_history.py pretokenize
python preprocess_chat.py --input_dir ../private --output_dir ../private
This creates binary token files and turn-offset indices so the training loop can sample from complete conversation turns (not random positions in the middle of a sentence).
Fine-tune:
On CUDA:
python finetune_msg.py \
--device=cuda \
--compile=True \
--batch_size=64 \
--warmup_iters=500 \
--learning_rate=1e-5 \
--dropout=0.1 \
--wandb_log=True \
--wandb_project=pi-ris \
--wandb_run_name=run_nn
On Apple Silicon (MPS):
python finetune_msg.py \
--device=mps \
--compile=False \
--batch_size=32 \
--wandb_log=True \
--wandb_project=pi-ris \
--wandb_run_name=run_nn
The model fine-tunes from a TinyStories checkpoint (stories15M.pt by default). To use 42M params instead, pass --ckpt_path=stories42M.pt. Output checkpoint lands in finetunes/ckpt.pt.
Test your model:
python sample.py \
--checkpoint=finetunes/ckpt.pt \
--device=cuda \
--compile=True \
--num_samples=5 \
--max_new_tokens=100 \
--start="I: Hello\nM:"
2. Deploying to the Pi
The bare-metal inference code lives in pi-baremetal/inference-server/.
Export your model to binary:
python export.py finetunes/ckpt.pt
This creates a quantized .bin file that the C inference engine can read.
Build the kernel image:
cd pi-baremetal/inference-server
make MODEL=path/to/your/quantized_model.bin
This does a few things:
- Compiles
main.c(protocol handler) andllama2.c(inference engine) with libpi - Uses
objcopyto bake the model weights and tokenizer directly into the kernel binary - Outputs
kernel.img(~17-18 MB)
Flash to SD card:
Copy kernel.img to your Pi's SD card (alongside bootcode.bin and start.elf from the Raspberry Pi firmware). The bootloader loads the kernel into RAM at 0x8000 and starts inference.
One forward pass takes ~2.2s on the Pi Zero. Not fast, but it works.
3. Running the Webapp
The webapp lives in webapp/.
Install dependencies:
cd webapp
pip install -r requirements.txt
Start the bridge:
python server.py /dev/ttyUSB0 115200
(Replace /dev/ttyUSB0 with your actual serial port — on Mac it's usually something like /dev/tty.usbserial-*.)
This starts:
- An HTTP server on
http://localhost:8080(serves the chat UI) - A WebSocket server on
ws://localhost:8765(real-time comms) - A serial bridge to the Pi
Open the chat:
Go to http://localhost:8080 in your browser. The UI mimics iMessage. It'll show a loading state until the Pi handshake completes, then you can start chatting.
Debug tool:
If something's weird with the serial connection:
python debug_serial.py # listen mode (hex + ASCII dump)
python debug_serial.py --send # handshake + send a test prompt
python debug_serial.py --raw # raw mode (no protocol)
Architecture Details
Model
- Llama 2-style transformer (from llama2.c)
- 288-dim, 6 layers, 6 heads
- Vocab size: 32,000 (Llama 2 SentencePiece tokenizer)
- Sequence length: 256 tokens
- Quantized to int8 for inference (4x smaller weights)
- Pre-trained on TinyStories, fine-tuned on ~20k chat messages
Bare-Metal Inference
- Pure C, no OS, no stdlib (custom libpi)
- Hardware floating point (VFP) enabled
- KV cache kept in float32 for quality, weights in int8 for size
- Model baked directly into kernel binary via
objcopy - Custom UART protocol with handshaking, cancel support, and retry logic
Communication Protocol
The Pi and host communicate over serial with a custom protocol:
- Handshake: Pi sends
READYevery 500ms until host responds withACK - Prompt: Host sends
STX + prompt + ETX, Pi responds withOK - Generation: Pi sends
TYPING→MSG:<text>→IDLEas it generates - Cancel: Host sends
CANbyte, Pi acknowledges and stops
The protocol handles race conditions (messages sent mid-generation) and retries (if the model doesn't produce a valid M: response).
Things I Learned
- There's way more skill expression in how you prepare data than in hyperparameter tuning. My model went from incoherent garbage to passable English mostly by cleaning the training data better.
- The KV cache is everything. You can checkpoint it between turns instead of recomputing from scratch, which saves a ton of inference time.
- Watching randomly initialized weights learn my actual speech patterns was wild. I still can't fully explain how the simplest possible training pipeline produced something that sounds like me.
- Dropout actually works. My val loss curve literally inverted itself when I added some.
What's Next
- More data: I couldn't download my last year of Messenger data in time. That's a significant chunk.
- Faster inference: Haven't really optimized the C code beyond quantization. The Pi Zero has a GPU of sorts that might be faster at matmuls. Also want to look at llama.cpp for tricks.
- Better training: Curious about pre-training on general conversation data first, then fine-tuning with frozen layers. Also want to understand the failure modes of my synthetic data generation.
Project Structure
pi-ris-public/
├── llama2.c/ # Training & model code
│ ├── finetune_msg.py # Fine-tuning script
│ ├── chat_history.py # Tokenization pipeline
│ ├── preprocess_chat.py # Turn-boundary preprocessing
│ ├── sample.py # Inference sampling (PyTorch)
│ ├── model.py # Transformer architecture
│ ├── export.py # Model export to binary
│ ├── train.py # Base training script
│ ├── run.c # Float32 C inference
│ └── runq.c # Int8 quantized C inference
│
├── pi-baremetal/ # Raspberry Pi bare-metal code
│ ├── libpi/ # Core Pi library (GPIO, UART, memory)
│ ├── bootloader/ # Bootstrap loader
│ └── inference-server/ # LLM inference on bare metal
│ ├── main.c # Protocol handler + inference loop
│ ├── llama2.c # Quantized inference engine (C)
│ └── Makefile # Builds kernel.img
│
├── webapp/ # Web UI + serial bridge
│ ├── server.py # Async serial-WebSocket-HTTP bridge
│ ├── index.html # iMessage-style chat UI
│ └── debug_serial.py # Serial debugging tool
│
└── private/ # Your chat data goes here (gitignored)
├── chat_history_train.txt
└── chat_history_val.txt
Acknowledgements
- llama2.c — training and inference code. This project would not be possible without it.
- Eric Chen — guidance during the model training process.
- Iris Nguyen — the idea + the data :)
Analysis
View
Metric
- 2
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- CIn code
- FastAPIIn code
- HTMLIn code
- Hugging FaceIn code
- PythonIn code
- PyTorchIn code
- StreamlitIn code
7 of 7 appear in the indexed code.
AI coding agents
No AI coding agent signals were found in this repository.
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
558 KB
Source files
150
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
matthewover137/pi-ris-public
296 files · 2.8 MB · @ c865b72
Structure
Interface
1 file · 0%Screens, components and styles rendered to the user.
Application logic
249 files · 84%Domain rules, services and shared utilities.
+2 more
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- C60%
- Python22%
- Markdown9%
- YAML6%
- HTML3%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
llama2.c/wandb/run-20260214_053939-ps1xwflu/files/requirements.txt
pypi · 176- altair
- annotated-types
- antlr4-python3-runtime
- anyio
- appnope
- arviz
- asttokens
- attrs
- autogluon.common
- autogluon.core
- autogluon.features
- autogluon.tabular
- backoff
- beautifulsoup4
- blinker
- boto3
- botocore
- cachetools
- +158 more
llama2.c/wandb/run-20260214_055907-3vq9dqfq/files/requirements.txt
pypi · 176- altair
- annotated-types
- antlr4-python3-runtime
- anyio
- appnope
- arviz
- asttokens
- attrs
- autogluon.common
- autogluon.core
- autogluon.features
- autogluon.tabular
- backoff
- beautifulsoup4
- blinker
- boto3
- botocore
- cachetools
- +158 more
llama2.c/requirements.txt
pypi · 7- numpy
- pytest
- Requests
- sentencepiece
- torch
- tqdm
- wandb
webapp/requirements.txt
pypi · 2- pyserial
- websockets
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
This project’s features have not been analysed yet.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.
