LLM Developer DocsLLM Docs

Loading the search index ...

↑ ↓ move, Enter open, Esc close

GitHub

Data Pipeline

How data.py and train.main turn a CSV file into training windows and batches, and why the targets are the inputs shifted by one.

data.py

This page follows the text from the CSV file to the batches the training loop reads. None of these steps has a backward pass: gradients stop at the embedding table, and the ids have no gradient.

The order in train.main

train.main runs these lines in this order, before the model exists. The comment in the code says the same: split BEFORE windowing.

train.pymainGitHub: train.py
    text = load_corpus(config.corpus_path, config.corpus_column, config.max_rows)
train.pymainGitHub: train.py
    tok = build_tokenizer(text, config.num_merges)
train.pymainGitHub: train.py
    ids = encode_corpus(text, tok)
train.pymainGitHub: train.py
    train_ids, val_ids = split_ids(ids, config.val_fraction)
train.pymainGitHub: train.py
    tr_x, tr_y = make_windows(train_ids, config.seq_len, config.stride)    va_x, va_y = make_windows(val_ids, config.seq_len, config.stride)

The tokenizer is built on the whole text, including the part that later becomes validation. The split separates the windows, not the vocabulary or the merges.

Next-token prediction

Training has one objective: at each position of a window, predict the next token. For input x[t], the correct answer is y[t], the id right after it in the stream. A causal mask in attention stops a position from reading later ones, so the prediction at t can only use x[0] to x[t]. Softmax & Cross-Entropy covers how the model is scored on each answer.

Note

In general, the probability of a whole sequence can be written as a product, over positions, of each token's probability given the tokens before it. The PDF states this as Eq. 61. This code never computes that product. It computes a mean cross-entropy over windows of at most 16 tokens.

Text to ids

load_corpus reads one column of a CSV file. In config.py the file is "data/corpus.csv", the column is "text", and at most 2000 rows are read.

  • A column that does not exist raises ValueError.
  • max_rows limits the rows read from the file, before missing values are dropped.
  • Each remaining row is stripped of surrounding whitespace.
  • All rows are joined with newlines, and one final newline is added.

The result is a single string. Rows have no other boundary in it, so a window can run from the end of one row into the start of the next, and the model sees the newline as a token.

From rows to one textIllustrative ExampleHand-picked rows, not from the corpus.
RowsText returned
[" hello ",null,"world"]"hello\nworld\n"
Code listing: data.load_corpus
data.pyload_corpusGitHub: data.py
def load_corpus(path, column, max_rows) -> str:    df = pd.read_csv(path, nrows=max_rows)     if column not in df.columns:        raise ValueError(f"no column {column!r}, the file has {list(df.columns)}")     texts = df[column].dropna().astype(str).str.strip()    return "\n".join(texts) + "\n"

encode_corpus passes that string to the tokenizer and stores the ids in a one-dimensional int64 array of N ids. Tokenization covers encode.

Code listing: data.encode_corpus
data.pyencode_corpusGitHub: data.py
def encode_corpus(text: str, tok: Tokenizer) -> np.ndarray:    return np.array(tok.encode(text), dtype=np.int64)

The split

What is it?

The splitImplemented

Data

split_ids cuts the id array at one position. Everything before the cut is for training and everything after it is for validation. Nothing is shuffled, so both parts stay in order.

Why does the model need it?

Validation loss is only useful on text the model did not train on. Because the cut happens before windows exist, no window can hold ids from both sides.

Windows overlap, so the order matters. If train.main made windows first and split them afterwards, the windows around the cut would hold ids from both sides, and validation would partly test on training text. Splitting first removes those windows, at the cost of fewer windows overall.

Mathematics

Where the stream is cut #Link to equation: Where the stream is cut
cut=⌊(1−ρ) N⌋\text{cut} = \lfloor (1 - \rho)\, N \rfloor

What does each symbol mean?

SymbolMeaningIn codeShape
ρ\rhoFraction kept for validationval_fractionscalar
NNNumber of ids in the streamlen(ids)scalar
cut\text{cut}Number of training idscutscalar

Tiny numerical example

The last column counts the windows that would hold ids from both sides if the order were reversed. At stride 1 it equals T in every row.

Splitting before windowingIllustrative ExampleHand-picked lengths, with stride 1. These are not counts from the corpus. The last row uses the N of tests/test_data.py with the settings in config.py.
Nval_fractionTcutTrain windowsValidation windowsWindows crossing the cut, if windowing came first
200.253151223
400.24322844
3,0000.1162,7002,68428416

Tensor shapes

ids has shape (N,), train_ids has cut elements and val_ids has N - cut. In config.py, val_fraction is 0.1.

Actual implementation

data.pysplit_idsGitHub: data.py
def split_ids(ids, val_fraction):    cut = int((1 - val_fraction) * len(ids))    return ids[:cut], ids[cut:]

Code and math connection

EquationRoleLinesCode
Where the stream is cutComputes itdata.py:20cut = int((1 - val_fraction) * len(ids))
Where the stream is cutComputes itdata.py:21return ids[:cut], ids[cut:]
Where the stream is cutCalls ittrain.py:43train_ids, val_ids = split_ids(ids, config.val_fraction)
Where the stream is cutTests ittests/test_data.py:37assert len(train) == 90 and len(val) == 10

Where is it used?

Source

data.py, lines 19-21, split_ids

PDF reference

Section 4.2, p. 6

Used by

Called by: train.main

Tested by: tests.test_data.test_split_is_contiguous_and_disjoint, tests.test_data.test_window_counts_for_n_3000

Status
Implemented

Implemented in

EquationRoleFileFunctionLinesGitHub
Where the stream is cutComputes itdata.pysplit_ids20GitHub: data.py, line 20
Where the stream is cutComputes itdata.pysplit_ids21GitHub: data.py, line 21

Sliding windows

What is it?

Sliding windowsImplemented

Data

make_windows slides a window of T ids along the stream, moving k ids each time. The inputs are the window. The targets are the window that starts one id later, so each input row moves one place left and gains one new id at the end.

Why does the model need it?

The model reads a fixed number of tokens at once, and training needs a correct answer for every position. Shifting by one makes each target the id that comes right after the input at that position.

Mathematics

Where windows start Derived from implementation#Link to equation: Where windows start
s∈{0, k, 2k, … },s<N−Ts \in \{0,\, k,\, 2k,\, \dots\}, \qquad s < N - T

The last target of a window is ids[s + T], which must exist. That is why s stops before N - T.

Inputs #Link to equation: Inputs
xt(s)=idss+t,t=0,…,T−1x^{(s)}_{t} = \text{ids}_{s+t}, \qquad t = 0, \dots, T-1
Targets #Link to equation: Targets
yt(s)=idss+t+1,t=0,…,T−1y^{(s)}_{t} = \text{ids}_{s+t+1}, \qquad t = 0, \dots, T-1
Targets are inputs shifted by one #Link to equation: Targets are inputs shifted by one
yt(s)=xt+1(s),t=0,…,T−2y^{(s)}_{t} = x^{(s)}_{t+1}, \qquad t = 0, \dots, T-2

What does each symbol mean?

SymbolMeaningIn codeShape
ssStart position of a windowstarts(W,) for all of them
ttPosition inside the windownp.arange(seq_len)scalar
kkHow far the window moves each timestridescalar
TTWindow lengthseq_lenscalar
NNNumber of ids given to make_windowslen(ids)scalar
xxInputs, one row per windowinputs(W, T)
yyTargets, one row per windowtargets(W, T)

Tiny numerical example

Edit the ids, the window length or the stride.

Sliding windowsIllustrative ExampleHand-picked ids that you can edit. They are not ids from the corpus. Tests compare the engine behind this table with the real make_windows.

N = 10, W = ceil((10 - 4) / 1) = 6

Start sInputsTargets
03, 1, 4, 11, 4, 1, 5
11, 4, 1, 54, 1, 5, 2
24, 1, 5, 21, 5, 2, 6
31, 5, 2, 65, 2, 6, 5
45, 2, 6, 52, 6, 5, 3
52, 6, 5, 36, 5, 3, 5

In every row, the targets are the inputs moved one place left, with one new id at the end.

Tensor shapes

train.main uses 16 as T and 1 as k. At stride 1, neighbouring windows share all but one id.

Inputs of the training windowsdata.py:31
  1. train_ids(N,)
  2. tr_x(W, T)(W, 16)

N is the number of ids. It depends on the corpus. W is the number of windows. It depends on the corpus. T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt.

Targets of the training windowsdata.py:32
  1. train_ids(N,)
  2. tr_y(W, T)(W, 16)

N is the number of ids. It depends on the corpus. W is the number of windows. It depends on the corpus. T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt.

make_windows returns two pandas DataFrames, with one row per window and one column per position. They are not NumPy arrays until iterate_batches or validation_loss calls to_numpy. The two DataFrames share the same row index, which keeps each input row with its target row.

Actual implementation

data.pymake_windowsGitHub: data.py
def make_windows(ids, seq_len, stride = 1):    if len(ids) <= seq_len:        raise ValueError(f"need more than seq_len={seq_len} tokens, got {len(ids)}")     starts = np.arange(0, len(ids) - seq_len, stride)    rows = starts[:, None] + np.arange(seq_len)     inputs = pd.DataFrame(ids[rows])    targets = pd.DataFrame(ids[rows + 1])    return inputs, targets

Code and math connection

EquationRoleLinesCode
Where windows startComputes itdata.py:28starts = np.arange(0, len(ids) - seq_len, stride)
InputsComputes itdata.py:29rows = starts[:, None] + np.arange(seq_len)
InputsComputes itdata.py:31inputs = pd.DataFrame(ids[rows])
TargetsComputes itdata.py:32targets = pd.DataFrame(ids[rows + 1])
Targets are inputs shifted by oneComputes itdata.py:32targets = pd.DataFrame(ids[rows + 1])
Targets are inputs shifted by oneTests ittests/test_data.py:21assert (y.to_numpy()[:, :-1] == x.to_numpy()[:, 1:]).all()

Where is it used?

Source

data.py, lines 24-33, make_windows

PDF reference

Section 4.2, Eq. 17, p. 6

Status
Implemented

Implemented in

EquationRoleFileFunctionLinesGitHub
Where windows startComputes itdata.pymake_windows28GitHub: data.py, line 28
InputsComputes itdata.pymake_windows29GitHub: data.py, line 29
InputsComputes itdata.pymake_windows31GitHub: data.py, line 31
TargetsComputes itdata.pymake_windows32GitHub: data.py, line 32
Targets are inputs shifted by oneComputes itdata.pymake_windows32GitHub: data.py, line 32

Window count

What is it?

Window countDerived from implementation

Data

The number of windows is the number of start positions. The code never computes this formula. It follows from the array of starts that make_windows creates.

Why does the model need it?

The window count sets how many batches make up an epoch, and so how many steps training runs. The learning-rate schedule needs that total.

Mathematics

np.arange(0, N - T, k) lists the multiples of k from 0 up to, but not including, N - T. Counting them gives the formula.

Number of windows #Link to equation: Number of windows
W=⌈N−Tk⌉,N>TW = \left\lceil \frac{N - T}{k} \right\rceil, \qquad N > T

If N is not greater than T, make_windows raises ValueError before it creates any start. With a stride above 1, the ids after the last window may go unused.

What does each symbol mean?

SymbolMeaningIn codeShape
WWNumber of windowslen(inputs)scalar
NNNumber of ids given to make_windowslen(ids)scalar
TTWindow lengthseq_lenscalar
kkStridestridescalar

Tiny numerical example

The formula and make_windowsIllustrative ExampleHand-picked lengths. The last column is the number of rows the real make_windows returned.
NTkFormulamake_windows
104166
104233
104322
104611
104711
54111
101199
401612424

Tensor shapes

W is the number of rows of tr_x and tr_y, as shown under Sliding windows. It depends on the corpus, so it stays a symbol.

Actual implementation

The code is the starts line of make_windows, shown under Sliding windows. No function computes W from the formula.

Code and math connection

EquationRoleLinesCode
Number of windowsComputes itdata.py:28starts = np.arange(0, len(ids) - seq_len, stride)
Number of windowsComputes itdata.py:25if len(ids) <= seq_len:
Number of windowsTests ittests/test_data.py:44assert len(make_windows(train, 16)[0]) == 2684

Where is it used?

Source

data.py, lines 24-33, make_windows

PDF reference

Section 4.2, Eq. 18, p. 6

Status
Derived from implementation

Implemented in

EquationRoleFileFunctionLinesGitHub
Number of windowsComputes itdata.pymake_windows28GitHub: data.py, line 28
Number of windowsComputes itdata.pymake_windows25GitHub: data.py, line 25

Shuffling and batching

What is it?

Shuffling and batchingDerived from implementation

Data

iterate_batches puts the windows in a random order, using the random generator it is given, and hands them out in groups of batch_size. A shorter last batch is kept.

train.main creates one generator from 42 (train.py:37) and uses it for everything random. iterate_batches receives it once per epoch and passes it to pandas, which moves the generator along. So each epoch gets a new order, and a repeated run with the same settings gets the same orders.

Why does the model need it?

One update from all the windows would be slow, and one update from a single window would be noisy. A new order each epoch changes which windows are averaged together.

Mathematics

Batches in one epoch #Link to equation: Batches in one epoch
batches per epoch=⌈WB⌉\text{batches per epoch} = \left\lceil \frac{W}{B} \right\rceil

What does each symbol mean?

SymbolMeaningIn codeShape
WWNumber of windowslen(tr_x)scalar
BBWindows in one batchbatch_sizescalar

Tiny numerical example

Two epochs of iterate_batchesIllustrative ExampleHand-picked sizes: 10 windows, batch_size 4, generator seed 42. The order is the stored result of running the real function, because it comes from pandas and NumPy. Each number is the start of one window. This is not the order of a real training run, which also depends on every earlier use of the generator.

Batch sizes: 4, 4, 2

EpochBatch 1Batch 2Batch 3
12, 9, 1, 63, 8, 5, 74, 0
25, 9, 6, 20, 1, 7, 34, 8

Tensor shapes

B is 16, except in a shorter last batch. The batch is a NumPy array, but the model has no batch axis: batch_loss_and_grads takes one row at a time (train.py:17). Training Loop covers how the results are averaged.

From all windows to one sequencedata.py:43
  1. tr_x(W, T)(W, 16)
  2. xb(B, T)(16, 16)
  3. x(T,)(16,)

W is the number of windows. It depends on the corpus. T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt. B = batch_size = 16. A final short batch has fewer rows.

Actual implementation

data.pyiterate_batchesGitHub: data.py
def iterate_batches(inputs, targets, batch_size, rng, shuffle = True):    order = inputs.index    if shuffle:        order = inputs.sample(frac=1, random_state=rng).index     for i in range(0, len(order), batch_size):        rows = order[i : i + batch_size]        yield inputs.loc[rows].to_numpy(), targets.loc[rows].to_numpy()
train.pymainGitHub: train.py
    for epoch in range(config.epochs):        for xb, yb in iterate_batches(tr_x, tr_y, config.batch_size, rng):

Code and math connection

EquationRoleLinesCode
Batches in one epochComputes itdata.py:41for i in range(0, len(order), batch_size):
Batches in one epochComputes itdata.py:39order = inputs.sample(frac=1, random_state=rng).index
Batches in one epochWritten inlinetrain.py:54steps_per_epoch = math.ceil(len(tr_x) / config.batch_size)
Batches in one epochTests ittests/test_data.py:58assert sizes == [16, 16, 14]

Where is it used?

Source

data.py, lines 36-43, iterate_batches

PDF reference

Not stated in the PDF

Used by

Called by: train.main

Tested by: tests.test_data.test_batches_cover_every_window_once, tests.test_data.test_no_shuffle_keeps_order

Written inline in train.py, line 54. The same count, computed for the learning-rate schedule.

Status
Derived from implementation

Implemented in

NoteInline equivalent: the pipeline calls data.iterate_batches, and also computes the same formula in place, with no call, at train.py:54.
EquationRoleFileFunctionLinesGitHub
Batches in one epochComputes itdata.pyiterate_batches41GitHub: data.py, line 41
Batches in one epochComputes itdata.pyiterate_batches39GitHub: data.py, line 39
Batches in one epochWritten inlinetrain.pymain54GitHub: train.py, line 54

Every symbol in data.py

SymbolLinesOn this page
data.load_corpusdata.py:5-12Text to ids
data.encode_corpusdata.py:15-16Text to ids
data.split_idsdata.py:19-21The split
data.make_windowsdata.py:24-33Sliding windows
data.iterate_batchesdata.py:36-43Shuffling and batching