Build a language model on your Mac with Language Model Builder

By

Updated

Tagged: AI

A complete, measured guide to Language Model Builder: plan the run, pick a blueprint and tokenizer, pre-train a small GPT on your Mac, read the loss, sample checkpoints, then fine-tune with SFT and DPO.

Language Model Builder is a free macOS app that teaches you how language models work, then lets you train one yourself.

Everything happens on your Mac. You pick the data, watch the training run, fine-tune the result, and chat with the model you made. No Python, no cloud GPU, no machine learning background needed.

I really like this idea. Language models feel a lot less magical once you’ve watched each part do its job.

This guide goes through the whole thing with real runs and real numbers. There are two models in it:

  • TinyStories GPT-2, the 19.3-million-parameter model I trained on my M4 Pro on the TinyStories 2 dataset. I took its training history, checkpoints and samples straight from the project folder, then fine-tuned it with SFT and DPO.
  • A 2.1-million-parameter model I trained for 7 minutes on the bundled Tiny Shakespeare text, to see what a tiny run looks like from the first step to the last, and what overfitting looks like when it happens.

If you want the theory behind each stage explained for software engineers, read How to build an LLM from scratch next to this one. Here we stay close to the app and to what it measured.

Language Model Builder welcome screen with options to learn about models, build a model, or open an existing project

What is Language Model Builder?

Language Model Builder is an educational app created by Felix Rieseberg.

It has two parts. The first is an interactive introduction to language models. It takes about 90 minutes and covers next-token prediction, tokenization, embeddings, attention, transformers, training data, gradient descent, and fine-tuning.

The second is a native workbench where you put those ideas to work. You build a tokenizer, pick a dataset, start a training run, look at samples along the way, and talk to the finished model.

On the default path you create a small model from scratch and train its weights yourself, instead of downloading a finished chatbot and renaming it.

The whole run follows one chain. Every section of this guide is one link of it:

The Language Model Builder pipelineText becomes tokens, tokens become training examples, training updates the parameters and saves checkpoints, checkpoints produce samples, then SFT and DPO shape the behavior before chat and evaluation.Text datasetTokenizer: text totoken IDsPre-training:predict the nexttokenCheckpoints and losscurvesSampling: generatetext from acheckpointSFT: learn theassistant turnDPO: prefer chosenanswersChat, compare, writethe report

What do you need?

You need an Apple Silicon Mac running macOS 15 or later. The app uses MLX, Apple’s machine learning framework for Apple Silicon, so Windows, Linux, and Intel Macs are out for now.

The app is free, with no account and no subscription. Download the current build from the official website or from the GitHub releases. The runs in this guide used version 1.2.3.

Plan for disk space. The app is 40 MB, but my TinyStories project folder holds 3.3 GB: 2.2 GB of downloaded text, a 1.1 GB token cache, and about 150 MB of checkpoints.

It also runs inside a macOS virtual machine. I ran the Tiny Shakespeare model and the fine-tuning in a Sequoia VM on the same M4 Pro, where the app reports the GPU as a “Paravirtual device” and calibrates its estimates for it. It’s slower than the host, but it works.

Plan the experiment before you press Start

Training takes long enough that you want to know what you’re measuring before you begin.

Write one hypothesis, a few fixed prompts, and a short run manifest. Here’s the manifest for my TinyStories run, filled in from the project’s own files:

Run ID:        tinystories-gpt2
Mac:           M4 Pro, app 1.2.3
Tokenizer:     GPT-2 byte-level BPE, 50,261 tokens
Dataset:       TinyStories 2 (2.2 GB train, 19 MB validation)
Training data: 564,033,192 tokens
Model shape:   512 context, 256 wide, 8 blocks, 4 heads
Parameters:    19,289,344
Batch:         32 sequences of 512 tokens
Learning rate: 5e-4 peak, 200 warmup steps, cosine decay
Prompts:       "Once upon a time", "Tell me a story about a brave little boat."

Every value in it matters later. The tokenizer decides what a loss number means, the batch and context decide how many tokens one step is, and the prompts are what you compare checkpoints with.

Keep observations separate from explanations in your notes. “Validation loss rose after step 600” is an observation. “The model started memorizing” is an explanation that needs a test.

How to build an LLM from scratch has a full model-lab.md template if you want one.

Start with the interactive textbook

My advice is to resist the temptation to start training right away. Go through the introduction first. The lessons build the mental model you need to make sense of the training screens later.

It starts from one idea: a language model predicts what comes next. Then it walks through the pieces that make this possible.

Language Model Builder interactive textbook with its 16 chapters listed in the sidebar

Tokens and token IDs

A model receives text as a list of numbers called tokens, not as words.

A tokenizer splits text into pieces and maps each piece to an integer ID. The model sees the IDs, looks up a learned vector for each one, and predicts the ID that comes next.

Language Model Builder offers two tokenizers:

  • Fast 10K BPE (recommended): a 10,000-token byte-level BPE vocabulary trained on your own dataset.
  • GPT-2 BPE: the classic 50,257-token GPT-2 vocabulary plus four reserved chat tokens, 50,261 in total.

Both reserve special tokens for chat: <|system|>, <|user|>, <|assistant|> and <|end|>. Ordinary text can never produce them, which is what lets fine-tuning mark who is speaking.

The app saves each tokenizer as two plain files, vocab.json and merges.txt, so you can look at them. The Fast 10K BPE trained on Tiny Shakespeare has 5 special tokens, 256 single bytes, and 9,739 learned merges. Its first merges are what you’d expect from English text:

Ġ t
h e
Ġ a
o u
Ġ s

Ġ is how byte-level BPE writes a leading space, so the very first merge glues a space to a t. Some of its longest tokens are Northumberland and NORTHUMBERLAND: the name shows up 79 times in the file, 53 of them as the speaker label in capitals.

How the two tokenizers split real text

Tokenizers are easy to measure, so I measured them. I wrote a small byte-level BPE encoder in Python that reads the app’s vocab.json and merges.txt. It counts 312,081 tokens for the whole Tiny Shakespeare file, the same number the app’s token cache holds, so it splits text exactly like the app does.

Here’s how many tokens each one needs per 1,000 characters (fewer is better):

Text Fast 10K BPE (trained on Shakespeare) GPT-2 BPE
Last 20,000 characters of Tiny Shakespeare 294 326
First 20,000 characters of TinyStories validation 301 256
20,000 characters of my build-llm-from-scratch post 353 244
src/figures/flat.mjs from this site (JavaScript) 647 409

A small vocabulary trained on your data wins on that data and loses everywhere else. On Shakespeare it needs 10% fewer tokens than GPT-2. On my blog post it needs 45% more, and on code 58% more.

Single words show why:

Input Fast 10K BPE (Shakespeare) GPT-2 BPE
ROMEO: ROMEO : ROM EO :
Northumberland Northumberland North umber land
tokenizer to ken iz er token izer
Language Model Builder L an gu age ĠM od el ĠB uild er Language ĠModel ĠBuilder
Once upon a time, a small fox 8 tokens the same 8 tokens

This matters for context length. A 256-token context holds about 870 characters of Shakespeare with the Fast 10K tokenizer, and much less of anything it never saw.

The tokenizer playground

The textbook has a BPE playground. It starts from single characters and merges the most frequent neighboring pair, one merge at a time.

Run five merges by hand and watch the token count drop after each one. The algorithm follows counts, not grammar, which is exactly why Northumberland became one token in a vocabulary that splits tokenizer into four.

Interactive BPE tokenizer after learning ten merges from the sample text

Embeddings

An embedding represents each token as a position in a large numerical space. Tokens used in similar ways can develop similar representations. Cosine similarity compares two of those positions by the angle between them. The cosine similarity calculator does that from two JSON arrays.

The embedding playground flattens those vectors into two dimensions so you can look at them. Related words end up close together without anyone labeling them.

Two-dimensional embedding map grouping animals, places, colors, numbers, food, and family words

Be careful with what you read into the map. The real vectors have 256 numbers each in the Starter model, and squashing them into two dimensions distorts distances. Two names can also sit together because they appeared in the same story template, not because they mean similar things.

Embeddings are also where a big chunk of a small model’s parameters live, as we’ll see in the blueprint section.

Trace a transformer prediction

Anatomy of a Transformer walks one prompt through the model: token IDs, embeddings, masked self-attention, the feed-forward blocks, and finally one score for every token in the vocabulary. Softmax turns those scores into probabilities, and sampling picks one.

How to build an LLM from scratch has an interactive figure of that path, so I won’t repeat it here.

You can see the last step of it in the Sampling screen. After my TinyStories checkpoint wrote “She liked it very much and decided to take it home. Lucy took the ball home and showed”, the app listed the candidates for the next token:

 it    57.7%
 her   42.3%

Only two candidates survived the sampling filters at that position. The model does not store a sentence and play it back. It produces one distribution, picks a token, appends it, and runs again.

Choose a model blueprint

Open Project setup. The blueprint sets the tokenizer, context length, width, number of transformer blocks and attention heads, and shows the parameter count, the size of the weights on disk, and a calibrated training-time estimate.

Starter model blueprint showing a 9-million-parameter model and its calibrated training estimate

There are three presets, plus anything you build with the sliders. I recorded each one with the Fast 10K tokenizer, and the Starter shape with GPT-2 too. The estimates are the ones the app calibrated in the VM for its standard 20,000-step run:

Blueprint Context Width Blocks Heads Parameters Weights on disk Work per step Estimate in the VM
Tiny custom (sliders at minimum) 256 128 4 4 × 32 2.1M 4.1 MB 0.1× 1–30 min
Starter 512 256 8 4 × 64 9.0M 17.1 MB 1.0× 1–4 h
Starter with GPT-2 BPE 512 256 8 4 × 64 19.3M 36.8 MB not shown 3–7 h
Patient 768 384 12 8 × 48 25.1M 47.8 MB 4.2× 7–14 h
Ambitious 1024 512 16 8 × 64 56.5M 107.8 MB 13× 21–42 h

On my M4 Pro itself, the app estimated one to two hours for the Starter run.

Start with the recommendation. The point of the first run is a clean baseline you can finish, not the biggest model your Mac can hold.

The tiny custom blueprint: 256 tokens of context, width 128, 4 transformer blocks, 2.1 million parameters and a 1 to 30 minute estimate

The shape is a contract. Once a checkpoint exists, the app won’t let it load into a different shape, so changing the tokenizer or the number of blocks means a new run.

Parameters, memory and estimates

Parameters are the numbers training changes. A rough count for a decoder transformer is:

parameters ≈ vocabulary × width + blocks × 12 × width²

The first term is the token embedding table, which the app also reuses as the output layer. The second is attention plus the feed-forward network in each block. It lands close to the app’s exact counts:

Model Embeddings (vocab × width) Blocks (12 × width² each) Estimate App’s count
Tiny custom 10,000 × 128 = 1.28M 4 × 196,608 = 0.79M 2.07M 2,131,968
Starter, GPT-2 BPE 50,261 × 256 = 12.87M 8 × 786,432 = 6.29M 19.16M 19,289,344

Look at the second row. Two thirds of my TinyStories model is the vocabulary table. Switching from Fast 10K to GPT-2 BPE took the Starter shape from 9.0M to 19.3M parameters, and none of those extra parameters made it deeper or wider.

Disk size follows from the parameter count. The trainer keeps parameters in bfloat16, two bytes each, and the checkpoint files match that exactly:

File in the TinyStories project Size Why
checkpoints/model.safetensors 38,592,727 bytes 19,289,344 parameters × 2 bytes
checkpoints/optimizer.safetensors 77,191,834 bytes AdamW keeps two running averages per parameter
checkpoints/best-val/model.safetensors 38,592,733 bytes a copy of the weights at the best validation loss
checkpoints/token-cache/train.npy 1,128,066,512 bytes 564M training tokens as 2-byte integers

Training memory is bigger than the weight file. It also holds gradients, the optimizer state, and the activations of every layer for a batch of 32 sequences. If you want the general math, how much VRAM an LLM needs covers it.

Time is the number to sanity-check yourself. One step is one batch: 32 sequences × 512 tokens = 16,384 tokens. My run held a median of 30,924 tokens per second on the M4 Pro, so a step took about 0.53 seconds, and 20,000 steps of pure training would take this model about three hours. Validation, sample generation and checkpoint writes come on top.

Choose the dataset

Open Pre-training data. The catalog shows each dataset’s size, source, license and a preview of the text the model will read.

Pre-training dataset catalog with TinyStories, WikiText, Wikipedia, arXiv, and poetry datasets

The two datasets in this guide sit at opposite ends:

TinyStories 2 Tiny Shakespeare
Size 2.2 GB, downloaded from Hugging Face 1.1 MB, bundled with the app
License CDLA Sharing 1.0 MIT (from karpathy/char-rnn)
Tokens 564,033,192 (GPT-2 BPE) 312,081 (Fast 10K BPE)
Characters per token 3.95 3.57
Validation a separate file, TinyStories-valid.txt 10% split off the same file
One pass with batch 32 × 512 about 34,400 steps about 17 steps

That last row decides everything. On TinyStories, my 5,330 steps saw 87 million tokens, about 15% of one pass. On Tiny Shakespeare, the tiny model’s 5,000 steps at a 256-token context went through the training split 146 times.

The app describes Tiny Shakespeare as a smoke test, and it’s right. It’s the fastest way to see the machinery work, and the fastest way to see a model overfit.

Be careful with data you did not create. A local training run does not remove copyright, privacy, or licensing concerns. If you add your own text, read a few dozen examples first and search for broken encoding like é before you spend hours on it.

Run pre-training

During pre-training, the model learns to predict the next token. Each step takes a batch, compares the model’s predictions with the actual next tokens, and nudges the weights to make them more likely.

The app describes its training recipe as close to the TinyStories runs, with a trainer in the spirit of Karpathy’s nanoGPT: AdamW, a learning rate that warms up over 200 steps to 5e-4 and then follows a cosine curve down to 5e-5, and gradient clipping at 1.0.

Open Pre-training, check that the summary matches your manifest, choose a step target, and press Start pre-training.

Pre-training workbench prepared for a 20,000-step run on TinyStories 2

A tiny run, start to finish

For the tiny model I set the step target to 5,000. Before the start, the app showed:

5.0k steps · about 1–15m on this Paravirtual device
≈41.0M training tokens · ≈146 passes over the 281K-token corpus

Here’s what happened:

Value
Parameters 2,131,968
App’s estimate before starting 1–15 minutes
Real training time 7 min 9 s
Throughput about 110,000 tokens/s
Training loss, step 1 → 5,000 9.19 → 2.67
Best validation loss 4.96 at step 600
Final validation loss 6.00

The estimate was right. The SFT estimate later in this guide was not, so read the time as a range until the run settles.

A finished 5,000-step run of the 2.1 million parameter model on Tiny Shakespeare: training loss falls to 2.673 while validation loss climbs back to 5.995

The samples it saved along the way show the progression. Step 100 is a bag of Shakespeare words:

foretell crept giveWARWICWho mystery RICHARDClown thus world bearing Citizen curstEAS with night rich finIUS course be

By step 300 it has learned the shape of a play, with speaker names and line breaks:

ROMEO:
Either him, that my top him's point,
For a child as I may perfect me.

JULIET:
Even, lady.

At step 5,000 the lines look more confident, and they still make no sense:

GLOUCESTER:
I be a Richard; how wiseless substitute,
Asface'd light school you thus been more.
Why, Wor for this hand for time of arms,

A real run, paused and resumed

My TinyStories run is the one worth reading. It trained to step 4,225, I paused it, resumed it from that checkpoint, and stopped it at step 5,330.

The resume barely shows in the data. Training loss was 1.59 at step 4,000 and 1.72 at step 4,230, inside the normal noise between batches (it was 1.74 at step 4,400). Validation went from 2.037 at step 4,200 to 2.051 at step 4,300, and the step counter and the learning-rate schedule continued from where they were.

Pausing works because a checkpoint saves more than the weights: the optimizer state, the step, and the random state of the batch sampler (batch_rng_state in meta.json).

Read training and validation loss

Training loss measures how well the model predicts the batches it learns from. Validation loss measures the same thing on held-out text it never trains on.

For one token, the loss is the negative log of the probability the model gave the right answer:

probability 0.50 → loss 0.69
probability 0.14 → loss 1.96
probability 0.01 → loss 4.61

At step 1 my TinyStories model had a loss of 10.86. That’s almost exactly ln(50,261) = 10.83, the loss of a model that gives every token in the vocabulary the same probability, which is what random weights do. Its best validation loss of 1.957 means it gives the correct next token an average (geometric mean) probability of about 14%, out of 50,261 choices.

Click through the run to see the loss and the saved sample move together:

Fig 1 · 1/6Loss and samples during pre-training
step 1 · random weights

Here are the same checkpoints as a table:

Step Train loss Val loss Sample the app saved
1 10.86
100 5.80 5.95 LilyAnth weay clean 167 tomorrow come manHello grav park
500 2.56 2.85 Lily looked at her and said, "I can know, Ben, I will help you."
1,000 2.16 2.48 The rich man was so proud of his new friendship, and they all went home to cozy
2,000 1.79 2.22 He was so excited to eat the cheese. He took a big bite and it smelled so yummy!
5,000 1.62 1.96 Once upon a time, there was a big, red ball. The ball loved to bounce all day long.

Most of the improvement happens early. The loss dropped by 8 between step 1 and step 500, and by less than 1 between 500 and 5,000. The samples tell the same story from the other side: step 500 already has quotes and names, while steps 500 to 5,000 are about the story making sense.

Two curves, two diagnoses

The two runs look very different, and the difference is the most useful thing in this guide:

TinyStories GPT-2 (19.3M) Tiny Shakespeare (2.1M)
Data seen 15% of one pass 146 passes
Lowest validation loss 1.957 at step 5,000 4.962 at step 600
Last validation loss 2.005 at step 5,300 5.995 at step 5,000
Gap at the end (val − train) about 0.4 about 3.3
Diagnosis still learning overfitting since step 600

On Tiny Shakespeare the training loss kept falling for 4,400 steps after the validation loss hit its low and turned around. That’s overfitting: the model got better at the 281K tokens it kept seeing and worse at text it hadn’t. The useful checkpoint is the one from step 600, and the app keeps it for you in checkpoints/best-val.

Overfitting doesn’t have to mean copying. Not one six-word sequence in the samples saved at steps 300, 600, 1,000, 3,000 and 5,000 appears verbatim in the training file. The model got more confident about Shakespeare’s patterns without reciting him.

On TinyStories the gap of about 0.4 is hard to blame on memorization, since 15% of one pass means the model saw very few stories twice. Validation there comes from a different file, TinyStories-valid.txt, and the training text is TinyStoriesV2-GPT4-train.txt, so part of the gap is simply that the two files were written differently. Validation was still trending down, with the best value at step 5,000. More steps would have helped.

Look at the trend, not single points. TinyStories validation went from 2.091 at step 2,900 to 2.137 at step 3,000 and back down to 2.078 at step 3,100. One bump is noise. Ten rising points in a row, like on Shakespeare, is a trend.

Don’t compare loss values across tokenizers. A loss of 1.96 over a 50,261-token vocabulary and a loss of 4.96 over a 10,000-token vocabulary measure different things.

Two blueprints on the same data

Then I trained the Starter blueprint on the same Tiny Shakespeare file, same Fast 10K tokenizer, same VM, for 2,000 steps. The app estimated 1–30 minutes:

Tiny custom Starter
Parameters 2,131,968 8,982,528
Context 256 512
Tokens per step 8,192 16,384
Median throughput 108,920 tokens/s 34,386 tokens/s
Time for 2,000 steps about 2 min 30 s 16 min 3 s
Best validation loss 4.962 at step 600 4.892 at step 300
Train / val loss at step 500 4.44 / 4.97 2.89 / 5.31
Train / val loss at step 2,000 3.13 / 5.63 0.95 / 6.24

The tiny run was set to 5,000 steps and the Starter run to 2,000, so their learning rates had decayed by different amounts at step 2,000. Still, the pattern is clear: the bigger model reached a slightly better best checkpoint in half the steps, then overfit much harder. By step 2,000 its training loss was 0.95, which means it gave the right next token about 39% probability on text it had seen 117 times, while validation got worse than the tiny model’s ever did.

The Starter blueprint on Tiny Shakespeare: training loss falls to 0.945 while validation loss bottoms out at step 300 and climbs to 6.242

That’s the case for matching capacity to data. More parameters on 281K tokens mostly buy a faster path to overfitting them. Even so, none of the Starter samples saved between steps 300 and 2,000 repeats six words of the training text verbatim.

Checkpoints and sampling

A checkpoint is the saved state of the model at one step. The app keeps the latest one and the best-validation one in the project folder:

checkpoints/
  meta.json               step, shape, tokenizer, best_val_loss, best_val_step
  model.safetensors       latest weights
  optimizer.safetensors   AdamW state, needed to resume
  best-val/               weights at the lowest validation loss
  token-cache/            the dataset, already tokenized

My TinyStories meta.json says "best_val_step":5000 and "step":5330, so the best checkpoint is not the latest one.

Open Sampling to generate text from a checkpoint. The checkpoint decides the probabilities. The sampling settings decide how the app picks from them, and changing them never changes the weights:

  • Temperature sharpens or flattens the distribution.
  • Top-k keeps the k most likely tokens.
  • Top-p keeps the smallest set of tokens whose probabilities add up to p.
  • Min-p drops tokens below a fraction of the top token’s probability.
  • Seed makes a sample reproducible.

Sampling the TinyStories GPT-2 checkpoint at step 5,330: the prompt Once upon a time continues into a story about Lucy and a red ball, with the next token candidates it at 57.7% and her at 42.3%

Compare all three personas runs the same prompt with the same seed under three presets. With seed 42 on step 5,330, “Once upon a time” became:

Preset Settings Continuation
Predictable T 0.40 · k 12 · p 0.85 · min-p 0.10 there was a little girl named Lily. She loved to play outside in the sun. One day, she saw a big, red ball in the grass. She wanted to play with it.
Balanced T 0.80 · k 40 · p 0.95 · min-p 0.05 there was a little girl named Lucy. She had a big heart and loved to play with her toys. One day, Lucy found a new toy. It was a small, red ball. She
Creative T 1.20 · k 120 · p 1.00 · min-p 0.00 there was a brown dog named Buddy. Buddy liked to play because he could run so fast he would get in trouble.” One day, Buddy found a big race with all his friend. He

Same weights, three stories. The Creative one gets a dog and an adventure, and also a stray quote mark and “all his friend”. When you compare checkpoints, fix one preset and one seed, or you’ll be measuring the sampler.

Language Model Builder comparing the Predictable, Balanced and Creative sampling presets on the same prompt and seed

Diagnose poor results

Start from the symptom, then look for the measurement that explains it. These are the cases I hit or saw in these runs:

Symptom What to check What it was here
Validation loss rises while training loss falls how many passes over the data 146 passes over Tiny Shakespeare. Use the step 600 checkpoint, or more data
Loss around 10.8 that won’t move the loss of a uniform guess, ln(vocab) that’s step 1. If it stays there, check the data and learning rate
Words glued together (RICHARDClown, manHello) how far training got normal at step 100, rare by step 500 on TinyStories
Fluent text that ignores the question whether the checkpoint was fine-tuned the base model answering a chat prompt, see SFT below
Training time far longer than the estimate throughput in the first minutes SFT estimated 1 h 14 min at step 30, see below

Change one thing at a time, and give each new run its own name. A failed run is the comparison that explains why the next one exists.

Supervised fine-tuning

Pre-training teaches a model to continue text. It doesn’t teach it to answer a question like an assistant would.

You can see that in the chat view. I asked the TinyStories base checkpoint “Tell me a story about a brave little boat.” and it replied:

ly was happy and excited. She had never heard a car in the forest before.
The boat had a lot of fun!

It picked up mid-word, because to a base model the conversation is just more text to continue.

Supervised fine-tuning, or SFT, trains the model on example conversations. Open Fine-tuning data and pick a dataset. The app recommends Everyday Conversations 2k, 2,379 short, friendly multi-turn chats from Hugging Face under Apache 2.0, split into 2,260 for training and 119 held out. Each row is a messages array with user and assistant turns. This is the example row the app previews:

{
  "messages": [
    { "role": "user", "content": "Hi! What's a good breakfast?" },
    { "role": "assistant", "content": "Something simple like eggs and toast is a great start." }
  ]
}

Only the assistant turns are learning targets. The user turns are context, and the loss is computed on the replies. You can bring your own .jsonl file in the same shape, and the direct-download build can even turn your iMessage group chats into training data.

Everyday Conversations 2k in the fine-tuning data catalog, with an example user and assistant turn and the note that assistant turns are the learning targets

Open Supervised fine-tuning and start the run. It begins from your latest pre-training checkpoint and saves a separate SFT checkpoint, so the base model stays available.

Here’s my run on the TinyStories model, in the VM:

Value
Base checkpoint step 5,330
Steps 800, batch of 10 conversations
Estimate shown at step 30 about 1 h 14 min
Real time 8 min 35 s
Training loss 7.52 at step 1, around 3.8–4.2 at the end
Validation loss 4.567 at step 100 → 3.977 at step 800

The early estimate was nine times too long. The “at this pace” number at step 30 was based on the slow first steps. By step 200 it said 7 minutes left, and the run finished 6 minutes later.

Supervised fine-tuning of the TinyStories GPT-2 model: 800 steps from the step 5,330 base, validation loss 3.977, 8 minutes 35 seconds

The loss stays high, around 4, because these conversations are about breakfast, rocks and dog breeds, and the model only ever read children’s stories. SFT teaches the format, and 800 steps of short chats won’t teach a story model what a metamorphic rock is.

The sample timeline shows that clearly. At step 300, asked “What is a circle?”, the model replied:

A square is a circle with six buildon, which is the same.

Before & after asks both checkpoints the same question:

Before and after SFT: the base checkpoint continues a story, the SFT checkpoint answers the cozy question about blankets and warm coats

The base model kept writing a story. The SFT model answered the question, about warm blankets and coats, in two. That’s the change SFT makes: the model now treats <|assistant|> as its turn to answer and <|end|> as the place to stop.

Preference data and DPO

Direct preference optimization, or DPO, teaches the model from comparisons. You give it a preferred answer and a rejected answer, and training pushes the model toward the preferred one, relative to a frozen copy of the SFT model.

Open Direct preference optimization. The preference ballot asks your SFT checkpoint the same question twice and shows both answers. You click the one you’d rather receive.

The preference ballot for the TinyStories model: two answers to Can you suggest one cozy thing to do today, each with a This one is better button

Four pairs unlock training. I used one rule for all of them: pick the answer that stays closest to the question. The app saved them to preference-pairs.jsonl in the project folder:

Prompt Chosen Rejected
Can you suggest one cozy thing to do today? The perfect cloth to cover is very soft. It’s also a good idea to brush your teeth as a energy and comfortable for being warm and cozy. One soft of the most gentle thing is a soft and gentle ting glass. It’s also easy to clean and can make it more comfortable.
What is a good breakfast? A rice is a good salad with bread, bread, and eggs. You can also try some over air. A pasta is a great choice. It helps make a healthy and healthy meal and grow.
How do I make a new friend? You can create a unique friend or family on a modern home to your own and family. You can use a new idea in a comfortable place or a clear track, or make sure you have a quiet and quiet, safe and safe.
What should I do on a rainy day? You can try hot winter, cool, or cool ice cream. Have fun playing in the summer, or feel free to go outside and explore the ice. To stay warm and hot in a cold place, cool water, and cool back into the sunlight. You’ll also try making a cool weather to protect your body and its life.

Both answers in every pair are weak. That’s what a 19-million-parameter model produces, and you’re choosing the less wrong one.

Then I ran 300 DPO steps:

Value
Reference model SFT step 800
Pairs 4
Real time 1 min 33 s
Preference loss, step 1 0.694
Preference loss, step 300 0.0003
Held-out loss 0.601 at step 50 → 0.469 at step 300
Margin 0.19 → 0.51
Accuracy on held-out pairs 100%

The loss starts at 0.693, which is ln(2). At step 1 the trained model and its frozen reference are identical, so the model is at a coin flip between your two answers. Below that line, it prefers the answers you chose.

The training loss hit almost zero by step 50. With four pairs, the model doesn’t learn a preference so much as memorize which of four answers to rank first. The held-out numbers moved much less, and with so few pairs the 100% accuracy can’t tell you much.

The DPO run: preference loss falls from the coin-flip line at 0.693 to almost zero within 50 steps, with 100% held-out accuracy and a margin of 0.51

The saved samples show the limit. At step 300, for the breakfast prompt, the DPO model wrote “A pasta is a good job to eat. It’s easy to eat and is a great choice.” Pasta was in the answer I rejected.

Taste test asks the SFT and DPO checkpoints the same question:

Taste test: the SFT checkpoint talks about a warm spot, the DPO checkpoint suggests sleeping

Click through the whole sequence for one prompt:

Fig 2 · 1/4How SFT and DPO change one answer
base · keeps writing a story

For a first experiment, SFT is enough. DPO is worth running once to see the machinery, and the app’s own textbook says the same: for a model this size, better pre-training or SFT data is a better use of the next hour than more preference pairs.

Chat with your model

Open Chat with your model and pick a stage with the BASE → SFT → DPO switch. Switching starts a fresh chat, so every conversation belongs to one checkpoint.

The Transcript panel shows the exact text the model reads. Your message and its replies are flattened into one string with the reserved tokens, and the whole thing goes back in on every turn:

<|system|>
You are a helpful assistant. Answer the user clearly and concisely.<|end|>
<|user|>
What should I eat for breakfast?<|end|>
<|assistant|>
You can try using a fast-up or a wide-time track, which can make your food more comfortable.<|end|>

The context meter at the bottom shows how much of the 512-token window is used. My two-turn conversation with the SFT model used 93.

X-ray turns each reply into tokens, tinted by how likely each one was. Click a token to see what else the model could have said there. For the SFT model’s breakfast answer, the first token was:

You    67.1%
It      8.1%
In      6.4%
To      6.4%
If      6.0%
That    6.0%

X-ray view of the SFT checkpoint's replies, with the alternatives for the first token of the breakfast answer: You at 67.1%, It at 8.1%

The training data explains part of it. Its 2,260 conversations have 8,625 assistant turns, and their most common first words are Hello! (2,248 times, because nearly every conversation opens with a greeting), Yes, (1,348), The (477) and You (395), and 334 replies start with “You can”. After 800 steps, a breakfast question makes “You” the clear favorite.

Chat with the SFT checkpoint: short assistant-style answers about a boat and breakfast, with the raw transcript and its role tokens on the right

Compare checkpoints

Compare stages one transition at a time: base to SFT, then SFT to DPO. Use the same prompts, the same system instruction and the same sampler (here the chat default, temperature 0.8 and top-k 40).

I sent the same two-turn conversation to each stage:

Prompt Base (step 5,330) SFT (800 steps) DPO (300 steps)
Tell me a story about a brave little boat. ly was happy and excited. She had never heard a car in the forest before. The boat had a lot of fun! (26 tokens) That’s a great idea! It’s a good idea to use a strong boat and a smart-up or a strong boat. (26 tokens) You can find a doctor for your boat to help you with that. (14 tokens)
What should I eat for breakfast? : “I’m hungry,” she said, “I don’t want to eat a lot of food.” The boat was happy that she had listened to her hard work and decided to try some. So she put on her favorite dress and went outside to play. She had a great time, and she was very careful with her bathtub. (74 tokens) You can try using a fast-up or a wide-time track, which can make your food more comfortable. (23 tokens) You can try making a sandwich with red fruit, banana, or cheese. (15 tokens)

Scored with a small rubric:

Check Base SFT DPO
Answers in the assistant role 0 of 2 2 of 2 2 of 2
Ends cleanly with `< end >` 2 of 2
Gives a usable answer to the question 0 of 2 0 of 2 1 of 2
Writes story-like text when asked for a story 1 of 1 (a fragment) 0 of 1 0 of 1
Average reply length 50 tokens 25 tokens 15 tokens

Chat with the DPO checkpoint: the boat prompt gets a sentence about a doctor for the boat, the breakfast prompt gets a sandwich with fruit, banana or cheese

What I’d conclude from this, and what I wouldn’t:

  • SFT changed the format completely. That’s visible in every reply.
  • SFT cost the storytelling. The base model was the only one that wrote anything like a story, even if it ignored the boat. The SFT data has no stories in it.
  • DPO made the replies shorter, and the breakfast answer is the best of the three. With one sample per cell and four preference pairs, I can’t tell how much of that is DPO and how much is the dice.

To make the comparison stronger, run each prompt with three fixed seeds in Sampling, and report counts like “2 of 3 stayed on topic” instead of the best example.

Write the lab report

The report is what turns the run into something you can learn from a month later. Here’s mine, cut down to the numbers in this guide:

# Lab report: TinyStories GPT-2

## Setup
App 1.2.3 on an M4 Pro. GPT-2 BPE (50,261 tokens). 512 context, 256 wide,
8 blocks, 4 heads: 19,289,344 parameters. TinyStories 2: 564M training tokens.

## Pre-training
5,330 steps of 32 × 512 tokens (87M tokens, 15% of one pass), paused at 4,225
and resumed. Train loss 10.86 → 1.62 at step 5,000. Best val loss 1.957 at step 5,000.
Validation was still falling: the run was stopped early, not finished.

## SFT
Everyday Conversations 2k, 800 steps, 8 min 35 s in a VM.
Val loss 4.567 → 3.977. Replies moved into the assistant turn and got shorter.
Storytelling was lost.

## DPO
4 pairs, 300 steps. Training loss 0.694 → 0.0003 (memorized).
Held-out loss 0.601 → 0.469. Replies got shorter. Effect on content not
separable from sampling noise with one sample per prompt.

## Limits
One sample per prompt and stage. Validation file differs from the training file.

## Next experiment
Same blueprint and tokenizer, 20,000 steps instead of 5,330.
Expected: val loss below 1.9 and fewer grammar errors at step 20,000.

The next experiment changes one variable, so any difference in the result comes from that change.

What kind of model can you build?

Small ones. Language Model Builder trains educational models. Its website says the default settings can produce coherent, grammatical text in about a day.

The higher-end example it gives is a MacBook Pro with an M5 Max training a GPT-2-small-class model, around 100 to 150 million parameters, on a few billion tokens in about a week.

That is tiny next to current frontier models, and that’s fine. The goal is to find out what training a model involves, and small models are the right size for that. Their mistakes are easier to inspect, and experiments finish on hardware you already own.

You can also import a base model

There is a shortcut. You can import an open base model and skip project setup and pre-training.

The catalog includes SmolLM2, Supra, and Qwen2.5 models. The smallest ones are easier to fine-tune, while the larger ones need much more unified memory.

Importing locks the project’s architecture and tokenizer to the selected model. You can still run SFT, DPO, sampling, and chat.

This is how most real fine-tuning projects start. Still, if you want to understand the whole process, build the small model first.

Language Model Builder catalog of SmolLM2, Supra, and Qwen base models

Does everything stay local?

Your projects, imported data, weights, prompts, preferences, and training results stay on your Mac. There is no account and no cloud training service.

A few network connections do happen. Dataset downloads go to their hosting service, such as Hugging Face. The direct-download version of the app checks for updates. Crash reports are sent only with your permission, and you can read them before they go out.

The privacy policy spells all of this out.

Who is this for?

Language Model Builder is for people who use language models every day without really knowing what happens inside, and who learn better by changing something and watching what happens. It also suits anyone who wants to experiment without assembling a Python toolchain or paying an API bill, and anyone who teaches AI concepts to other people.

It won’t train a production frontier model, and it only runs on recent Apple Silicon Macs. The app removes the setup work so you can watch each training stage and change its settings.

Download Language Model Builder, go through the introduction, and train the tiny blueprint on Tiny Shakespeare first. It takes about 7 minutes, and you’ll see both learning and overfitting before your coffee is cold. Then do the real run on TinyStories.

Want me to talk about your product? You can sponsor this site.

Related posts about ai: