Skip to content
Forward Deployed

Forward Deployed
Lessons and contents
Fine-Tuning & Model AdaptationLesson 5 of 9
9 lessons
Learn

Data preparation

Source
From the RAG & Agents Engineering course
By
Sourav Banerjee
Last updated
9 Nov 2026

Report an issue

Data preparation

What you'll learn

  • Explain how a formatted example becomes one row of token ids in a rectangular batch.
  • Trace tokenizing, padding and truncation on real Pythia token ids.
  • Diagnose the three silent failures: a cut answer, a cut question and a missing end token.
  • Check a dataset for empty rows, duplicates, refusals and length before training on it.

Lesson 3 of T14.1 ended on a rule: decode the tokenized row, not the string. This page shows why. A Larkspur engineer sets a token limit of 48 to save memory, trains, and finds the assistant now stops the refund answer mid-sentence. Nothing raised an error. The limit cut the end off every long answer before the model ever saw it.

Data preparation turns each formatted example into one row of token ids, makes the rows fit a length limit and a common width, and holds some rows back for evaluation. A model trains on token ids, in rectangular batches, and every step below exists to produce one:

Diagram: Formatted text question + answer; Token ids tokenizer; Cut to max_length left or right; Pad to one width attention_mask; labels = input_ids then 90/10 splitDiagram: Formatted text question + answer; Token ids tokenizer; Cut to max_length left or right; Pad to one width attention_mask; labels = input_ids then 90/10 split

Figure 1: The preparation pipeline, from formatted text to a split, labelled dataset.

Tokenize, pad and truncate

The notebook loads the EleutherAI/pythia-70m tokenizer, the one its model was trained with. Text and tokens are different units: the first lamini_docs example is 358 characters and 77 tokens. On a short sentence the three operations look like this:

Operation Input Result
Tokenize "Hi, how are you?" 6 ids: 12764, 13, 849, 403, 368, 32; decoding returns the same string
Pad to the longest row "Hi, how are you?", "I'm good", "Yes" (6, 3 and 1 tokens) Three rows of 6, the short ones filled with id 0 on the right
Truncate right, max_length 3 "Hi, how are you?" 12764, 13, 849 ("Hi, how")
Truncate left, max_length 3 "Hi, how are you?" 403, 368, 32 (" are you?")

Table 1: Pythia tokenizer operations on the notebook's sample sentences, checked against the tokenizer in the scratchpad.

Padding exists because a batch is one tensor, so its rows need one width. Pythia's tokenizer has its own <|padding|> token (id 1), but the notebook sets the pad token to the end-of-text token. Padding therefore uses id 0, the end-of-text id. attention_mask marks real tokens with 1 and padding with 0, and attention skips the padding. Truncation caps every row at max_length; the default cuts from the right, and the notebook switches the tokenizer to cut from the left.

Label and split

For a causal language model the target is the input itself, shifted by one inside the model. The notebook adds labels as a copy of input_ids. A shuffled split with 10% held out and seed 123 then turns the 1,400 rows into 1,260 for training and 140 held out. The fixed seed gives the same split on every run, which is what lets lesson 6 of T14.1 compare models on the same 140 questions.

Three failures that raise no error

Each choice above changes what the model learns, and three of them fail silently:

Planted failure What reaches the model What it learns
Cut answer: right truncation on a long row The question and the start of the answer; the end, and any end token, are gone Answers that stop mid-sentence
Cut question: left truncation on a long row The answer, with the ### Question: header and the start of the question gone Answers to questions it cannot see
No end token: the Pythia tokenizer appends nothing A question and an answer with no marker after the answer Nothing about where an answer stops

Table 2: Three silent failures in data preparation, one row per guard.

The guards follow from the table. Append the tokenizer's end-of-text token to each training text. Since padding reuses id 0 and labels copies it, set padded positions in labels to -100 so the loss ignores them. And choose max_length from measured lengths, not from a memory budget alone.

The interactive lab builds a three-example Larkspur batch (refund, reboot and export questions in the ### Question: template) with the real Pythia tokens. At max_length = 48 right truncation cuts 11 answer tokens from the 59-token refund example. Left truncation keeps that answer but cuts the 4-token header and 7 question tokens instead. Padding to the longest row pads the two shorter examples by 15 and 3 tokens, 13% of the batch. With the end token appended and padding to a fixed max_length of 64, the batch carries 52 padding positions, 27%. No row ends with <|endoftext|> until you append it.

Interactive lab: Truncation and padding. Open full screen (opens in a new tab)

Those counts depend on the batch, so the next question is what limit this dataset needs:

Statistic over the 1,400 lamini_docs rows Tokens
Median 75
90th percentile 128
Longest 479
The notebook's cap 2,048

Table 3: Token lengths of question plus answer, measured with the Pythia tokenizer in the scratchpad.

In the notebook the limit never bites: no row comes near 2,048. Measure your own length distribution before choosing max_length: set it just above the longest rows you want to keep whole, and decide on purpose what happens to the rest.

Two notebook details matter before you reuse its approach. Its tokenizing function reads only the first item of each batch it receives. It works only because the dataset is mapped one example at a time. And with one example per batch there is nothing to pad: the training run in lesson 5 of T14.1 never pads at all.

Tokenizing checks none of the content, which is the job of the next section.

Checking the rows

Tokenizing does not check the data. Read the rows before you train on them; in lamini_docs.jsonl a short pass finds four things:

Check What it finds in the 1,400 rows Why it matters
Empty rows 1 row with a blank question and a blank answer It tokenizes to zero ids; lesson 5 of T14.1's custom trainer has a guard that skips empty inputs
Duplicate questions 3 extra copies of questions already in the file Duplicates weigh a behaviour twice, and one copy can land in the test split while the other trains
Refusals 39 answers that ask to "keep the discussion relevant to Lamini" These rows are what teach the model to decline off-topic questions such as What do you think of Mars?
Length 0 rows over 2,048 tokens; the longest is 479 Sets max_length, and with it memory and step time

Table 4: Row checks on lamini_docs.jsonl, counted in the scratchpad.

The refusal rows show how a behaviour gets in: the model declines off-topic questions because 39 training answers do. A Larkspur dataset needs its own share of those rows, written in the house style, or the fine-tuned assistant will answer What do you think of Mars? as cheerfully as the base model. We call that gap missing refusals: a behaviour the team wants but no training row shows.

The duplicates point to an ordering rule. Split leakage happens when one copy of a question trains and another sits in the test split, and evaluation rewards memory. Deduplicate before you split, and check that no test question also appears in training.

Run the notebooks

The code for this lesson is in these notebooks. Colab runs one in your browser: install the packages its first cell imports and set the API keys it reads. To run every notebook as written, clone the repository and use its uv environment.

Notebook GitHub Colab
Data preparation lab Open on GitHub Open in Colab
Data preparation lecture (optional) Open on GitHub Open in Colab

Key Takeaways

  • Data preparation is tokenize, truncate, pad, copy input_ids to labels, then split with a fixed seed. Right truncation cuts the answer's end and left truncation cuts the question, both without an error.
  • Append the end token to every training text and set padded label positions to -100. Measure the length distribution before you choose max_length.
  • Read the rows before training. Remove empty rows, deduplicate before the split, and include refusal rows in the house style, because the model only declines what its training answers decline.

Check yourself

1. The Pythia tokenizer appends no end token, and padding reuses the end-of-text id. What do you add to each training text, and what do you set at padded label positions?

Append the end-of-text token to each training text: every row then shows the model where an answer stops. Then set the padded positions in labels to -100: padding uses id 0 and labels copies input_ids, so without that step the loss would train on the padding.

2. In the lab's Larkspur batch at max_length 48, what does each truncation side remove from the refund example?

Right truncation cuts 11 answer tokens from the end, and the model trains on an answer that stops mid-sentence. Left truncation keeps the answer but cuts the ### Question: header and 7 question tokens. The model trains on an answer to a question it cannot fully see. Neither raises an error.

3. A fine-tuned Larkspur assistant answers What do you think of Mars? as cheerfully as the base model. What was missing from its training data?

Refusal rows. In lamini_docs.jsonl the model learns to decline off-topic questions because 39 training answers ask to keep the discussion relevant to Lamini. A Larkspur dataset needs its own share of those rows, written in the house style.

Rate your confidence and keep notes on the module page.