The complete guide

How a 125.8M-parameter language model gets built, from an empty folder to a live demo.

This page explains every concept behind slm-125m in plain English — what pretraining actually is, why the dataset looks the way it does, how the tokenizer works, how to read the training curve, and how the whole thing is deployed on Modal and Vercel. No prior ML background assumed.

What is "pretraining"?

A language model is, mechanically, a function that takes some text and predicts what word (technically, token — more on that below) comes next. Pretraining is the process of teaching that function to be good at this prediction, by showing it an enormous amount of real text and letting it guess, over and over, billions of times.

Every one of the model's 125,847,552 parameters starts as a small random number. Before training, the model has never seen a single sentence — it knows nothing about English, law, or finance. It guesses randomly, and it's wrong in a very specific, measurable way: it assigns roughly equal probability to all 16,384 possible next tokens, which is exactly the worst-case starting point.

The loop, one step at a time

Take a window of real text
Model predicts the next token
Compare to what actually came next
Nudge every parameter to make that guess slightly better

Repeat that loop 3,889 times (this project's entire run), each time over a fresh batch of 524,288 tokens, and the "nudges" accumulate into a model that has implicitly learned grammar, facts, and style — not because anyone told it the rules, but because predicting the next token well requires learning them.

"Base model" vs. what you might expect from ChatGPT

This is a base model — it only ever did the loop above. It was never shown "here's a question, here's a good answer" pairs (that additional stage is called instruction-tuning), and it never got human feedback on which responses are better (RLHF). So it doesn't answer questions — it continues text, the way autocomplete does, just far more fluently. Type "The plaintiff shall" into the live demo and it'll keep writing in that style, not respond to it as an instruction.

In plain terms

Pretraining is show, don't tell at an enormous scale — the model never receives a rule of grammar or a legal definition directly. It only ever gets one signal, repeated billions of times: "here's some text, predict what's next," followed by "here's how wrong you were." Everything it appears to "know" is a side effect of getting extremely good at that one narrow task.

Why "125.8M" is small

For comparison, GPT-3 has 175 billion parameters — about 1,400× larger. This project deliberately stays small: it trains in under 3 hours on a single GPU for about $12, which makes the entire pipeline (data → clean → dedup → tokenize → train → deploy) something one person can run end to end and afford out of pocket.

02 · This project

What we're actually doing here.

Six phases, each one a small, cheap, resumable cloud job — not one long fragile script.

Phase 1–2

Collect & clean

Stream three public datasets, filter out noise, remove near-duplicates, and strip anything that overlaps with the benchmarks we'll later use to judge the model.

Phase 3–4

Tokenize

Train a custom vocabulary from scratch on the cleaned text, then convert every document into the fixed-length integer sequences the model actually trains on.

Phase 5–6

Train & ship

Run the pretraining loop on a single GPU, then push the finished weights to Hugging Face and stand up a live inference endpoint.

The specific bet this project makes: instead of a generic model trained on a random scrape of the internet, train one almost entirely on US case law and SEC filings — two dense, high-quality, domain-specific text sources — with just enough general web text mixed in for fluency. See the dataset section for why that mix isn't 70/20/10.

03 · The dataset

Where the text actually comes from.

Three public, ungated sources, streamed directly from Hugging Face — never fully downloaded to disk:

US case law
HFforLegal/case-law — 282,390 US court opinions. Some are scanned documents with OCR noise, which is why this source alone gets an extra cleaning gate (below).
SEC filings
PleIAs/SEC — 48,543 filings (10-Ks and similar), born-digital so far cleaner per document than the scanned case law.
fineweb-edu
HuggingFaceFW/fineweb-edu — general educational web text, used only as a small fluency top-up, not the backbone.

Why the mix is ~35/42/23, not 70/20/10

The original idea was 70% case law, 20% SEC, 10% web. That's arithmetically impossible: measuring actual clean-token yield showed case law tops out around 0.81B tokens and SEC around 1.16B — together, about 2 billion tokens. You cannot make case law 70% of a 10-billion-token corpus when the entire source doesn't contain that much text. fineweb-edu, by contrast, is effectively bottomless (~11.67B tokens available in the sampled slice alone).

So the actual strategy — take all of both legal sources, and add just enough web text to round things out — produces roughly 40% case law, 40% SEC, 20% web by design, landing at 35/42/23 after cleaning and dedup removed slightly different fractions from each source.

The cleaning pipeline

Every document passes through the same deterministic chain before it's allowed into the corpus:

1Drop short/noisy lines
2Strip known boilerplate (letterheads, page numbers, signature blocks)
3Drop documents under 600 characters
4Drop repetitive/degenerate text (same phrase looping)
5Drop non-English text
6Case law only: drop OCR garbage (>20% non-dictionary words)

Deduplication & decontamination

Two more passes happen after cleaning. Exact deduplication hashes every document and drops byte-for-byte repeats. Near-duplicate detection (via MinHash, run on case law specifically) catches documents that are mostly the same text with minor edits — the kind of near-copy exact hashing would miss.

Then, critically: a 13-word overlap check against two legal benchmarks (CaseHOLD and LexGLUE) strips out any training document that shares a long verbatim chunk with those test sets. Without this, the model could effectively "memorize the answer key" and score well on an eval it was never supposed to have seen — a problem called test-set contamination. This run removed about 24,000 case-law documents on that basis alone.

StageTokens kept
Raw, cleaned2.68B
+ deduped + decontaminated2.40B
+ tokenized & packed2.04B
case law SEC filings fineweb-edu
Why this matters

Every drop reason is logged, not silent — this is what lets you audit exactly why a document was excluded rather than trusting a black box.

04 · Tokenizer

What byte-pair encoding is, and why it exists.

A neural network doesn't read letters — it reads numbers. Tokenization is the step that converts text into a sequence of integers, and byte-pair encoding (BPE) is the specific algorithm used here to decide what counts as one "unit."

The naive options are both bad: split by character and sequences become enormous (slow, hard to learn long-range patterns); split by whole word and you need a vocabulary of millions of entries plus a fallback for every word you've never seen. BPE finds a middle ground — common enough to be efficient, small enough to handle any input, including words the tokenizer has never encountered.

The algorithm, conceptually

Start with every input broken into individual bytes. Repeatedly find the most frequent adjacent pair of units in the training corpus and merge it into one new unit. Do this thousands of times. Common sequences — "tion", "the", whole short words — end up as single tokens; rare or unfamiliar text falls back to smaller pieces or individual bytes, so nothing is ever "unrepresentable."

0t  o  k  e  n  i  z  e  d — start: one token per byte
1t  o  k  en  i  z  ed merge "e"+"n" → "en", merge "e"+"d" → "ed"
2tok  en  iz  ed merge "t"+"o"+"k" → "tok", "i"+"z" → "iz"
3token  ized merge again → 2 tokens instead of 8 bytes

(A simplified illustration — the real algorithm operates over the whole training corpus at once, at byte level, not word-by-word like this.)

What's specific to this project

The tokenizer isn't a pretrained, off-the-shelf one — it's trained from scratch (Phase 3) on this project's own cleaned corpus, so its vocabulary reflects legal and financial language specifically, not generic internet text. It's byte-level (the starting alphabet is raw bytes, not Unicode characters), which guarantees any input — any language, emoji, or malformed text — can always be encoded, with a vocabulary of exactly 16,384 entries.

tokenizer test
>>> tok.encode("The plaintiff shall bear the
    burden of proof by a preponderance
    of the evidence.")
15 tokens

>>> tok.encode("The Company's net revenues
    increased 12% year over year
    pursuant to the agreement.")
16 tokens

>>> tok.decode(tok.encode(text)) == text
True  # lossless roundtrip, always
Why 16,384, not 50,000+

Bigger vocabularies mean fewer tokens per sentence (faster, more efficient) but a larger embedding table to train. At 125M total parameters, a huge vocabulary would eat a disproportionate share of the model's capacity. 16,384 is sized for this model, not copied from a larger model's defaults.

05 · The training run

Reading the loss curve.

Every training step, the model produces a probability distribution over all 16,384 possible next tokens. Loss (specifically, cross-entropy loss) measures how much probability it assigned to the token that actually came next — low probability on the right answer means high loss, high probability means low loss.

Perplexity is the same number, just more intuitive: it's e^loss, and it roughly means "the model is as confused as if it were guessing uniformly among this many options." A perplexity of 10 means: about as uncertain as picking correctly out of 10 equally likely choices.

What actually happened in this run

Step 0
loss 9.87, perplexity ≈19,400 — this is exactly ln(16384), the mathematical signature of a freshly-initialized model with no learned signal at all. Seeing this number confirms the model started correctly, before a single dollar was spent training further.
Step 3,889 (final)
loss 2.263 on the training set, val loss 2.326 (perplexity ≈10.2) on held-out data the model never trained on — the number that actually matters, since it tests generalization, not memorization.

The learning rate schedule

The step size the model takes on each update (the learning rate) isn't constant. It ramps up linearly for the first ~381 steps (warmup — starting at full speed on random weights tends to destabilize training), peaks at 6e-4, then decays smoothly along a cosine curve down to 6e-5 by the final step — large careful steps early, small precise ones as the model converges.

One epoch means one full pass through all 2.04B training tokens. This run did exactly one — the corpus is small enough, and Chinchilla scaling research suggests roughly 20 tokens per parameter is compute-optimal for a model this size; this run landed at about 16.2, just under that line.

Training loss — all 3,889 steps

9.87 → 2.263 nats
Hardware & cost

1× H100 GPU, ~2.85 hours, ~$12–13 for this phase — the original design defaulted to 8 GPUs, which would mostly sit idle at this model size. See the Modal section for why.

07 · Infrastructure

Vercel — deploying this exact page.

This site (the one you're reading right now) is static HTML/CSS/JS with no build step, deployed to Vercel.

Why there's no backend here

The "Try it" demo on the homepage doesn't route through Vercel at all — the browser calls the Modal inference endpoint directly from client-side JavaScript. This works because the Modal endpoint sends Access-Control-Allow-Origin: * (a CORS header granting cross-origin browser access), so no proxy server is needed in between. The result: this entire site is just files, no server code of its own to maintain.

deploying a static site
npm install -g vercel

# from inside the site's own directory
vercel link --yes --project my-site
vercel deploy --prod

Get a token

Vercel dashboard → Settings → Tokens → Create Token. This authenticates the CLI without needing an interactive browser login every time.

Link the project

vercel link --yes --project <name> — first run creates the project, later runs just reconnect. Run it from the actual site directory, not a parent folder with unrelated files in it.

Deploy

vercel deploy --prod uploads the files and returns a live URL — plus a stable alias (like slm-125m-phi.vercel.app) that stays constant across every future deploy, so links you've shared keep working.

Where it ended up

Final numbers.

125.8M
parameters
2.04B
training tokens
2.326
final val loss · ppl ≈10.2
$15.54
total cost, every phase