§2.5–2.6Byte Pair Encoding & Sliding-Window Sampling

Part I pp. 12–16 · ~6 min read

  • byte pair encoding
  • tiktoken
  • sub-word tokenization
  • input–target pair
  • sliding window
  • stride
  • context length
  • batch size

The word-level tokenizer of §2.4 has two weaknesses: it needs an <|unk|> escape hatch for unseen words, and its vocabulary must list every word form separately. Real GPT models use byte pair encoding instead, and then slice the token stream into training examples with a sliding window.

2.5 Byte pair encoding

Byte pair encoding (BPE) is sub-word tokenization : instead of whole words, its vocabulary is made of pieces — from single characters up to common whole words. Any string can be spelled out of these pieces, so BPE never meets a word it can’t encode and needs no <|unk|> token.

How the vocabulary is built (training). BPE starts with a base vocabulary of individual bytes/characters, then repeats one step: find the most frequent adjacent pair of symbols in the corpus and merge it into a single new token. Frequent letter groups (like es, then est) and eventually whole common words get their own tokens, while rare words stay split into small pieces. GPT-2’s BPE runs this until the vocabulary reaches 50,257 tokens.

startnewestmerge e·s → esnewestmerge es·t → estnewest
Training BPE on a corpus containing “newest”/“widest”: the pair e·s is merged first, then es·t, growing sub-word tokens from characters.

At inference, BPE simply applies the learned merges. Feed it an invented word and it falls back to sub-word pieces — here the book’s "Akwirw ier" example:

"Akwirw ier"
1

An unknown "word"

The string "Akwirw ier" appears in no word-level vocabulary — a word tokenizer would emit <|unk|>.

1 / 3

An unknown string is still fully tokenized — BPE splits it into known sub-word pieces, each with its own ID (book Figure, p. 13).

In practice the book doesn’t reimplement BPE — it uses OpenAI’s fast tiktoken library (its core is written in Rust):

</> Tokenizing with tiktoken (GPT-2 BPE)
import tiktoken

tokenizer = tiktoken.get_encoding("gpt2")                    #A
text = "Hello, do you like tea? <|endoftext|> In the sunlit terraces."
integers = tokenizer.encode(text, allowed_special={"<|endoftext|>"})   #B
print(integers)
# [15496, 11, 466, 345, 588, 8887, 30, 220, 50256, 554, 262, ...]

strings = tokenizer.decode(integers)                         #C
print(strings)   # round-trips back to the original text
  • #A Load the exact GPT-2 BPE vocabulary (50,257 tokens) used by the pretrained weights we load in ch5.
  • #B encode() returns token IDs. allowed_special lets the literal "<|endoftext|>" be encoded as the single special token 50256 instead of being split.
  • #C decode() reverses it perfectly — BPE is lossless.

2.6 Data sampling with a sliding window

We can now turn text into a long stream of token IDs. To train next-word prediction we need (input, target) examples, where — since the model predicts the next token — the target is the input shifted right by one . A sliding window of width max_length slides across the stream in steps of stride , cutting out one pair per position.

Play with the two knobs below. Watch how max_length (the model’s context length ) sets how much the model sees, and how stride controls the overlap between consecutive training windows:

LLMslearntopredictonewordatatime

5 input→target pairs. The target is the input shifted right by one. Because stride < max_length, consecutive windows overlap.

  • #1[LLMs, learn, to, predict][learn, to, predict, one]
  • #2[learn, to, predict, one][to, predict, one, word]
  • #3[to, predict, one, word][predict, one, word, at]
  • #4[predict, one, word, at][one, word, at, a]
  • #5[one, word, at, a][word, at, a, time]

Within a single window, the pair itself is really n next-word predictions stacked up — the model may use “LLMs” to predict “learn”, “LLMs learn” to predict “to”, and so on, but it can never peek past its target:

one window (max_length = 4): input → target
input
40
367
2885
1464
target
367
2885
1464
1807
Real GPT-2 token IDs. Each target cell is the input cell one step to its right — the essence of next-word prediction.

The book packages this as a PyTorch Dataset that precomputes every window:

</> GPTDatasetV1 — one (input, target) pair per index
import torch
from torch.utils.data import Dataset, DataLoader

class GPTDatasetV1(Dataset):
  def __init__(self, txt, tokenizer, max_length, stride):
      self.input_ids = []
      self.target_ids = []
      token_ids = tokenizer.encode(txt)                          #A

      for i in range(0, len(token_ids) - max_length, stride):    #B
          input_chunk  = token_ids[i : i + max_length]
          target_chunk = token_ids[i + 1 : i + max_length + 1]   #C
          self.input_ids.append(torch.tensor(input_chunk))
          self.target_ids.append(torch.tensor(target_chunk))

  def __len__(self):
      return len(self.input_ids)                                 #D

  def __getitem__(self, idx):
      return self.input_ids[idx], self.target_ids[idx]
  • #A Tokenize the whole text once, up front, into a flat list of BPE token IDs.
  • #B Slide a window across the stream in steps of `stride`, stopping max_length before the end so a full target always exists.
  • #C target_chunk is input_chunk shifted right by one — the next-word labels.
  • #D __len__ and __getitem__ are the two methods PyTorch needs; __getitem__ returns one (input, target) tensor pair.

A thin wrapper builds the DataLoader, which handles batching (grouping batch_size pairs), shuffling, and iteration:

</> create_dataloader_v1
def create_dataloader_v1(txt, batch_size=4, max_length=256,
                       stride=128, shuffle=True, drop_last=True,
                       num_workers=0):
  tokenizer = tiktoken.get_encoding("gpt2")
  dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
  dataloader = DataLoader(
      dataset, batch_size=batch_size, shuffle=shuffle,
      drop_last=drop_last, num_workers=num_workers)              #A
  return dataloader
  • #A DataLoader turns the per-example Dataset into batched, optionally shuffled tensors of shape (batch_size, max_length). drop_last discards a ragged final batch. This exact loader feeds pretraining in ch5.

A batch stacks several windows so the GPU processes them in parallel — for batch_size=8, max_length=4, stride=4 the inputs tensor has shape (8, 4) and the targets tensor is the same, shifted:

inputs batch (8×4)
40
367
2885
1464
1807
3619
402
271
10899
2138
257
7026
15632
438
2016
257
922
5891
1576
438
568
340
373
645
1049
5975
284
502
284
3285
326
11
Eight non-overlapping windows (stride = max_length = 4) stacked into one batch. Small batches use less memory but give noisier gradient updates — a hyperparameter to tune.

Key idea

BPE gives a lossless, fixed 50,257-token vocabulary that can spell any text; the sliding window turns that token stream into (input, target) pairs where the target is the input shifted by one. The DataLoader batches them — and this is the exact data pipeline that feeds pretraining in Chapter 5. Next, §2.7 turns these token IDs into the embedding vectors the model truly consumes.

📝 Check yourself: BPE and sampling

0 / 5
  1. 1.How does byte pair encoding handle a word it has never seen before, like 'Akwirw'?

  2. 2.What is the core training step of the BPE algorithm?

  3. 3.In sliding-window sampling, how is the target sequence related to the input sequence?

  4. 4.If max_length = 4 and stride = 4 (equal), what happens to consecutive training windows?

  5. 5.Why does the book wrap the sampling in a PyTorch Dataset + DataLoader rather than building the tensors by hand?