TAI BUI
Back to curriculum
CapstonePython

Tokenized Dataset with Sliding Window

A pretraining run turns token ids into gradients. This lesson builds the conveyor that feeds the ids in.

TIME: ~90 minutesPREREQUISITES: Phase 04 lessons, Phase 07 transformer lessons, Lesson 30 of this phase

Learning Objectives

  • Convert a raw corpus into a stream of token ids by training a BPE tokenizer on it and encoding it.
  • Slice the id stream into fixed-length windows with a configurable overlap stride.
  • Build a PyTorch Dataset that returns input and target tensors for next-token prediction.
  • Wrap the dataset in a DataLoader with a deterministic shuffle seeded per epoch.
  • Reason about the trade-off between stride, redundancy, and effective dataset size.

The frame

A causal language model trains on batches of token ids, and the data pipeline's job is to produce those batches on demand. Each batch holds (B, T) input ids and (B, T) target ids, where the target is the input shifted left by one. The pipeline has to deliver that contract deterministically and reproducibly, from a corpus that may be several gigabytes of raw text.

The pipeline has four pieces. The tokenizer from the previous lesson turns text into one long flat list of ids. A sliding window slices that list into training examples. A custom Dataset exposes the examples as tensors. A DataLoader batches them and shuffles them with a known seed.

Check: The frame0/1
1. What is the id stream that the data pipeline slices into training examples?Define

The shape contract

A causal LM consumes ids of shape (B, T), where B is the batch size and T is the context length. The target at position t is the input at position t+1, so every training example covers T+1 raw ids. The window stride sets how much consecutive examples overlap.

Mermaid diagram

The slicer stays inside the corpus. If the last window lacks the ids to fill T+1 positions, the slicer drops it. Padding the tail with <|pad|> is also valid, but it complicates the loss mask. Here we drop.

Check: The shape contract0/4
1. In the shape (B, T) that a causal language model consumes, what do B and T stand for?Define
2. Why does each training example cover T+1 raw ids when the model only takes T input ids?Explain
3. Why does the lesson drop an incomplete trailing window instead of padding it?Explain
4. A teammate pads the short last window with <|pad|> and averages the loss over every position. What goes wrong?Analyze

Why a sliding window

A sliding window is a fixed-size slice that moves along the id stream by a set stride. A pretraining corpus is one long stream of ids, and with only non-overlapping windows every example would put its boundaries at the same T-aligned positions. Adjusting the stride moves those boundaries around, so the model sees more varied predict-next-token tasks.

A stride of T gives non-overlapping windows. A stride of T // 2 gives fifty-percent overlap and doubles the effective dataset. A stride of 1 gives maximum overlap and multiplies the dataset by a factor of T. The cost is more compute per epoch, and the benefit is more boundary diversity. Most pretraining runs use a stride equal to the context length. The corpus is already much larger than the model can finish in one epoch, so the boundary-diversity argument is weaker.

Check: Why a sliding window0/6
1. What is a sliding window over an id stream?Define
2. What does the stride control in this pipeline?Define
3. What effect does halving the stride have on the dataset?Explain
4. What trade-off does the lesson describe for choosing a stride of 1 instead of the context length?Explain
5. Why do most pretraining runs set the stride equal to the context length?Explain
6. Your corpus is several gigabytes, far more than the model will finish in one epoch. You want to keep compute per epoch low. Which stride fits?Design

The Dataset class

A PyTorch Dataset is a class with two required methods. __len__ returns the number of examples, and __getitem__ returns one example as a pair of tensors. Ours stores the encoded id stream and the stride. Indexing computes the start of the window on the fly, so the memory cost is one copy of the id stream no matter how many examples the stride produces.

Mermaid diagram

The shift-by-one happens inside __getitem__. It returns (input, target) where input = window[:-1] and target = window[1:], both as PyTorch long tensors. The training loop treats them as ground truth.

Check: The Dataset class0/5
1. Which two methods must a PyTorch Dataset provide?Define
2. Why does the dataset return (window[:-1], window[1:]) from each window of size T+1?Explain
3. Why does the memory cost stay at one copy of the id stream, however small the stride is?Explain
4. A dataset is built on ids [0, 1, 2, ..., 9] with context_length=4 and stride=2. What does dataset[1] return?Apply
5. A dataset slices ids[start : start + context_length] instead of T+1 ids, then returns window[:-1] and window[1:]. What breaks?Analyze

Deterministic shuffle

A deterministic shuffle is one that produces the same order every time you restart the run. A DataLoader with shuffle=True draws from a PyTorch random generator, so passing an explicit torch.Generator seeded per epoch fixes the order. That matters when you want to compare two runs that differ in a single hyperparameter. Without a seed the two runs see the data in different orders, and the loss curves diverge for reasons unrelated to the change.

The seed contract is epoch_seed = base_seed + epoch_index. make_dataloader takes base_seed and epoch as arguments and seeds the generator with their sum. The caller passes the epoch index and increments it at the top of each epoch. A re-run with the same base seed sees the same order in every epoch.

Check: Deterministic shuffle0/6
1. What is a deterministic shuffle?Define
2. Why pass an explicit torch.Generator to the DataLoader?Explain
3. In make_dataloader, the generator is seeded with base_seed + epoch. Who supplies the epoch value?Explain
4. You call make_dataloader(ds, batch_size=4, base_seed=42, epoch=3). With which seed is the generator created?Apply
5. Two runs differ only in learning rate, yet their loss curves diverge from the first epoch. The loader uses shuffle=True with no generator. What is the likely cause?Analyze
6. A loop calls make_dataloader(ds, 4, base_seed=7, epoch=0) at the start of every epoch. What do the epochs see?Analyze

Batch sampler

The default PyTorch sampler picks indices uniformly at random with replacement disabled, which is what pretraining wants. Finetuning on a small dataset uses the same contract. The DataLoader assembles a batch by calling __getitem__ B times and stacking the results. Every example has the same length by construction, so no padding logic is needed.

We keep num_workers=0 for simplicity. In a production run, workers parallelize the __getitem__ calls. Here that is mostly a no-op, since the work is a slice of an in-memory tensor, but the same Dataset API supports workers cleanly.

Check: Batch sampler0/3
1. How does a DataLoader assemble one batch of B examples?Define
2. Why does this DataLoader setup need no padding logic when it stacks examples into a batch?Explain
3. A dataset has 10 examples and you call make_dataloader(ds, batch_size=4) with the defaults. How many batches does one pass yield?Apply

Counting examples

For an id stream of length N, a context length T, and a stride S, the number of examples is max(0, 1 + (N - (T + 1)) // S). The Dataset exposes that calculation as a static method, so the trainer can compute total steps per epoch without iterating.

Check: Counting examples0/4
1. How many windows of length T+1 fit in an id stream of length N with stride S?Define
2. Your tokenized corpus has 1,000 ids, the context length is 15, and the stride is 8. How many examples does the dataset hold?Apply
3. What does SlidingWindowDataset.count_windows(num_ids=33, context_length=8, stride=8) return?Apply
4. Your trainer needs the number of steps per epoch before training starts, and the corpus has millions of ids. What is the cheapest correct approach?Design

What this lesson does not do

There is no disk streaming. The corpus is encoded fully in memory and held as a single tensor. For a few million ids that is well under a hundred megabytes, which suits this build. Disk streaming is a separate concern: it plugs in by replacing the storage and keeps the Dataset contract.

There is also no per-document handling. The corpus is treated as one continuous id stream. When the corpus is built from multiple documents, the next-document boundary is encoded by inserting <|endoftext|> ids, and the model learns to predict around the boundary.

Check: What this lesson does not do0/2
1. How does this pipeline handle a corpus built from multiple documents?Explain
2. You must build one training stream from 3,000 separate text files and want the model to learn where documents end. Which design fits?Design
Author's Note

I'd default the stride to T. When the corpus is already much larger than the model can finish in one epoch, boundary diversity buys little, and a smaller stride costs more compute per epoch. I'd shrink the stride only with a specific reason to want more boundary diversity.

I'd also seed the shuffle from the first run. Two runs that differ in one hyperparameter only compare cleanly if they see the data in the same order, and epoch_seed = base_seed + epoch_index is a one-line contract. My bet is that count_windows earns its spot as a static method too: the trainer gets steps per epoch without iterating a single batch.

How to read the code

main.py defines two classes (MiniBPE and SlidingWindowDataset) plus helpers. SlidingWindowDataset is the PyTorch Dataset. make_dataloader returns a configured DataLoader with a seeded generator. _encode_corpus_to_ids trains the inline BPE tokenizer on the corpus, then encodes it into one flat id list. The demo at the bottom builds a small tokenizer in-process, encodes a built-in corpus, constructs the dataset and dataloader, prints one batch, and asserts the shape contract. The tests in code/tests/test_dataset.py pin the window count formula, the shift-by-one property, the deterministic shuffle, and the stride trade-off.

Run the demo, then change the context length from 16 to 32 and watch the number of examples per epoch fall. That number is your steps-per-epoch budget.

Warm-up0/3
1. In next-token prediction, what is the training target for the token at position t of a sequence?Warm-up
2. Why would you set a random seed before shuffling data for an experiment?Warm-up
3. A trained tokenizer is given a short piece of text. What does it hand back?Warm-up
Check: How to read the code0/3
1. Why does _encode_corpus_to_ids call train_bpe before encode_text?Explain
2. A dataset holds 5 examples. What does dataset[-1] return?Apply
3. You call make_dataloader(ds, batch_size=4, shuffle=False). Which window is the first row of the first input batch?Apply

Code