mini-lab in five questions
What is it? An open-source AI lab, small enough to read. About 6,900 lines of Python train a language model from scratch, serve it behind an OpenAI-compatible API, and bill every token from prepaid credits.
Why build it? A frontier lab is too big to see, and most tutorials stop once the model trains. mini-lab keeps going to the API and the bill, so you can follow the whole chain in one repo.
What can its model do? mini-3 has 5.8M parameters. It tells short children's stories and adds numbers, step by step or with a calculator. It knows nothing else, and says so. It is tiny on purpose: every training stage moves a number you can see.
What do I need? A laptop, no GPU. The whole training takes about 31 minutes on an Apple M5 Pro, or about 70 on its CPU alone.
Can I try it? Yes: on the live demo, or on your laptop with four commands (at the end of this post).

The chain
Every lab, big or small, runs the same chain. The rest of this post walks it, one diagram per step.
Research · once per model
Serving · every request
Tokens
A model reads numbers, not letters. The tokenizer has 4,096 pieces of text, called tokens, most of them learned from the data, and turns any text into their ids.
41 characters → 11 tokens
17 characters → 11 tokens
Pretraining: guess the next token
The first training stage reads 57M tokens of stories and sums, and learns one skill: predict the next token.
Story (shortened)
Worksheet
After …the best day ever!<|bos|>, the model's top guesses:
right token: One → loss = −log 0.151 = 1.89
After 7,000 steps the base model writes little stories. It can't chat: it has never seen a conversation.
How long should it read? Labs ask a scaling law, and mini-lab ran a small one: four model sizes, each trained with the same compute. At mini-2's budget, a smaller model won: mini was a little too big for what it read. At twice the budget, mini's own size won. So mini-3 kept the model and read twice as much.
Midtraining: learn the chat format
Same loss, new documents: chats, mixed with old text so the model keeps its language. The model learns the format here, from a lot of data, so the next stage needs only a little.
A calculator call · 50 tokens
SFT: learn to behave
SFT (supervised fine-tuning) uses a small, fixed set of 8,000 conversations with what midtraining never shows: system prompts, follow-ups, polite ways to say no. The loss changes too: it now skips every token the model will never have to write.
System prompt · loss on 17 of 33 tokens
Follow-up · loss on the last answer only (22 of 47)
RL: practice with a grader
SFT can only copy answers someone wrote. RL (reinforcement learning) needs no answers, only a program that checks them. mini-lab's RL practices math and nothing else: sums of up to 5 digits, where chat data stops at 3. It uses GRPO: the model tries each problem 8 times, the program grades every try, and each try gets pushed up or down by how it compares to the group.
Prompt
Compute 5+8921
The same step has 15 other groups. 4 got 8 of 8 right (Compute 79+834), 9 got 0 of 8 (68277 + 56 = ?). Same reward for all 8 → every advantage 0 → skipped. Only 2 split, like this one.
loss = −advantage × log p(token)averaged over every token the model wrote: +0.72 pushes each right try up, -1.21 pushes each wrong one down
RL hits exactly the target you give it, never the one you meant:
| The grader checked | RL learned |
|---|---|
| The answer starts with "Sure!" | Sure! Bye! Come back for another story soon., for every request |
| A correct calculator call is in the turn | Write the call, then invent the tool's result |
| The last number is right | What is 4521 + 380? → 451 + 0 = 4901: right sum, broken question |
Each fix made the grader stricter.
Distillation: two teachers, one student
Practice on math alone makes a specialist, and a specialist forgets what it stopped practicing. Told to answer with the number only, it obeys 47% of the time, down from 100% after SFT. So labs now split the work: one specialist per skill, trained by RL, then a single student taught by all of them.
mini-lab has two teachers: the math specialist for sums, the SFT model for everything else. The student starts as a copy of the SFT model and answers on its own, mistakes included. The teacher reads those same tokens and grades each one. RL gives one number per answer. Distillation gives one per token.
Prompt
Compute 5+8921
The student's answer · RL reward 0
loss = KL(student ‖ teacher) at every token3 tokens of 61 carry 99.6% of it
On everything but math, the teacher is the SFT model itself. The student starts as its copy, so there the loss starts at zero, and only grows if the student drifts.
Eval: one test, every stage
Run the same questions after every stage, and you see what each one taught and what it broke.
The last column, perplexity on new stories, counts roughly how many tokens the model hesitates between. It rises most at SFT: learning to behave costs a little language. The specialist pays a little more, and distillation gives it back. At the end of the story above, the base model bets 99.7% on <|bos|>: a new text starts. mini-3 bets 84% on <|assistant_end|>. It expects a chat, not a new story.
Inference: one model, many users
Prefill reads the whole prompt in one step and picks the first token. Decode then adds one token per step.
Without a cache
tokens computedWith a KV cache
tokens computedOn a model this small, a decode step costs about the same for 1 request or 8. An empty slot wastes it, so the engine keeps every slot busy.
Static batching
10 steps · 10 idleContinuous batching
8 steps · 4 idleThe API: bill every token
The inference engine never faces the internet. A gateway stands in front of it, speaks the OpenAI API, and bills. Output costs 5 times more than input, as at the big labs, and it matches the work: the whole prompt goes through in one step, but each output token needs its own.
- 1Authenticatesha256(key) → org, balance→401
- 2Rate limit60 requests / min per key (default)→429
- 3Validaterequest body and model→400 · 404
- 4Check creditsbalance > 0, key under its limit→429
- 5Reserve tokens40,000 tokens / min per key (default)→429
- 6Generateinternal call to the inference engine
- 7Billone transaction: log + debit + key spend
"What is 12 + 30?" → scratchpad + "The answer is 42."
14 prompt tokens × $10/M + 40 output tokens × $50/M
= 140 + 2,000 = 2,140 micro-dollars
Try it
git clone https://github.com/cthiriet/mini-lab && cd mini-lab
uv sync
bash speedrun.sh small # every stage: ~31 min on an M5 Pro, ~70 min CPU-only
./scripts/serve.sh # inference, API and platform
Or try the live demo, where billing runs in test mode. The training notes go deeper into every stage, and every reward hack.