mini-lab in five questions
What is it? An open-source AI lab, small enough to read. About 6,500 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-1 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 16 minutes on an Apple M5 Pro, or 36 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 29M tokens of stories and sums, and learns one skill: predict the next token.
Story (shortened)
Worksheet
After …they even played together, the model's top guesses:
right token: . → loss = −log 0.134 = 2.01
After 3,500 steps the base model writes little stories. It can't chat: it has never seen a conversation.
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 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
Add 3183 and 5.
All 8 attempts start with the same scratchpad, 3183+0005: 3+5=8, 8 … 3+0: 3+0=3, 3188, then:
The other 15 groups of the same step: 9 got 8 of 8 right (Compute 79+834), 6 got 0 of 8 (68277 + 56 = ?). Same reward for all 8 → every advantage 0 → skipped.
loss = −advantage × log p(token)averaged over every token the model wrote: +2.47 pushes the right try up, -0.35 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.
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 mostly during SFT and RL: learning to behave costs a little language. At the end of the story above, the RL model bets 99.6% 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 3 times more than input, which 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 × $0.50/M + 40 output tokens × $1.50/M
= 7 + 60 = 67 micro-dollars
Try it
git clone https://github.com/cthiriet/mini-lab && cd mini-lab
uv sync
bash speedrun.sh small # every stage: ~16 min on an M5 Pro, ~36 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.