youtube.nixfred.com nixfred.com

Let's reproduce GPT-2 (124M)

Four hours, empty file to trained model. Karpathy writes the GPT-2 124M architecture to match the released weights exactly, then spends most of the video making it fast: TF32 and bfloat16, torch.compile, flash attention, choosing vocabulary sizes with nice powers of two, then the optimization recipe from the GPT-3 paper (AdamW settings, gradient clipping, cosine schedule with warmup, weight decay, gradient accumulation to reach a half million token batch) and finally distributed training across eight GPUs. It ends with a real run on FineWeb-Edu that beats the published GPT-2 124M numbers.

Published Jun 9, 2024 4:01:26 video 120 min read Added Jul 30, 2026 Open on YouTube →

At a glance

Four hours. One empty file. One trained model that beats the thing it was copying.

Andrej Karpathy opens an empty train_gpt2.py, writes the GPT-2 124 million parameter architecture from scratch in PyTorch, loads the released OpenAI weights into his own module tree to prove the implementation is correct, throws those weights away, and trains the thing himself on FineWeb-Edu. By the end of a run that costs about ten dollars and takes under two hours on eight rented GPUs, his from scratch model is above the published GPT-2 124M on both the validation loss and HellaSwag. The companion repository is build-nanogpt, with one git commit per step of the video.

The architecture is the easy part and takes about forty minutes. What makes this the video people come back to is the middle two hours, where every optimization is applied one at a time and measured, so you watch the step time fall from 1,000 milliseconds to 90 on a single A100: TF32, bfloat16 autocast, torch.compile, flash attention, padding an ugly vocabulary size of 50257 up to a pretty 50304, and fused AdamW. Then the full optimization recipe copied out of the GPT-3 paper, gradient accumulation to reach the intended 524,288 token batch, and eight way distributed data parallel at 1.5 million tokens per second.

It is a lecture, a benchmark log, and a debugging diary at the same time. He leaves in the bug where he forgets to move a tensor to the GPU, the generations that refuse to match Hugging Face's pipeline, the torch.compile error he cannot solve on camera, and a loss curve with a periodicity he does not fully understand at the end of four hours. Those are not blemishes. They are most of why the video is loved.

The setup: what we are reproducing, and what it costs

He is careful about the word "reproduce" in the first thirty seconds. GPT-2 is not one model, it is a miniseries, and the biggest one usually gets the name. OpenAI released four sizes in 2019, from 124 million parameters up to 1558 million, so that you can put model size on the x axis of a plot, downstream metrics like translation, summarization and question answering on the y axis, and chart the scaling laws. This video does the smallest one, 124M.

Right away a small correction to the published record: the parameter counts in the GPT-2 paper's table disagree with the numbers he uses, and the reason is that the table is wrong. The GPT-2 repository says there was an error in how they added the parameters up. The real 124M configuration is 12 layers, 12 attention heads, and 768 channels.

Two assets exist and two do not, and the whole video is shaped by that asymmetry:

So the plan is to take the architecture and the target weights from GPT-2, and the training recipe from GPT-3. The cost framing he gives up front is the headline of the whole video: this was probably a fairly complicated optimization five years ago on much smaller GPUs, and today you can reproduce it in roughly an hour or less for about ten dollars of rented cloud compute.

Start at the target: opening the OpenAI checkpoint

The first move is to start at the end. Before writing a line of his own, he loads the released GPT-2 124M and takes it for a spin.

The original code is TensorFlow, which he calls "not used as much anymore", so instead he goes through Hugging Face Transformers, which has already done the work of converting the weights to PyTorch friendly tensors. One awkward detail he flags immediately: in Hugging Face, from_pretrained("gpt2") gives you the 124M model, not the famous 1.5 billion one. For that you want gpt2-xl.

He pulls the state_dict and prints every key with its shape, in a Jupyter notebook running inside VS Code, because he likes one interface for everything.

The two shapes that explain the model:

Then he does something most tutorials skip: he plots the weights. The position embedding table, visualized as 1024 rows, has obvious structure, because those embeddings end up learning sinusoids and cosines that stand in for position. In the original Attention Is All You Need the positional encodings are fixed to sinusoids of different frequencies; in GPT-2 they are ordinary parameters trained from scratch, and they recover sinusoid like features anyway.

He pushes on the picture and gets a real diagnostic out of it. The curves are a bit jagged and noisy, and that tells you the released model was not fully trained, because a more trained model would smooth them out. He also stops to be properly impressed: in principle these curves do not have to be smooth at all. The table starts as complete random noise, and the fact that anything interpretable falls out of the optimization is already remarkable.

Looking into individual channels as a function of position, three picked at random, some channels respond to parts of the position spectrum and not others. One green channel fires for everything from about 200 up to 800, much less below, with a sharp dropoff near zero. "Who knows what these embeddings are doing and why they are the way they are." He takes the first block of 300 by 300 from the first layer's weights and sees structure there too, with an aside for anyone who likes mechanistic interpretability, which is not what this video is about.

Finally he samples from the Hugging Face pipeline with the prefix "Hello, I'm a language model," asking for five sequences of 30 tokens. It produces coherent text. One wrinkle: even with the seed fixed he does not get the generations that the documentation example produced, so presumably the code changed. The point lands anyway. The weights load, they generate, and the state dict keys tell you exactly where everything lives in the model.

Which sets up the real task. The Hugging Face modeling_gpt2.py is "medium readable but not fully readable" and about 2,000 lines. He wants his own GPT class, so he has full understanding of what is happening, and his first milestone is to load this exact checkpoint into it.

We're going to have a lot of confidence that because we can load the OpenAI model we are in the same model family and model class, and we just have to rediscover a good setting of the weights, but from scratch. Andrej Karpathy, 13:20

SECTION 1: implementing the GPT-2 nn.Module

He opens the Attention Is All You Need architecture figure and starts deleting. GPT-2 is a decoder only Transformer, so the entire encoder stack is gone, and the cross attention block that was feeding off the encoder goes with it. Everything else stays almost the same, with two changes documented in section 2.3 of the GPT-2 paper:

  1. The layer norms moved. Instead of sitting after the attention and after the feed forward, they move to the input of each sub block. This is the pre normalization layout.
  2. One extra layer norm was added right before the final classifier, after the last block.

The skeleton, named to match Hugging Face

He deliberately mirrors the Hugging Face naming scheme, because matching keys is what makes the weight port trivial later. The container is an nn.ModuleDict called transformer, which lets you index submodules by string key:

Then, outside the dict, lm_head, the language model head: a linear projection from 768 up to the vocabulary size of 50257. GPT-2 uses no bias on this final projection.

He then reads his own skeleton back against the figure from the paper, element by element, which is the step that makes the names stop being arbitrary:

So the module tree is not a convention he invented, it is the figure read top to bottom with GPT-2's two edits applied, and it happens to be spelled the way Hugging Face spells it so the state dict keys line up for free.

The block: a clean residual stream

The block is where he makes his first strong opinion stick. In the original figure the normalizations are inside the residual stream, so the residual pathway has normalizations in it. That, he says, is "not very good or desirable".

The argument runs through addition. Recall from micrograd that addition simply distributes gradients to both of its branches equally during the backward pass. So if the residual path is pure addition, gradients from the top flow straight down to the input tokens unchanged, while in addition flowing through the blocks, which contribute their own corrections over time and kick in gradually. You want a single clean residual stream all the way from supervision down to the tokens.

So the block is pre normalization, and in his code it reads as two lines of forward pass:

And then the framing that has probably been quoted more than anything else in the video. Attention is a communication operation. There are 1024 tokens lined up in a sequence and attention is where they exchange information. It is an aggregation, a pooling, a weighted sum, a reduce. The MLP happens at every token individually with no information exchanged between tokens at all, so it is the map.

So the attention is the reduce and the MLP is the map, and what you end up with is that the Transformer just ends up being a repeated application of map reduce, if you want to think about it that way. Andrej Karpathy, 19:58

Attention is where they communicate; the MLP is where each one thinks individually about what it gathered; and every block iteratively refines the representation sitting in the residual stream.

The MLP, and a tangent about GELU that is worth the detour

The MLP is two linear projections sandwiched around a nonlinearity. Up by a factor of four, from 768 to 3072, then the nonlinearity, then back down to 768.

The nonlinearity is nn.GELU(approximate='tanh'), and this is where he spends five minutes on something nobody would have blamed him for skipping.

GELU, Gaussian Error Linear Units, looks very much like a slightly smoother ReLU, except there is no exactly flat tail at zero. It comes from a paper with "some mathematical calac reasoning", as the captions render it, that connects it to stochastic regularizers and the expectation of a modification to adaptive dropout.

PyTorch offers both the exact version and a tanh approximation. There is no real good reason to use the approximation today. So why does it exist, and why is he using it?

Because of a GitHub issue. Dan Hendrycks, the GELU author, explains in PyTorch issue 39853 that at the time he developed the nonlinearity, the error function erf that you need for the exact GELU was very slow in TensorFlow, so they built the approximation instead. That approximation then got picked up by BERT, by GPT-2, and onwards. His exact words in the thread:

I used the tanh approximation simply because the error function erf was slow in tensorflow some years ago. If the exact version is fast enough now and does not have numerical issues, I do not see a reason to use an inexact version. Dan Hendrycks, PyTorch issue 39853, quoted in the video at 22:03

So the tanh form is a historical quirk, and the only reason to keep it is fidelity. Karpathy is reproducing GPT-2 exactly, GPT-2 used the tanh approximate version, so he sticks with it.

The intuitive argument for GELU over ReLU gets its own paragraph, and it is the dead ReLU neuron problem. In the flat tail of a ReLU, any activation that lands there gets exactly zero gradient. No change, no adaptation, no development of the network for that neuron. GELU always contributes a local gradient, so there is always a change and always an adaptation, and smoothing it out ends up working better empirically. He notes that more modern networks, Llama 3 among them, have moved on again to SwiGLU and variants.

Causal self attention, as tensor gymnastics

He goes through this one faster, pointing back at the previous video in the series for the slow version. The content is the same multi headed attention; the implementation is different.

In the earlier video the heads were separate modules whose outputs were concatenated, which made it obvious that heads are "just kind of like parallel streams". Here all of that collapses into one module, and the cost is "a bunch of transpose, split, tensor gymnastics to make this very efficient in PyTorch". Fundamentally and algorithmically nothing is different.

What happens, in his order:

  1. Each token emits three vectors, the query, the key and the value. c_attn is a single linear layer producing all three at once, which he then splits.
  2. The number of heads is folded into the batch dimension, so PyTorch treats both B and n_head as batch dimensions and applies everything in parallel across both.
  3. Queries and keys multiply to give the attention, "how interesting they find each other", which has to be a multiplicative interaction.
  4. The autoregressive mask makes sure tokens only attend to tokens before them and never to the future. It is a registered buffer, not a parameter, which matters in a minute.
  5. Softmax normalizes the attention so it sums to one.
  6. The attention matrix multiplied against the values is a weighted sum of the values of the tokens each token found interesting.
  7. A final transpose, contiguous and view reassembles everything, and that step is what actually performs the concatenation of the heads.
  8. c_proj projects back out.

The shapes, which are the part worth having written down if you are typing along, with B the batch, T the sequence length up to 1024, C the 768 channels, nh the 12 heads and hs the 64 dimensional head size, where nh times hs equals C:

He is careful with variable names throughout, so his keys follow the Hugging Face schema exactly, which is what makes the port a copy loop.

The result: Hugging Face's file is about 2,000 lines. His complete GPT-2 implementation is under 100 lines of code.

GPT (nn.Module) idx (B, T) T ≤ 1024 wte nn.Embedding 50257 x 768 wpe nn.Embedding 1024 x 768 arange(0, T) on idx.device + broadcast over the batch h (nn.ModuleList) h.0 h.1 h.2 ... h.11 Block Block ... Block x12 ln_f (the extra GPT-2 layer norm) lm_head: Linear(768, 50257, bias=False) weight IS wte.weight, same data_ptr weight tying: ~40M of 124M parameters saved logits (B, T, 50257) cross_entropy against targets shifted by one expected loss at init: -ln(1/50257) = 10.82 Block, expanded clean residual stream x ln_1 LayerNorm(768) CausalSelfAttention c_attn: Linear(768, 2304) -> q, k, v 12 heads folded into the batch dim mask, softmax, att @ v, reassemble c_proj: Linear(768, 768) + the reduce: tokens communicate ln_2 LayerNorm(768) MLP c_fc: Linear(768, 3072) GELU(approximate='tanh') c_proj: Linear(3072, 768) std scaled by (2 * n_layer) ** -0.5 + the map: every token thinks alone x addition distributes gradients equally, so the top reaches the tokens unchanged
Figure 1. Under 100 lines of PyTorch, drawn out. Hugging Face's equivalent file is about 2,000 lines. Two choices on the right carry most of the video's teaching: the layer norms sit at the input of each sub block rather than inside the residual path, which is the one architectural change from the 2017 Transformer that GPT-2 made, and the residual stream itself is pure addition, which is what lets a gradient from the loss reach the token embeddings without passing through a single normalization.

Loading the Hugging Face parameters

The config gets set to the real GPT-2 124M numbers: block_size 1024, vocab_size 50257, n_layer 12, n_head 12, n_embd 768.

Then from_pretrained, a class method that returns a GPT object given a model type. He calls the loading code "kind of dry" and "not that exciting", and walks it anyway, because the two gotchas in it are exactly the kind of thing that silently ruins a reimplementation:

  1. Skip the buffers. attn.bias is not a parameter, it is the autoregressive mask. Ignore those keys.
  2. Transpose four weights. Because the checkpoint came out of the TensorFlow repo, some weights are transposed relative to what PyTorch wants. He hardcodes the list of names that need transposing and flips them. "I'm not sure how. This is a little bit annoying."

He runs it. No crash. Weights, biases and everything else load into his nn.Module.

The forward pass

The input is idx, token indices, always of shape B by T, where T cannot exceed the block size. B independent sequences stacked in a batch for efficiency.

Inside, he creates the positions with torch.arange and is careful to put them on idx.device, which is a detail that pays off twice later: once for CPU and Apple silicon support, and once as the reason a device mismatch bug does not happen here.

Position embeddings plus token embeddings. There is broadcasting hidden in that plus, because the position embeddings are identical for every row of the batch, so a dimension gets created and the two add. Then the blocks, then ln_f, then lm_head.

What comes out is logits of shape B by T by vocab_size. At every single position, the logits for what token comes next, which is "just a softmax away from becoming probabilities".

Sampling: tokenization, the loop, and top k 50

He sets up the exact same experiment as the Hugging Face pipeline: five sequences, 30 tokens, prefix "Hello, I'm a language model,".

model.eval() first, which is good practice when you are not training. Then an honest aside: he does not actually know if it is doing anything here, because nothing in this model has training versus evaluation behavior. Dropout and batch norm do; every layer he wrote should be identical in both modes. So model.eval() may be doing nothing, but he is not sure, and PyTorch internals may be doing something clever.

Then model.to('cuda'). He is SSHed into a cloud box with eight GPUs, and this ships every tensor off to what he describes as "basically a whole separate computer that is sitting on the GPU", with its own architecture, connected to the CPU and able to communicate with it, but well catered to the parallel processing that neural networks are.

Tokenization uses tiktoken and the GPT-2 encoding. The prefix comes out as eight tokens, which he cross checks by pasting the same string into Tiktokenizer. Those eight tokens get replicated five times into a five by eight tensor, moved to the GPU, and that is the starting idx.

The sampling loop, which runs under torch.no_grad() so PyTorch does not cache intermediates for a backward pass that will never come:

The generations:

Hello, I'm a language model, not a program. Andrej Karpathy's implementation, first sample, 39:55

Hello, I'm a language model, and one of the main things that bothers me when they create languages is how easy it becomes to create something that ... Andrej Karpathy's implementation, second sample, 39:55

They do not match the Hugging Face pipeline's output, and he cannot find the discrepancy on camera. "I can't find the discrepancy to be honest." He suspects there is something hiding in the pipeline in addition to the top p setting. So he does the thing you should do: he replicates the Hugging Face call path directly in the notebook, gets identical results there, and concludes the model internals are not wrong, he just does not know what the pipeline is doing.

That is the milestone. Every weight ported, this is the exact OpenAI GPT-2, and it generates sensible sequences. Now throw it away.

From random initialization, and auto detecting the device

Initializing from scratch turns out to be the easy part, because PyTorch already initializes randomly by default. Every linear layer has a default constructor, using for example the Xavier initialization covered in earlier videos. So model = GPT(GPTConfig()) is a randomly initialized 124M model, and the output is, as promised, "total garbage garbled", random token string pieces chunked up at random.

Before moving on he adds device auto detection, explicitly so that people without a GPU can follow along, at least until the multi GPU section at the end. The ladder goes by compute capability: start with CPU, which every computer has, then try CUDA, then try MPS, the Apple silicon backend, which on a fairly new MacBook gives you a GPU that is "actually fairly capable depending on which MacBook you have" and will beat CPU.

With device swapped in for the hardcoded 'cuda', forcing CPU still works, because the forward pass creates the position tensor on idx.device rather than assuming. A CPU generation takes about six seconds, without torch.compile and the rest, so following along on a laptop is viable.

For now, let's just say the device makes code go fast. Andrej Karpathy, deferring the whole topic of what PyTorch does when you call .to(device), 45:32

Let's train: data batches, loss, and crushing a single batch

Tiny Shakespeare, by the numbers

His favorite debugging dataset is tiny Shakespeare, and he gives it in full detail because the arithmetic matters later. Word count on the file: about 40,000 lines, about 200,000 words, about 1 million bytes. It is all ASCII, one byte per character, so roughly a million characters. The GPT-2 tokenizer has a compression ratio of roughly 3 to 1, so a thousand characters is about 300 tokens, and the whole file comes out at 338,000 tokens.

He tokenizes the first thousand characters and prints the first 24 token ids, and points out that if you can read GPT-2 tokens you will recognize 198 as the newline character, appearing twice in a row where the text has a blank line.

Making a (B, T) batch out of a one dimensional stream

The problem: a Transformer wants a batch of B independent sequences of up to T tokens, and what you have is one very long one dimensional sequence.

His favorite way to do it is a .view(). Take the first 24 tokens, view them as 4 by 6, and the first six tokens become the first row, the next six the second row, and so on. It stacks every six tokens as an independent row.

Then the labels. You could compute the targets inside the forward pass, since the next token is just one to the right, except that the very last token in the batch has no next token loaded, so you are one short. His fix, and the pattern worth stealing:

Now token 25's target is 198 and it sits at exactly the same position in the target tensor, and the last token 13 has its label too, because of that plus one.

Cross entropy, and the number you should expect at initialization

The forward pass grows an optional targets argument, returns (logits, loss), and calls F.cross_entropy. The reshaping in that call looks scary and is not: F.cross_entropy will not take a three dimensional B by T by vocab_size input, so the logits get flattened to two dimensions, B * T rows by vocab_size columns, and the targets get flattened to a single B * T tensor.

Then the sanity check, which is the most reusable fifteen seconds in this part of the video. At initialization you want the network to be maximally uncertain, so the probability of any arbitrary token should be roughly 1 over 50257. Cross entropy is negative log likelihood, so the loss you expect is the negative natural log of that, which is 10.82. He prints 11. Not way off, so the distribution at initialization is suitably diffuse and nothing is confidently wrong before training starts.

Overfit a single batch

With AdamW, not SGD. He explains the choice as a bug fix: "AdamW is a bug fix of Adam is what I would say." It keeps two buffers per parameter, m and v, which it calls the first and second moment, one looking a bit like momentum and one a bit like RMSProp, a normalization applied to each gradient element individually that speeds up optimization, especially for language models. He treats it as a black box here.

Two things he insists on in the loop:

Then the bug, left in. The run dies with expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu. The model was moved, the data buffer was not, and the reason the fix is not obvious is a real PyTorch asymmetry:

You have to be careful, because you can't just do buff.to(device). It's not stateful, it doesn't convert it to be a device. It instead returns a pointer to a new memory which is on the device. So you see how we can just do model.to(device), that does not apply to tensors, you have to do buff = buff.to(device). Andrej Karpathy, 1:00:21

Fixed, and the single batch gets crushed. Starting from 10.82 or 11, optimizing the same batch over and over with no new data, the loss goes to very very low. The Transformer is memorizing one batch, which is the point: it proves the whole forward and backward path works. Learning rate 3e-4, which he calls "a pretty good default for most optimizations that you want to run at a very early debugging stage".

DataLoaderLite

A deliberately minimal loader. Read the whole text file, tokenize it, print the total token count and the number of batches in a single epoch. Start at position zero, take chunks of B * T, always advance by exactly B * T, but always fetch B * T + 1 because of the target for the last token. Run off the end, loop back to zero.

He predicts the result before running it, which is the habit worth copying. The loss should come down but not to zero, and the reason is that of 50257 tokens many never occur in Shakespeare at all, so there are very easy gains available: drive the logits of all the never seen tokens towards negative infinity. All the crazy Unicode and the other languages. "That's probably most of the loss gain that we're going to see at this scale right now." Fifty iterations is also not enough for one epoch, which with B of 4 and T of 32 takes 2,600 batches.

The run: starts in familiar territory around 11, comes down to about 6.6. Exactly as called.

Two details from the real GPT-2: weight tying and the initialization

Weight tying, found by comparing pointers

He goes back to the state dict because he missed something while loading, and calls it a bug with respect to how GPT-2 training should happen.

The token embedding at the bottom of the Transformer and the language model head at the top are both 50257 by 768. Same shape, which could be coincidence. So he checks harder:

That is weight tying, and it comes from Attention Is All You Need, which says in its embeddings and softmax section that it shares the same weight matrix between the two embedding layers and the pre softmax linear transformation, citing an earlier 2017 paper that argues for the scheme. He calls that phrasing "an awkward way to phrase that these two are shared and they're tied and they're the same matrix", and he cannot find where Hugging Face does the tying, but he can find it in the original OpenAI GPT-2 TensorFlow source, where wte is used once at the bottom as the token encoder and again at the top in the matmul that produces the logits.

The intuition he gives for why it should work:

If two tokens are very similar semantically, like maybe one of them is all lowercase and the other one is all uppercase, or it's the same token in a different language or something like that, if you have similarity between two tokens presumably you would expect that they are nearby in the token embedding space. But in the exact same way you'd expect that if you have two tokens that are similar semantically you'd expect them to get the same probabilities at the output of a Transformer. Andrej Karpathy, 1:09:04

Both positions, the very bottom and the very top, want similar tokens to have similar weights. So tie them.

In the backward pass the shared tensor collects gradient contributions from both branches, from the classifier at the top and from the embedding lookup at the bottom, and they add up on it.

The implementation is one line: point wte.weight at lm_head.weight. That copies the reference, orphans the old value, Python cleans it up, and now a single tensor is used twice in the forward pass.

And the parameter count makes it more than an elegance argument. 768 times 50257 is about 40 million parameters, in a 124 million parameter model. Roughly 30 percent of the parameters saved. He offers that as a possible reason the scheme works slightly better if you are not training long enough: fewer parameters to train makes the process more efficient, and you are putting in a real inductive bias.

Initialization, read between the lines

Neither the GPT-2 nor the GPT-3 paper is explicit about initialization, so he goes to the released code instead of the paper, which he calls "quite vague".

What model.py in the OpenAI repo actually does:

His version uses nn.Module.apply to walk every submodule and initialize. Linear weights normal with std 0.02, biases zero, embeddings 0.02. He keeps embeddings at 0.02 rather than switching position embeddings to 0.01 because "it's about the same". He flags that zero initialization for the bias is not the PyTorch default, which is a uniform distribution. LayerNorm he leaves alone, because the PyTorch default is already scale one and offset zero, which is what you want.

Then the question of whether 0.02 is a sensible number at all. If you follow Xavier, the standard deviation would be one over the square root of the incoming feature count. Run the arithmetic on the GPT-2 sizes and 0.02 lands right in the middle of the range:

So the hardcoded 0.02 is "not completely crazy", though typically you would want something that scales with model size. He keeps it, because it is what GPT-2 did.

The residual initialization scaling, derived on screen

One caveat remains, from the GPT-2 paper itself:

A modified initialization which accounts for the accumulation on the residual path with model depth is used. We scale the weights of residual layers at initialization by a factor of one over the square root of N, where N is the number of residual layers. The GPT-2 paper, read out in the video at 1:17:15

Rather than just implementing it, he motivates it in the notebook, and this is one of the clearest two minutes in four hours.

Start with a residual stream of 768 zeros. The stream has the form x = x + something, so every block contributes some amount and it gets added. Now add a standard normal draw, zero mean and unit standard deviation, one hundred times. By the end the residual stream has a standard deviation of 10, which is the square root of 100. The variance of activations in the residual stream grows with depth, because you keep adding.

Scale each contribution by one over the square root of n, which is n ** -0.5, and the standard deviation comes back to 1. The paper's factor exactly compensates for the growth.

His implementation is, in his own words, possibly not PyTorch sanctioned but it works: set a flag attribute NANOGPT_SCALE_INIT = 1 on the modules that write into the residual stream, which is the c_proj at the end of each attention and each MLP, then in the init function check for the flag and multiply the standard deviation by (2 * config.n_layer) ** -0.5. "There must be a better way in PyTorch, right? But I don't know."

Why two times the number of layers? Because every layer in the Transformer has two blocks that add into the residual pathway, the attention and the MLP. That is where the factor of two comes from.

One awkwardness he flags and chooses not to fix. Because wte and lm_head are tied, the module walk comes around to that same tensor twice: once as an embedding, initialized to 0.02, and once as a linear, initialized to 0.02 again. Same value both times, since lm_head carries no scale flag, so it is harmless, just initialized twice identically.

Seeds get set for reproducibility, and that is the GPT-2 initialization as faithfully as the public record supports.

SECTION 2: let's make it fast

This is the center of the video, and the method is as important as the content. Every change is applied on its own, timed, and compared against the number before it. Nothing is taken on faith, and twice the speedup he gets is nothing like the speedup the hardware promised, which turns into the single most useful lesson in the section.

You always want to start with: what hardware do you have, what does it offer, and are you fully utilizing it? Andrej Karpathy, 1:22:26

The hardware, and the napkin math

nvidia-smi shows eight A100 SXM 80GB GPUs. He rents these from Lambda Labs, which he names as his favorite place to spin up a box, pay by the hour, and connect VS Code to, and discloses that they sponsor his development and his projects.

He sets the benchmark configuration: batch size 16, sequence length 1024, the real GPT-2 maximum, on tiny Shakespeare. At 16 by 1024 the model occupies 35 GB of the 80 available, and one epoch of Shakespeare is only 20 batches.

Then he breaks into the code right after the loss, prints logits.dtype, and gets torch.float32. By default everything in PyTorch, every activation and every parameter, is a 32 bit float. That is a lot of memory, and:

It turns out empirically that for deep learning as a computational workload this is way too much. Andrej Karpathy, 1:24:02

From the A100 datasheet, with his commentary:

Then the fact the whole section rests on. Lower precision does not just make the multiply faster. Fewer bits per number means less data to move, and that is where memory bandwidth comes in. The A100 can move about 2 terabytes per second, which is a lot, and it is still the binding constraint:

Many of the deep learning workloads for training are memory bound, and what that means is actually that the tensor cores that do all these extremely fast multiplications, most of the time they're waiting around, they're idle, because we can't feed them with data fast enough. Andrej Karpathy, 1:27:06

His rule of thumb: if you are getting 60 percent hardware utilization you are doing extremely well. Half the time, in a well tuned application, your tensor cores are not multiplying anything.

Tensor cores and TF32: 1,000 to 333 milliseconds

A tensor core is just an instruction in the A100 architecture, and what it does is a little 4 by 4 matrix multiply. There are multiple configurations for the input precisions, the internal accumulate precision and the output precision, but it is fundamentally a 4 by 4 multiply, and any matrix multiplication gets broken up into it because that is the fastest way to multiply matrices.

That matters because almost all of the computational work here is matrix multiplication, hidden inside the linear layers. There are additions in the residuals, GELU nonlinearities, layer norms, but time them and they are nothing. And at this small scale the single biggest matmul by a distance is the classifier at the top, 768 going to 50257, which dominates everything else in the network.

For the mechanism he goes to the A100 architecture whitepaper, figure 9, which he calls relatively readable if you half understand what is happening. TF32 is:

So nothing in your PyTorch code changes and every number still looks identical. All you do is let the tensor core crop 13 bits inside the operation, and the little matrix multiply goes 8 times faster.

The reason I like TF32 is because if you can tolerate a little bit of a precision fudge then this is free. Like none of your code sees this, it's fully internal to the operation, and the operation to you just goes 8x faster. Andrej Karpathy, 1:32:17

Before timing, he fixes a trap that catches everyone. When the CPU runs, it is only scheduling work on the GPU, so it queues kernels and races ahead. You have to call torch.cuda.synchronize() to wait for the GPU to finish everything that was scheduled before you take the time. He also notes the first iteration is often slower, because PyTorch is doing initializations, allocating tensors and buffers for the gradients, so be careful when timing.

He also adds tokens per second as the metric, on the grounds that it is the objective measure, since the batch size will change over the course of the video and milliseconds per step will not be comparable.

The float32 baseline: roughly 1,000 milliseconds per iteration, about 163,000 tokens per second.

Enabling TF32 is one line, torch.set_float32_matmul_precision('high'). The default is 'highest', which keeps everything in float32; 'high' lets matmuls use TF32 where the hardware has it, which on an Ampere A100 it does.

Promised: 8 times. Delivered: 1,000 down to about 333 milliseconds, roughly 50,000 tokens per second. About 3 times.

And the explanation is the lesson:

Even though the TF32 offers in principle a lot faster throughput, all of these numbers everywhere are still float32s, and it's float32 numbers that are being shipped all over the place through the memory system, and it's just costing us way too much time to shuttle around all this data. So even though we've made the multiply itself much faster, we are memory bound. Andrej Karpathy, 1:38:29

Still, a 3 times throughput improvement for one line of code, with every variable still float32 everywhere.

bfloat16: 333 to 300 milliseconds

If the problem is that we are still moving 32 bit numbers around, move 16 bit numbers instead. Which raises the question of which 16 bit format, and the answer is the clearest explanation of bfloat16 versus float16 in any lecture:

He follows the PyTorch mixed precision recipe, which he recommends specifically because "there are five other copies that I would not recommend", and the guidance is narrow:

Since his loss is computed inside the model's forward, the context manager wraps one call. Then he breaks in to show exactly what changed, and the answer is "not everything", which is why it is called mixed precision:

What gets cast and what does not is, he admits, not super clear. Matrix multiply like operations get converted. A lot of operations stay in float32, in particular normalizations like layer norms, and softmax, log softmax and the loss calculation, because those are more susceptible to precision changes while matmuls are fairly robust to them. "Unfortunately they don't document it very well, so we're not going to go into that in too much detail."

Result: 333 down to about 300 milliseconds, about 55,000 tokens per second. "We're definitely running faster, but maybe not a lot faster." Because there are still many, many bottlenecks, and we are only getting started.

torch.compile: 300 to 129 milliseconds

torch.compile is really quite incredible infrastructure from the PyTorch team, and it's basically a compiler for neural networks. Like it's almost like GCC for C and C++ code. This is just the GCC of neural nets. Andrej Karpathy, 1:48:10

One line, model = torch.compile(model). It costs compilation time, which he spends explaining what it does, and the result when it lands is 300 down to 129 milliseconds, which he calls about a 2.3 times improvement from a single line of PyTorch. Throughput about 125,000 tokens per second. His verdict is unambiguous: there is no real good reason not to use torch.compile, you should be using it almost by default, unless you are debugging.

The line from the documentation that he says actually explains it: the speedup mainly comes from reducing Python overhead and GPU read writes. Both halves get unpacked.

Python overhead. An nn.Module is just the algorithmic description of what you want to happen. Without the compiler, PyTorch runs in eager mode: the Python interpreter walks the forward pass layer by layer, dispatching and materializing each operation as it goes, with no idea what operations come later. torch.compile sees the whole thing at once, knows exactly what you intend to run, and takes the Python interpreter out of the forward pass entirely, compiling the network as a single object.

GPU read writes, which is the bigger half, and he builds it from the GELU. He writes out the tanh GELU formula by hand, as the equivalent of calling nn.GELU, and walks what happens without the compiler:

  1. The interpreter hits x ** 3, dispatches a kernel. The input lives in the GPU's high bandwidth memory. It has to travel to the cores, the caches and the registers on the chip, get cubed, and the result gets saved back to memory.
  2. Multiply by a constant: another kernel, another trip out, another trip back.
  3. Add the input back: travels again, adds, written back.

Every elementwise step is a full round trip to memory, because all the tensor cores and the arithmetic units are on the chip while the data is not. With the compiler, the data makes one trip to the chip, all the elementwise operations happen while it is sitting there, and it is written back once. That is kernel fusion, and it is the major way everything gets sped up.

He then supplements with the memory hierarchy, calling it a preview of what could be its own two hour video:

The three diagrams he walks through, in his order, are worth having in your head whenever you are reasoning about why a kernel is slow:

The two chips. The GPU chip is where almost all the calculation happens, and it does contain some memory, but most of the memory by far is on a physically separate HBM chip beside it. They are connected and they are not the same thing. Everything is super fast within the chip; going to the memory is extremely expensive and takes an extremely long amount of time.

The zoom in. On the sides of the die are the links to HBM. On the die are the streaming multiprocessors, 120 of them, and zooming into one shows four quadrants. The tensor core is where the matrix multiply work happens, and around it are separate units for FP64, FP32 and integers, because they are genuinely different hardware. The L2 cache lives on the die; L1 cache and registers live on each SM.

The capacity ladder, which is the punchline. A CPU might have a terabyte of DRAM, and for a GPU reaching it is extremely expensive because you have to go through the CPU. A typical GPU has tens of gigabytes of HBM, which is also, as he keeps saying, very expensive to access. And on the chip itself, across the L2, all the L1 caches and all the registers together, there are only a couple of tens of megabytes. The memory that is lightning fast is the memory there is almost none of, because on chip memory is expensive to build, and the implementation is different in kind: transistors and capacitors in HBM versus SRAM on the die.

So the accurate picture of a kernel is: inputs live in global memory, data streams to the chip, the calculation happens, the result streams back. Without fusion you make that round trip many, many times. With fusion a chunk of data lives on the chip, you do all the operations on it right there in an elementwise fashion, which is very cheap, and you take a single round trip back. "That gives huge savings, and that's why torch.compile ends up being a lot faster."

Flash attention: 130 to 96 milliseconds

And then the operation torch.compile cannot find.

FlashAttention came out of Stanford in 2022. It replaces four lines of his attention implementation with one, and it is a kernel fusion, but it is a kernel fusion the compiler cannot discover, because it requires an algorithmic rewrite of how attention is implemented.

The remarkable part, stated plainly:

Flash attention actually, if you just count the number of FLOPs, flash attention does more FLOPs than this attention here. But flash attention is actually significantly faster. In fact they cite 7.6 times faster potentially. Andrej Karpathy, 2:01:37, on the FlashAttention paper's own numbers

It is faster because it is mindful of the memory hierarchy he just drew. It is careful about what sits in high bandwidth memory and what sits in shared memory, and it orchestrates the computation so that there are fewer reads and writes to HBM. More FLOPs, fewer expensive loads and stores, and the loads and stores are the expensive part.

Specifically, it never materializes the T by T attention matrix. That matrix is where all the queries and keys interact, and for each head and each batch element at T of 1024 it is a million numbers. FlashAttention is designed so that matrix never exists at any point and is never read from or written to HBM.

The mechanism is the online softmax trick, with intermediate variables M and L and an update rule that lets you evaluate a softmax incrementally without ever realizing all of its inputs to do the normalization. FlashAttention-2 adds further gains on top.

And then the piece of history he clearly enjoys. The online softmax paper this is built on is Online normalizer calculation for softmax, and it came out of NVIDIA in early 2018, four years before FlashAttention. Its abstract proposes a way to compute the classical softmax with fewer memory accesses and hypothesizes that the reduction in memory accesses should improve softmax performance on actual hardware.

They are extremely correct in this hypothesis, but it's really fascinating to me that they're from NVIDIA and that they had this realization but they didn't actually take it to the actual flash attention that had to come four years later from Stanford. So I don't fully understand how this happened historically. Andrej Karpathy, 2:04:10

In PyTorch you get it by calling F.scaled_dot_product_attention, the compound operation, and PyTorch dispatches to flash attention. He finds it a little bit odd that torch.compile cannot see that his four lines should become exactly that call, and says so.

Before the change, step 49 gave a specific loss at 130 milliseconds. After, the same loss "basically identical up to a floating point fudge factor", at about 95 to 96 milliseconds. The computation is the same, the kernel is faster, and he puts the improvement at 96 over 130, roughly 27 percent.

Nice and ugly numbers: 50257 to 50304, 96.5 to 93 milliseconds

We are now getting to one of my favorite optimizations, and it is simultaneously the dumbest and the most brilliant optimization, and it's always a little bit surprising to me. Andrej Karpathy, 2:06:48

The premise: 64 is a beautiful number, 128 is even nicer, 256 is beautiful. What makes a number beautiful is having many powers of two inside it, so you can halve it repeatedly. Ugly numbers are 13, 17, primes, anything odd. And you always want nice numbers in code that deals with neural networks or CUDA, because everything in CUDA works in powers of two, kernels are written in terms of them, and blocks come in sizes like 16 and 64. Everything else gets special case handling.

So the heuristic is literally: scan your code and look for ugly numbers. His own audit, out loud:

The fix for the vocabulary size is to raise it to the nearest nice number above: 50304, which divides by 8, 16, 32, 64 and even 128. One line in the config. He is explicit that this increases the amount of computation the network does, more FLOPs by napkin math, and then thinks through whether it breaks anything while the compile runs:

So functionally nothing breaks. We're using a bit more extra memory, but otherwise this is a harmless operation as far as I can tell. And we're adding calculation, but it's running faster. Andrej Karpathy, 2:12:55

Result: 96.5 down to 93 milliseconds, roughly 4 percent, by doing more arithmetic.

Why it works, in his explanation: CUDA kernels chunk your input into block tiles of nice sizes, powers of two, calculations happen in chunks of 32 or 64. When your desired calculation does not fit neatly into those tiles, boundary kernels kick in to handle the leftover. In a lot of kernels they do the nice part first, then a whole second phase comes back for the remainder, and the kernels for that can be very inefficient, spinning up extra compute for a sliver of work. So you may as well pad your input and make it fit, and empirically that runs faster.

One caveat that matters enormously if you reproduce this today, and which he gives precisely:

I also have to point out that we're using PyTorch nightly, so that's why we're only seeing 4 percent. If you're using PyTorch 2.3.1 or earlier you would actually see something like 30 percent improvement just from this change, from changing it from 50257 to 50304. Andrej Karpathy, 2:14:27

At that point the running total is about 11 times, from about 1,000 milliseconds per step to 93.

milliseconds per step, lower is better. one A100 80GB, B=16, T=1024 0 200 400 600 800 1000 float32 baseline 1000 ms · 163k tok/s TF32 matmuls 333 ms · 50k tok/s · 3x, not the promised 8x bfloat16 autocast 300 ms · 55k tok/s torch.compile 129 ms · 125k tok/s · 2.3x from one line flash attention 96 ms · 27 percent, and more FLOPs than before vocab 50257 to 50304 93 ms · 4 percent on nightly, ~30 percent on 2.3.1 fused AdamW 90 ms · one kernel for the whole update 1000 ms to 90 ms: about 11 times, all of it measured, none of it algorithmic
Figure 2. The whole of section 2 in one picture, in the order he applies it, with his own measured numbers. Two bars are the lesson rather than the win. TF32 promises 8 times on paper and delivers 3, because the data being shuttled around is still float32 and the GPU is starved, not slow. Flash attention delivers 27 percent while doing more arithmetic than the code it replaces, because it never writes the attention matrix to memory. The bottleneck in this chart is almost never the math.

SECTION 3: the GPT-3 recipe, borrowed line by line

With the hardware wrung out, he turns to algorithmic changes, and the entire section is him reading the GPT-3 paper's appendix and implementing each sentence in turn. The reason is the asymmetry from the top of the video, restated:

GPT-2, we have the weights but no details. GPT-3, we have lots of details but no weights. Andrej Karpathy, 2:15:59

GPT-2's paper is "extremely vague as to the optimization details", and the code OpenAI released is inference only, with no training code and very few hyperparameters. So the recipe comes from GPT-3, which is fine because the two architectures are very very similar: context length went from 1024 to 2048, some Transformer hyperparameters moved, and GPT-3 is 175 billion instead of 1.6 billion and was trained far longer on more data with more thorough evaluation. Otherwise, pretty much the same model.

AdamW betas and epsilon

The GPT-3 paper: "To train all versions of GPT-3 we use Adam with beta one, beta two of 0.9 and 0.95."

PyTorch's AdamW defaults to betas of 0.9 and 0.999, so the second one changes. Epsilon is 1e-8 in the paper, which happens to match the PyTorch default, and he sets it explicitly anyway.

Gradient clipping at a global norm of 1.0

The paper: "We clip the global norm of the gradient at 1.0."

One line, torch.nn.utils.clip_grad_norm_, inserted right after loss.backward(). What it computes: take every gradient on every parameter, square it, add it all up, take the big square root. That is the norm of the parameter gradient vector, its length. Then make sure the length is no more than 1.0, and clip it if it is over.

Why people do it: sometimes you get unlucky, maybe a bad data batch, and an unlucky batch gives you a really high loss, which gives you a really high gradient, which shocks your model and shocks the optimization. Clipping upper bounds the magnitude of the shock. His honest assessment of the technique:

It's a bit of a hacky solution, it's about like a patch on top of like deeper issues, but people still do it fairly frequently. Andrej Karpathy, 2:18:31

And then the piece of advice buried in it, which is worth more than the clipping. clip_grad_norm_ returns the norm, and he always prints it, because it is useful information. A well behaved norm means things are good. A climbing norm means things are bad and destabilizing during training. A spike means there is some kind of issue or instability.

On this run the norm comes out high at the start, around 30, and stabilizes below one as training continues. That is not uncommon early on: the model is completely random, there is a ton of learning happening very early, and most of what it is learning is the biases of the output tokens, which is an unstable time. The network usually stabilizes in a very few iterations. He still looks at the actual sequence, 28 then 6 then 2 then 10, and calls it "not completely insane but just kind of a little bit funky".

The learning rate schedule: warmup then cosine decay

The paper: cosine decay for the learning rate down to 10 percent of its value over the first 260 billion tokens, then training continues at 10 percent after that, with a linear warmup over the first 375 million tokens.

He implements it himself rather than using PyTorch's schedulers, and says why: it is five lines of code and he fully understands what is happening inside them. "I don't love to use abstractions where they're kind of inscrutable and then I don't know what they're doing." Personal style, stated as such.

The shape: start at essentially zero, ramp up linearly, come down with a cosine form to a minimum learning rate that is up to you. Setting the learning rate in PyTorch he calls "a little bit gnarly", because you have to iterate over the optimizer's parameter groups and set it in a for loop, even though there is currently only one group.

The peak value comes straight out of the GPT-3 table of per model hyperparameters, and this is a real change. His debugging default was 3e-4. GPT-3 Small, which is 12 layers and 768 dimensions and therefore roughly a GPT-2 124M, uses 6e-4. So the max learning rate doubles, and the minimum is 10 percent of the max per the paper's description.

A small implementation detail with a reason: the warmup uses it + 1, so that on the zeroth iteration you are not using exactly zero, because updating with a learning rate of zero would not be useful.

And one place where he knowingly departs from the paper. GPT-3's decay horizon is shorter than its training horizon: it decays to 10 percent at 260 billion tokens and then trains the remaining 40 billion at 10 percent. In his implementation the decay time and the max steps are exactly equal. "So it's not exactly faithful, but it's okay for us and for our purposes right now. I don't think it makes too, too big of a difference honestly."

He also notes the whole question is open: cosine was popularized by GPT-2 and GPT-3, people have come up with all kinds of other schedules, and which one is most effective is an active area of research.

The batch size ramp, deliberately skipped

The paper describes a gradual, linear batch size increase: start very small and ramp up to a big batch size over time. He skips it, and gives three reasons, in order:

  1. It complicates the arithmetic. You are changing the number of tokens processed at every single step of the optimization, and he likes to keep that math very simple.
  2. It is not a major improvement.
  3. It is not an algorithmic improvement, it is a systems and speed improvement. And then the argument for why, which is the interesting part:

Early in the optimization the model is in a very atypical setting. Mostly what it is learning is to ignore the tokens that do not come up in the training set very often, and some very simple biases. So every single example you put through the network is basically just telling you "use these tokens and don't use these tokens", which means the gradients from every example are extremely highly correlated. They all look roughly the same.

Why are you doing batch sizes of like millions when, if you do a batch size of 32k, you're basically getting the exact same gradient early on in the training? And then later in the optimization, once you've learned all the simple stuff, that's where the actual work starts, and that's where the gradients become more decorrelated per example, and that's where they actually offer you sort of statistical power in some sense. Andrej Karpathy, 2:27:43

Sampling without replacement, already satisfied

The paper says data is sampled without replacement during training until an epoch boundary is reached. He walks through what that means: they are not drawing a sequence from a fixed pool and returning it, they are exhausting a pool, so a drawn sequence is gone until the next epoch. His loader already iterates over chunks of data in order, so there is no replacement and nothing becomes eligible again until the next pass. Nothing to implement.

Weight decay 0.1, on two dimensional parameters only

The paper: all models use a weight decay of 0.1 to provide a small amount of regularization.

PyTorch's AdamW default is 0.01, so this is ten times higher than the default. Rather than passing it in flat, he writes a configure_optimizers method on the model that returns the optimizer, because the decay needs to be applied selectively. The split:

The reason to decay at all, which he covered in an earlier video and recaps here: you can view it as a regularization, because pulling down all the weights forces the optimization to use more of the weights and does not allow any single weight to get way too large. It forces the network to distribute the work across more channels. "There's sort of like a pull of gravity on the weights themselves."

The counts his script prints: 50 decayed tensors, holding most of the parameters, and 98 non decayed tensors, which are mostly the biases and the layer norm parameters and amount to only about 100,000 parameters.

Fused AdamW: 93 to 90 milliseconds

The last of the pure speed wins, and it sneaks in here. torch.optim.AdamW grew a fused option in a later PyTorch version, and because it did not always exist he guards it with inspect.signature, checking whether fused is a valid keyword before passing it.

What it does: instead of iterating in a for loop over every parameter tensor and updating it, which launches a lot of kernels, all those kernels are fused into one. A single kernel call updates all the parameters, and all that launch overhead disappears. It is kernel fusion for the AdamW update specifically.

PyTorch does not default to it, because it was relatively new and they wanted to give it sufficient bake time, but it is a lot faster when it is available and you are running on CUDA. His advice: if you have it, use it. "I'm not actually 100 percent sure why they don't default to it, it seems fairly benign and harmless."

Result: 93 down to 90 milliseconds per step.

One closing honesty note on this whole section, which is easy to skip past and should not be:

The relationship between weight decay, learning rate, batch size, the Adam parameters beta one beta two, the epsilon and so on, these are very complicated mathematical relationships in the optimization literature, and for the most part in this video I'm just trying to copy paste the settings that OpenAI used. But this is a complicated topic, quite deep. Andrej Karpathy, 2:34:24

SettingPyTorch defaultGPT-3 paperWhat he ships
OptimizerAdamWAdam (he reads it as AdamW)AdamW, fused=True when available
betas0.9, 0.9990.9, 0.950.9, 0.95
eps1e-81e-81e-8, set explicitly
Gradient clippingnoneglobal norm 1.01.0, and he prints the returned norm every step
Peak learning rate1e-36e-4 for GPT-3 Small, 12 layers and 768 dims6e-4, up from his 3e-4 debugging default
Minimum learning raten/a10 percent of peak10 percent of peak
Warmupnonelinear over the first 375 million tokens715 steps, which is 375e6 divided by 2^19. He calls it very mild and says 100 would probably do
Decay schedulenonecosine to 10 percent over the first 260 billion of 300 billion tokens, then flatcosine over the full run: decay time equals max steps, knowingly unfaithful, judged not to matter
Weight decay0.010.10.1, applied only to parameters with 2 or more dims. 50 tensors decayed, 98 not, the 98 holding about 100,000 parameters
Total batch sizen/a0.5M tokens for GPT-3 Small524,288 = 2^19, reached by gradient accumulation
Batch size rampn/alinear ramp from small to largeskipped: complicates the arithmetic, and early gradients are highly correlated anyway
Sequence lengthn/a20481024, GPT-2's value. He gives the exact change for fidelity: T=2048 and micro batch 32, so they still multiply to half a million
Data samplingn/awithout replacement until an epoch boundaryalready satisfied by iterating chunks in order
Training tokensn/a300 billion10 billion for the main run, 40 billion overnight. Both beat GPT-2 124M
Figure 3. The recipe, by provenance. Green is what he takes from GPT-3 unchanged; amber is where he knowingly departs and says so on camera. Four of the five departures are about fitting a four hour lecture and a ten dollar budget rather than about disagreeing with the paper, and he gives the exact edit for anyone who wants the faithful version. The one substantive disagreement is the batch size ramp, which he argues is a systems win rather than an algorithmic one.

Gradient accumulation, and the normalization bug that catches everybody

The GPT-3 table lists a batch size per model, and the pattern across the sizes is clear: bigger networks get slightly lower learning rates and bigger batch sizes. GPT-3 Small uses 0.5 million tokens per batch.

He does the division out loud, because the units trip people. Half a million is a count of tokens, and every row is 1024 tokens, so 0.5e6 divided by 1024 is a batch of about 488 rows. "The problem is I can't come in here and set this to 488 because my GPU would explode. This would not fit for sure."

But he still wants that batch size, and the reason is not stubbornness. The batch size is correlated with all the other optimization hyperparameters, the learning rates among them, so if you want a faithful representation of the recipe you need the batch size the recipe assumes.

The answer is gradient accumulation, which simulates in a serial way any arbitrary batch size you set. Run many forward and backward passes, let the gradients add up, then do a single update.

The arithmetic as he sets it up:

Then the subtlety. He writes the obvious inner loop, 32 micro steps of forward and backward before everything else, pauses, and says this is actually incorrect, and invites you to work out why before he fixes it. The demonstration is in a notebook, on a toy problem, and it is the clearest treatment of this bug anywhere:

The reason: MSELoss defaults to reduction='mean', so the real objective has a one quarter in front of it, averaging over the four examples. In the accumulation version each loop's objective is a single example's squared error with no one quarter, and accumulating gradients is equivalent to a sum in the loss. So the accumulated version is missing the normalizer.

The fix is one line: loss = loss / 4. That puts the one quarter back in front of every individual loss, and when they accumulate by summing, every component carries its quarter. Run it again and the gradients are now identical.

Which maps straight onto the real model, because F.cross_entropy also defaults to a mean reduction, over all B * T elements. So:

loss = loss / grad_accum_steps

In the same way exactly, we are scaling down the loss so that when we do loss.backward, which basically corresponds to a sum in the objective, we are summing up the already normalized loss, and therefore when we sum up the losses divided by grad accum steps we are recovering the additional normalizer. Andrej Karpathy, 2:44:36

Two cleanups follow. Printing needs a loss_accum variable initialized to zero and accumulated into, using .detach() so the tensor comes off the graph and he is just tracking values, because otherwise he would be printing only the final micro step's loss. And the tokens processed per step is now B * T * grad_accum_steps.

And a nice consequence he points out: once you have a total batch size and gradient accumulation, the micro batch B becomes purely a performance knob. Big GPU, set it to 32 and go a bit faster. Very small GPU, try 8 or 4. You get the exact same optimization and the same answers up to floating point error, because the accumulation handles everything serially.

Eight GPUs: distributed data parallel

Now is the time to bring out the heavy weapons. You've noticed that so far we've only been using a single GPU for training, but actually I am paying for eight GPUs here, and so we should be putting all of them to work. Andrej Karpathy, 2:46:38

The tool is PyTorch's DistributedDataParallel. He flags up front that there is also a legacy DataParallel and recommends you not use it.

The model is simple to state. Eight GPUs means eight processes, one assigned to each GPU. Each process runs the training loop exactly as built so far, as far as it is concerned nothing has changed, except that secretly there are eight of them, they are each processing slightly different parts of the data, and one new step is added at the end: the gradients get averaged across all of them.

Launching and the environment variables

You no longer run python train_gpt2.py. You run torchrun, which launches eight copies in parallel and sets environment variables so each process can look up which one it is:

torchrun --standalone --nproc_per_node=8 train_gpt2.py

Three variables matter:

The presence of RANK in the environment is also, he notes, a somewhat bad way to detect whether DDP is running. If it is not set, the script falls back to single GPU: rank zero, world size one, master process true, autodetect the device, business as normal.

local_rank sets the device to cuda:<local_rank>, so no two processes collide on the same GPU. And he creates a boolean he uses everywhere after:

master_process = ddp_rank == 0. Process zero, arbitrarily, does all the printing, logging and checkpointing. The others are thought of as compute processes that assist.

Reading code with eight interpreters in your head

The advice for the rest of the section is the practical kind:

The tricky thing with running multiple processes is you always have to imagine that there's going to be eight processes running in parallel. So as you read the code now you have to imagine there's eight Python interpreters running down these lines of code, and the only difference between them is that they have a different DDP rank. Andrej Karpathy, 2:51:41

They all come to the same lines, all pick the same seed, all build the identical model, "completely unaware of the other copies running". So every calculation that depends on how much data exists has to be adjusted for world size and rank.

What actually changes

Gradient accumulation steps. Now total_batch_size / (B * T * ddp_world_size). With 16 by 1024 on 8 GPUs that is 131,072 tokens in a single forward and backward across the box, and 524,288 divided by 131,072 gives grad_accum_steps = 4, down from 32. He checks the division comes out clean, which it does.

Printing. Eight processes hit every print and you get eight copies, so everything informational gets guarded by if master_process. He demonstrates the failure first, with a script that just prints its rank and exits, and the output is instructive: process 5 prints first "just by chance", then zero, then three and two, and because a process exits without destroy_process_group, DDP complains that the process group has not been destroyed before destruction. In a real application you want to call destroy_process_group() so you clean up properly and NCCL does not complain. The ordering is not something you can guarantee; it depends on how the operating system scheduled the processes.

The data loader. Every process must get its own chunk, so the loader takes the rank and the number of processes:

Wrapping the model. model = DDP(model, device_ids=[ddp_local_rank]). He notes the documentation is extensive, full of caveats, and that everything complexifies by a factor of ten when multiple processes are involved. He also notes the docs for device_ids specifically are "extremely unclear" and the comment explaining it is "roughly nonsensical", but he is pretty sure it has to be the local rank, not the rank.

What DDP does for you: the forward pass behaves identically, nothing changes there. In the backward pass, once the backward is over on each independent GPU, each GPU has gradients for all parameters, and DDP calls an all reduce, averaging across all the ranks and depositing that average back on every rank. It is a bit more involved than that, because as the backward pass moves through the layers of the Transformer it can dispatch the communication for gradients that are already done while the backward is still running, so the communication overlaps the computation. More efficient that way.

raw_model. model.configure_optimizers no longer works, because model is now a DDP wrapper. The real module is at model.module, so he keeps a raw_model reference right after wrapping and calls configure_optimizers on that.

The synchronization problem, and the naughty fix

Here is the part that matters most, and the part he is openly unhappy about.

By default DDP synchronizes gradients after every loss.backward(). But inside a gradient accumulation loop that is extremely wasteful: for the first 31 micro steps, or the first 3 on eight GPUs, you are only depositing gradients locally and you do not want to pay for an all reduce. You want to add them up locally, and all reduce exactly once, on the very last micro step.

PyTorch's sanctioned way is the no_sync() context manager, a context manager that disables gradient synchronization so gradients accumulate without communication, and then you do the final step outside it.

They are asking us to do with ddp.no_sync(), do the gradient accumulation, accumulate grads, and then they are asking us to do DDP again with another input and backward. And I just really don't love this. I just really don't like it, the fact that you have to copy paste your code here and use a context manager. This is just super ugly. Andrej Karpathy, 3:03:24

So he reads the source, finds that entering the context manager simply toggles a variable called require_backward_grad_sync, and sets that variable directly instead, right before loss.backward(), true only when the micro step is the last one.

He is completely upfront that this is a hack:

This is a naughty thing to do, because they could probably change the DDP and this variable will go away. But for now I believe this works, and it allows me to avoid the use of context managers and code duplication. Andrej Karpathy, 3:04:25

The loss also has to be averaged

One more thing that falls out of the gradients being averaged. loss_accum lives outside the DDP container, so it is not averaged. If you print it on the master process you are printing only the loss that rank zero happened to see on its own slice of the data. Since the gradients are averaged, the loss you report should be too.

So: import torch.distributed as dist, then dist.all_reduce(loss_accum, op=dist.ReduceOp.AVG). The tensor exists on every rank, the all reduce creates the average and deposits it on all of them, and now the number the master process prints is the same number every rank holds.

And finally the token counter has to be multiplied by the world size as well, because the box really is processing that many more tokens.

The run

8 GPUs, grad accum of 4, and the throughput is 1.5 million tokens per second. "Wow, we're going really fast. These are some serious numbers." Tiny Shakespeare at 338,000 tokens is now so small that it is being looped over many times per minute, which is the signal that it is time for a real dataset.

A last consistency check he does, and it is a good one, because the numbers do not match between a single GPU doing 32 accumulation steps and eight GPUs doing 4. The reason is boring: the loader is looking for an entire page of data for all eight GPUs at once, and when that chunk exceeds the remaining tokens it loops, so the single GPU run and the eight GPU run reset at slightly different places and see slightly different batches.

To convince himself nothing is wrong he shrinks the total batch size to 32,768, which is 4 by 1024 by 8, so the single GPU does 8 accumulation steps and the eight GPU run does one each. That reduces the boundary effects of the data loader, and the numbers match.

Data loader FineWeb-Edu shards stride = B*T*world Forward bfloat16 autocast compiled + flash attention loss /= grad_accum_steps Backward grads += (deposit) require_backward_ grad_sync = False Optimizer step all_reduce AVG over 8 clip_grad_norm_ 1.0 fused AdamW, cosine LR inner loop: 4 micro steps per GPU on 8 GPUs, 32 on 1 GPU. no update, no communication on the LAST micro step only: sync on, gradients averaged, one update 1 step = 524,288 tokens = 2^19 16 x 1024 per GPU x 8 GPUs x 4 accumulation steps = 524,288 10B tokens / 2^19 = 19,073 steps = 1 epoch = about 1.7 hours at 330 ms/step 375M warmup tokens / 2^19 = 715 warmup steps throughput: 1.5 million tokens per second across the 8 ranks Eight ranks run this entire picture in parallel, each on its own stride of the shards. Only rank 0 prints, logs and checkpoints.
Figure 4. The finished step, with the exact numbers the final run uses. Every box in the middle two was an optimization target in section 2 and every label on the right came out of the GPT-3 paper in section 3. The one line that is easiest to get wrong is in amber in the forward box: because cross entropy reduces by mean and accumulation sums, the loss has to be divided by the accumulation count or the gradients are silently the wrong size.

The datasets: from Shakespeare to FineWeb-Edu

With the machinery finished, tiny Shakespeare has been outgrown, and he goes and looks at what GPT-2 and GPT-3 actually trained on.

GPT-2 used WebText, and it was never released. The paper's description: they scraped all outbound links from Reddit with at least three karma, and that was the starting point. 45 million links, collected, text extracted, ending up at 40 GB of text. There is a reproduction attempt called OpenWebText.

GPT-3 used a mixture, and it was never released either. The GPT-3 paper has a training dataset section where Common Crawl gets discussed properly. His assessment of Common Crawl by itself is blunt:

It's not a very high quality dataset all by itself, because it is extremely noisy. This is a completely random subset of the internet and it's much worse than you think. So people go into great lengths to filter Common Crawl, because there's good stuff in it but most of it is just like ad spam, random tables and numbers and stock tickers, and it's just total mess. Andrej Karpathy, 3:11:09

Which is why people train on curated data mixtures. Typically a large chunk, for example 50 percent of the tokens, will be Common Crawl, and then you add WebText2, books, Wikipedia, and whatever else you decide.

Since neither original dataset exists publicly, he names the modern stand ins:

And then the one he picks. FineWeb is an attempt to collect really high quality Common Crawl data and filter it, in this case down to 15 trillion tokens. More recently Hugging Face released the FineWeb-Edu subset, 1.3 trillion tokens of educational content and 5.4 trillion of high educational content, filtering Common Crawl to very high quality educational subsets. He recommends the FineWeb write up as "really fascinating reading" if you care about data mixtures and how data gets processed at these scales.

He uses the sample-10BT subsample, 10 billion tokens, and gives the reason: in his previous experiments that is enough to get really close to GPT-2 performance, and it is simple enough to work with.

One detail about the filtering that is worth knowing, and he flags it as pretty cool: the FineWeb-Edu filters were applied automatically using Llama 3 70B. An LLM judges which content is educational, and that judgement is what makes it through the filter. He browses the dataset viewer to check: nuclear energy in France, Mexican America, some Mac PJs. "Actually it seems like their filters are working pretty well."

Pre-tokenizing into shards

A separate fineweb.py downloads the dataset, pre-processes and pre-tokenizes everything, and writes shards to local disk. He does not walk the whole script because it is "not as interesting and not as LLM centric", but the choices he does narrate are the ones you would get wrong:

And a bug, left in on camera. The script fails, and the cause is using float division in Python where it must be integer division, so a count is not an int. "Apologies for that."

With the shards on disk, the data loader grows accordingly: load the uint16 numpy file, convert to a torch.long tensor because that is what the layers up top expect, enumerate all the shards, take a split argument so it can serve train or val, and track a current shard as well as a current position. Run out of tokens in a shard, advance the shard, loop if needed, get the tokens, readjust.

The run that will actually be the run

The numbers for the real run:

If this works then this is basically a serious pre-training run. We're not logging, we're not evaluating the validation split, we're not running any evaluations yet, so we haven't crossed our t's and dotted our i's. But if we let this run for a while we're going to actually get a pretty good model, and the model that might even be on par with or better than GPT-2 124M. Andrej Karpathy, 3:21:29

So he stops, and goes and crosses the t's.

ConstantValueWhere it comes from
n_layer12GPT-2 124M. The paper's own parameter count table is wrong; the repo says the addition was in error
n_head12GPT-2 124M. GPT-2 XL uses 25, which he calls a really ugly number that caused real kernel headaches
n_embd768GPT-2 124M. 12 heads of 64 each
block_size1024GPT-2's maximum sequence length, so wpe is 1024 by 768. GPT-3 uses 2048
vocab_size50257, run as 5030450,000 BPE merges + 256 byte tokens + 1 end of text token. Padded up to the nearest multiple of 128 for speed
End of text token id50256The special GPT-2 token that, despite the name, begins every document in the shards
Parameters124 million, about 40 million of them shared768 times 50257 is tied between wte and lm_head, so roughly 30 percent of the model is one tensor used twice
Expected loss at init10.82, he measures 11-ln(1/50257). The check that says the distribution is diffuse before step one
Init std0.02, biases 0OpenAI's released model.py. Position embeddings are 0.01 there; he keeps 0.02. Zero biases is not the PyTorch default
Residual init scale(2 * n_layer) ** -0.5GPT-2 paper. The 2 is because attention and the MLP each add into the residual stream
Tiny Shakespeare1 MB, ~1M chars, 338,000 tokensAbout 40,000 lines and 200,000 words, at a roughly 3 to 1 character to token ratio
FineWeb-Edu sample10 billion tokens, 100 shards of 100 millionShard 000000 is validation, the rest train. Stored as np.uint16 because the max token id is well under 65,535
Micro batch, final runB=64, T=102464 times 1024 times 8 GPUs equals the total batch exactly, so no gradient accumulation at all
Hardware8x A100 SXM 80GBRented from Lambda Labs. 19.5 TFLOPS fp32, 156 TF32, 312 bf16, about 2 TB/s of memory bandwidth
Memory used35 GB of 80At B=16, T=1024. If you hit out of memory, halve the batch and keep it a nice number
Good utilization60 percentHis rule of thumb. Half the time in a well tuned run the tensor cores are idle waiting for data
Figure 6. Every constant in the finished script, with its provenance. The three worth memorizing are in the middle: the expected loss at initialization, because it is the cheapest bug check in deep learning; the tied 40 million parameters, which is the single biggest architectural fact about a 124M GPT-2; and the vocabulary padded to 50304, which is the only number here that was chosen for the hardware rather than for the model.

Validation, sampling and checkpointing

The validation split

The val loader is the same class with split='val', serving the one held out shard. He also adds a reset() method to the loader, called at init and again before each evaluation, which is what makes a repeatable validation pass possible.

Every 100 steps, including step zero, and later every 250: put the model in eval mode, reset the val loader, and under no_grad accumulate the loss over 20 steps and average it. Same logic as the training loop with the backward pass removed. It is only inference, measuring the loss.

He is clear eyed about what it buys. With roughly infinite data, train and val loss should be about the same, so it tells you only a little about overfitting. But it would matter a lot if you went to multiple epochs, where a big enough model might start memorizing, and the validation split is how you would catch that. And:

In any case you would always want to have a validation split in a training run like this, so that you can make sure that you are not overfitting. Andrej Karpathy, 3:25:11

The other reason he wants it is sharper: you can initialize from the released GPT-2 124M and measure its loss on this validation split, which gives you a reference line. He flags the caveat himself. It is not a super fair comparison, because GPT-2 was trained on a very different data distribution, but it is an interesting data point and a good cross check.

Sampling, moved up and given its own RNG

The orphaned sampling code from the first hour gets deleted from the bottom of the script and moved up into the loop, so that once in a while the script validates, once in a while it samples, and it trains on every step.

One real change, and it is the kind of thing that quietly ruins reproducibility if you skip it. He creates a separate torch.Generator for sampling and passes it into torch.multinomial, specifically so that drawing samples does not touch the RNG state of the global random number generator used for training. Sampling stays completely outside the training loop. He seeds it so every rank gets a different seed.

And a problem he cannot solve on camera. torch.compile breaks the sampling and the HellaSwag evaluation, with "a really scary error from PyTorch, and I have no idea how to resolve it right now". So he turns compile off to get samples, which is why the run gets slower at this point, and then turns compile back on and gives up the samples. He says plainly that he hopes it is fixed by the time you see the code, and that he will fix the bug later.

The samples at step 1000, with the model just past the peak of the warmup:

Hello, I'm a language model, and I'm not able to get more creative. The model at step 1000, 3:25:43

Hello, I'm a language model, and languages file you're learning about here is or is the beginning of a computer. The model at step 1000, 3:25:43

His read: "this is still a garble, but we're only at iteration 1000 and we've only just barely reached maximum learning rate, so this is still learning". And then the line that is the best description of a half trained model anyone has written:

The model is still a young baby. Andrej Karpathy, 3:26:14

Logging and checkpoints

A log directory with a log.txt that records the train loss, the validation loss and the HellaSwag accuracies, opened for writing so it starts empty and then appended to. A simple text file, parsed later by a matplotlib cell in the notebook.

Checkpointing, added at the same place as the validation logging: every 5,000 steps, if you are the master process, save the model's state_dict. And then the warning about what a model checkpoint is not:

The reason to save at all, beyond resuming: you may want to evaluate the model much more carefully than he is doing here, where he is "only kind of winging the HellaSwag eval", using proper infrastructure like the EleutherAI evaluation harness and comparing against the OpenAI GPT-2 on many other tasks involving math, code or different languages.

And an aside about scope, worth stating because people do get confused about it:

Everything we've built here, this is only the pre-training step. The GPT here is, it dreams documents, it just predicts the next token. You can't talk to it like you can talk to ChatGPT. Andrej Karpathy, 3:55:17

To talk to it you fine tune into the chat format, and he says that is "not actually that complicated". Supervised fine tuning really means swapping in a dataset that is a lot more conversational with a user and assistant structure, filling in the user tokens and sampling the assistant tokens. "It's not a lot more deeper than that. Basically we swap out the dataset and continue training." But this video stops at pre-training.

HellaSwag: the evaluation, and why this one

The validation loss needs a companion that is held out, comparable and somewhat standard, and for that he uses HellaSwag, from a 2019 paper by Rowan Zellers and co authors.

What it is

A sentence completion dataset, multiple choice, four candidate endings sharing a context. The example he reads out verbatim:

A woman is outside with a bucket and a dog. The dog is running around trying to avoid a bath. She:

The options are constructed so that one is a natural continuation and the others are not, and some of them do not make sense at all. "Uses the hose to keep it from getting soapy, that makes no sense." Models that are not trained very well cannot tell these apart; models with a lot of world knowledge can.

The sentences come from ActivityNet and WikiHow, and the paper has a chart of the WikiHow domains, computers and electronics, home and garden and so on, giving it broad coverage of the kinds of things you need to know about the world to find the most likely completion.

The construction detail that makes it good: the incorrect options are deliberately adversarially sourced. They are not random sentences, they are generated by language models, and generated such that language models find them difficult and humans find them easy. The paper reports 95 percent human accuracy against 48 percent for the state of the art at the time.

And why it is now too easy

He does not oversell it. Five years later HellaSwag "has been totally just solved", and language models sit at 96 percent, so the last 4 percent is probably errors in the dataset or genuinely very hard questions. The dataset is, in his word, "crushed".

But it is still useful here, for a specific reason, and this is the part that generalizes to choosing any eval for a small model:

HellaSwag is a smooth eval, and it is an eval that offers quote unquote early signal. So early signal means that even small language models are going to start at the random chance of 25 percent, but they're going to slowly improve, and you're going to see 25, 26, 27 etc. And you can see slow improvement even when the models are very small and it's very early. Andrej Karpathy, 3:31:59

Smooth, early signal, and around long enough that everybody uses it. It is not used in the GPT-2 paper, but it is in the GPT-3 paper, which means published GPT-3 accuracies exist for every model size and he has a reference point.

How he actually runs it

Small models cannot do multiple choice. They do not understand the concept of associating a label with one of the options; "they don't understand that". So you have to give the task to them in a native form, which is token completion.

The construction, per example:

  1. Build a batch of 4 rows by T tokens. The shared context tokens are repeated across all four rows, then each row continues with one of the four options.
  2. The options differ in length, so T is the longest one, and the shorter rows get padded.
  3. You need three things out of this: the tokens, the correct label, and a mask marking which tokens are active option tokens, with zeros over the padding.
  4. Evaluate the cross entropy loss of predicting the next token across the option tokens of each row, average it per row, and pick the row with the lowest average loss, which is equivalently the highest average probability. That is the model's answer.

He believes this is also how GPT-3 did it, and then flags the fork in the road honestly. Other harnesses may run HellaSwag in a true multiple choice format, giving the context once followed by all four completions so the model can see the other options before it picks. That is an easier task, and models at this size cannot do it.

Our models are actually slightly handicapped in this way, that they are not going to see the other options, they're only going to see one option at a time and they just have to assign probabilities, and the correct option has to win out in this metric. Andrej Karpathy, 3:35:05

The implementation is a hellaswag.py that downloads the data, renders all 10,000 examples into the format above, and provides an evaluate function that can load a GPT-2 from Hugging Face and run the eval. He calls the code "kind of a little bit tedious honestly" and does not walk it line by line.

The reference numbers it produces, which are the numbers to beat:

Then it goes into the training script, periodic like everything else, so he can track HellaSwag over time and see when and if the run crosses 29.55. Under DDP, each process takes only the examples where the index modulo the world size equals its rank, then the counts are packaged into tensors, all reduced with a sum, unwrapped back to integers, and the master process prints and logs the accuracy.

SECTION 4: results in the morning

He goes to bed. The next cell in the notebook parses the log file and plots it, "a lot of this is just like boring matplotlib code", and the first run is done.

The one epoch run: 10 billion tokens, about two hours

Two panels. On the left, the loss: training loss in blue, validation loss in orange, and a horizontal red line for the OpenAI GPT-2 124M checkpoint evaluated on the FineWeb-Edu validation split. On the right, HellaSwag, with the OpenAI GPT-2 124M in red and the GPT-3 124M in green.

The orange is below the red. The from scratch model surpasses the released GPT-2 124M on this validation set, with the caveat he repeats: the data distribution is very different from what GPT-2 trained on, so this is not an exactly fair comparison, but it is a good cross check.

And on HellaSwag, the one that is held out and comparable:

You see that we basically surpassed the GPT-2 124M model right here, which is really nice. Now interestingly we were able to do so with only training on 10 billion tokens, while GPT-2 was trained on 100 billion tokens. Andrej Karpathy, 3:45:00

So a 10 times learning efficiency gap, in 2024, against a model that was a serious research artifact in 2019. He does not leave that unexamined, and offers three possible explanations rather than claiming a win:

  1. GPT-2 was trained on a much wider data distribution. FineWeb-Edu is all English, not multilingual, and does not have much math or code. Math, code and multilingual capability "could have been stealing capacity from the original GPT-2 model".
  2. HellaSwag is five years old and might have leaked. It is possible that aspects of HellaSwag, in some way or even identically, made it into FineWeb's training set. "If that was the case then we are basically looking at the training curve instead of the validation curve." He then gives the mitigating fact: Hugging Face used HellaSwag as an eval when they created FineWeb-Edu, so he would hope they deduplicated against it. "But we can't be sure."
  3. The data is probably just better per token. The original GPT-2 dataset was WebText, and "it's possible that not a lot of care and attention went into the dataset, this was very early in LLMs, whereas now there's a lot more scrutiny on good practices around deduplication, filtering, quality filtering and so on".

That is three caveats on his own headline result, unprompted, which is most of why this video is trusted.

The loss curve that is wrong, and he says so

There is a visible problem in the plot, and he does not paper over it:

The other thing I wanted to address briefly is, look at this loss curve. This looks really wrong here. I don't actually know 100 percent what this is, and I suspect it's because the 10 billion sample of FineWeb-Edu was not properly shuffled, and there's some issue here with the data that I don't fully understand yet, and there's some weird periodicity to it. Andrej Karpathy, 3:46:33

His diagnosis: his own loader is "in a very lazy way sort of serializing all the tokens and just iterating all of them from scratch without doing any permutation or any random sampling ourselves", so it is inheriting whatever ordering the dataset has. He expects it will be fixed in the repo by the time you read it.

The overnight run: 40 billion tokens, four epochs, about eight hours

Having seen the one epoch result, he wanted to know how far it would push, so he made exactly one change, multiplied the token budget by four, and went to sleep for eight hours. Four epochs, roughly 40 billion tokens.

The result, narrated honestly in both directions:

And a second efficiency claim, with the same framing as the first: the run is almost matching GPT-3 accuracy with 40 billion tokens where GPT-3 trained on 300 billion. "Again we're seeing about a 10 times improvement here with respect to learning efficiency." And again he says he does not know exactly what to attribute it to beyond the reasons already listed.

Two things he wants fixed for anyone doing multi epoch runs:

Two parting hyperparameter notes

The learning rate is probably too low. He has seen people play with this in a related repository, and it turns out you can go about three times higher on the max learning rate:

For some reason the GPT-3 hyperparameters that we are inheriting are actually extremely conservative, and you can actually get away with a higher learning rate and it would train faster. So a lot of these hyperparameters are quite tunable, and feel free to play with them. They're probably not set precisely correctly. Andrej Karpathy, 3:51:14

And the exact change for GPT-3 fidelity, if you want it. GPT-3's sequence length is double GPT-2's, 2048 instead of 1024. So set T to 2048, and then to keep the same half million tokens per step, drop the micro batch to 32, "so they still multiply to half a mil". With that, as far as he is aware, the models would be roughly identical, because GPT-2 and GPT-3 are very very similar models.

The overnight samples

The same prompt, a model that has seen four times as much data:

Hello, I'm a language model, and I try to be as accurate as possible. The overnight model, 3:53:17

Hello, I'm a language model, not a programming language. I know how to communicate. I use Python. The overnight model, 3:53:17

Hello, I'm a language model, and I'm going to be speaking English and German. The overnight model, 3:42:59, from the halfway samples

His read on the progression: the predictions are "getting less and less random", the model "is a little bit more self-aware and using language that is a bit more specific to it being a language model", and the overnight samples are "a lot more coherent" than the 10 billion token ones if you pause and compare them side by side.

HellaSwag accuracy, percent. his own script, completion format, not multiple choice 0 20 40 60 80 100 random chance 25.00 GPT-2 124M (OpenAI) 100B tokens, 2019 29.55 ← the number to beat this run, 10B tokens 1 epoch, ~1.7 hours passes 29.55 mid run. he never states the final figure this run, 40B tokens 4 epochs, overnight 33.24 GPT-3 124M, 300B tokens, sits just above. Not reached GPT-2 XL, 1558M ~49 humans 95 state of the art, 2024 ~96, so the benchmark is crushed Amber is trained from scratch in this video. Blue is a published reference he quotes. 29.55 and 33.24 are his own script, in the completion format. The Eleuther harness reports slightly different values, possibly because it uses multiple choice.
Figure 5. Every HellaSwag figure stated in the video, on one axis. The result is the two amber bars against the blue one at 29.55: a from scratch 124M model passes the 2019 release on a tenth of the token budget, and four epochs almost but not quite reach the GPT-3 124M line that took 300 billion tokens. The top two bars are the context he supplies himself. A benchmark where humans sit at 95 and production models sit at 96 is a solved benchmark, and he uses it anyway, because what he needs from it is a smooth signal at the bottom of the range, not a hard ceiling at the top.

The shoutout to llm.c

Everything built in this video was building towards nanoGPT, the earlier repository. But there is a second nanoGPT implementation hiding in a more recent project: llm.c, a pure CUDA implementation of GPT-2 and GPT-3 training that uses CUDA directly and is written as CUDA.

The relationship between the two is the interesting part. The nanoGPT style train_gpt2.py inside llm.c acts as the PyTorch reference code for the C implementation, so the two are exactly matched, and the hope is that the C and CUDA version is faster. Scroll through that Python file and "you'll find a lot of things that very much look like things that we've built up in this lecture". Then train_gpt2.cu is the C and CUDA implementation, full of MPI, NCCL, GPU, CUDA and C and C++, and you have to be familiar with that.

Then he runs them side by side, one GPU each, llm.c on GPU 1 and PyTorch grabbing GPU 0 by default, and the race has a comic first act:

Then the steady state numbers, with the honest asterisk:

And the verification that makes the comparison worth anything: line up the steps and the losses and the gradient norms printed by the two implementations are identical. Same computation, one runs faster.

His framing of the comparison is careful. This is a very specific implementation for GPT-2 and GPT-3, and PyTorch is a very general neural network framework, so they are not exactly comparable. But if you are only interested in training GPT-2 and GPT-3, llm.c is very fast, takes less space, is faster to start and faster per step.

Wrapping up, and what is left open

I think it's getting way longer than I anticipated, but we did cover a lot of ground, and we built everything from scratch. Andrej Karpathy, 3:59:24

His own summary: they looked at the GPT-2 and GPT-3 papers, looked at how you set up these training runs and all the considerations involved, wrote everything from scratch, and then over a two hour run or an overnight run matched the 124 million parameter checkpoints of GPT-2 and GPT-3 "to a very large extent". And in principle the code would train bigger models too, if you have the patience or the computing resources, so you could think about the bigger checkpoints as well.

Then, instead of ending on the win, he lists the open bugs:

  1. The loss periodicity, which he suspects is the FineWeb-Edu data sampling.
  2. Why torch.compile cannot be turned on, because it currently breaks generation and HellaSwag. "What's up with that."
  3. The data loader should permute the data when it reaches boundaries.

He expects to document those over time in build-nanogpt, and he makes a point about how that repository was built that is worth more than it sounds:

I will be releasing all this code, and actually I've been very careful about making git commits every time we add something. And so I'm going to release the entire repo that starts completely from scratch all the way to now, and so everything should be exactly documented in the git commit history. Andrej Karpathy, 3:27:45

Which is why the repo is a usable companion rather than a finished artifact: there is one commit per step of the video, so you can check out the state of the code at any point in these four hours. Questions go to the repository's discussions tab, issues or pull requests, or the Zero to Hero Discord.

Key takeaways

Where this sits in the LLM Learning track

This is the second half of the build portion of the track, immediately after the tokenizer video, and it is where everything the earlier videos describe abstractly turns into code you can run. The attention block, the parameter counts, the scaling relationships: they all appear here with exact shapes and exact values, in a file you can execute.

It is also the video that makes the efficiency arguments from the rest of the track concrete. Every optimization in the middle two hours is an arithmetic intensity argument in disguise, and the reason the chapter titles carry millisecond counts is that none of them is taken on faith.

If you watch one video in this track with a keyboard in front of you rather than a notebook, make it this one, and keep build-nanogpt open beside it. The repository has one commit per step of these four hours, so you can check out the code at the exact moment of any chapter below.

Chapters

All 31 entries are Karpathy's own chapter markers, reproduced exactly as written, typo in the section 3 heading and all. Note that seven of them carry a millisecond count in the title, which is the whole method of the video compressed into a table of contents.

Notable quotes

When we talk about reproducing GPT-2 we have to be careful, because in particular in this video we're going to be reproducing the 124 million parameter model. Andrej Karpathy, the first caveat, 0:00

The reason my numbers the way I say them disagree with this table is that this table is wrong. Andrej Karpathy, on the parameter counts in the GPT-2 paper, 1:00

Today you can reproduce this model in roughly an hour or probably less even, and it will cost you about 10 bucks if you want to do this on the cloud. Andrej Karpathy, the economics, 2:35

You can tell, for example, that because they're a bit more jagged and they're kind of noisy, you can tell that this model was not fully trained. Andrej Karpathy, reading the released GPT-2 position embeddings as a diagnostic, 9:14

You actually prefer to have a single clean residual stream all the way from supervision all the way down to the inputs, the tokens. Andrej Karpathy, on why GPT-2 moved the layer norms, 18:26

So the attention is the reduce and the MLP is the map, and what you end up with is that the Transformer just ends up being a repeated application of map reduce. Andrej Karpathy, 19:58

Today there's no real good reason to use the approximate version. You'd prefer to just use the exact version, because my expectation is that there's no big difference anymore and this is kind of like a historical quirk. But we are trying to reproduce GPT-2 exactly, and GPT-2 used the tanh approximate version, so we prefer to stick with that. Andrej Karpathy, on the GELU approximation, 22:33

We don't have to basically use this file from Hugging Face, which is fairly long, this is 2,000 lines of code. Instead we just have a less than 100 lines of code, and this is the complete GPT-2 implementation. Andrej Karpathy, 27:44

You have to be careful, because you can't just do buff.to(device). It's not stateful, it doesn't convert it to be a device, it instead returns a pointer to a new memory which is on the device. Andrej Karpathy, on the device bug he leaves in, 1:00:21

Not only are these two separate tensors that happen to have the same shape and elements, they're actually pointing to the identical tensor. Andrej Karpathy, discovering GPT-2's weight tying by comparing data pointers, 1:07:33

Unfortunately the GPT-2 paper and the GPT-3 paper are not very explicit about initialization, so we kind of have to read between the lines. Andrej Karpathy, 1:13:40

You always want to start with: what hardware do you have, what does it offer, and are you fully utilizing it? Andrej Karpathy, opening section 2, 1:22:26

It turns out empirically that for deep learning as a computational workload this is way too much. Andrej Karpathy, on float32 being the PyTorch default, 1:24:02

Many of the deep learning workloads for training are memory bound, and what that means is actually that the tensor cores that do all these extremely fast multiplications, most of the time they're waiting around, they're idle, because we can't feed them with data fast enough. Andrej Karpathy, the thesis of the whole optimization section, 1:27:06

Typical utilizations of your hardware, if you're getting 60 percent utilization you're actually doing extremely well. So half of the time, in a well tuned application, your tensor cores are not doing multiplies because the data is not available. Andrej Karpathy, 1:27:37

The reason I like TF32 is because if you can tolerate a little bit of a precision fudge then this is free. Like none of your code sees this, it's fully internal to the operation, and the operation to you just goes 8x faster. Andrej Karpathy, 1:32:17

Even though the TF32 offers in principle a lot faster throughput, all of these numbers everywhere are still float32s, and it's float32 numbers that are being shipped all over the place through the memory system, and it's just costing us way too much time to shuttle around all this data. Andrej Karpathy, on why the 8x became 3x, 1:38:29

torch.compile is really quite incredible infrastructure from the PyTorch team, and it's basically a compiler for neural networks. Like it's almost like GCC for C and C++ code. This is just the GCC of neural nets. Andrej Karpathy, 1:48:10

There's no real good reason for you to not use torch.compile in your PyTorch. I kind of feel like you should be using it almost by default, unless you're debugging. Andrej Karpathy, 1:49:15

Flash attention actually, if you just count the number of FLOPs, flash attention does more FLOPs than this attention here. But flash attention is actually significantly faster. Andrej Karpathy, 2:01:37

Great example, I think, of being aware of memory hierarchy, the fact that FLOPs don't matter, the entire memory access pattern matters, and that torch.compile is amazing but there are many optimizations that are still available to us that potentially torch.compile cannot find. Andrej Karpathy, 2:04:40

We are now getting to one of my favorite optimizations, and it is simultaneously the dumbest and the most brilliant optimization. Andrej Karpathy, introducing the vocabulary padding, 2:06:48

Basically, scan your code and look for ugly numbers is roughly the heuristic. Andrej Karpathy, 2:07:50

So functionally nothing breaks. We're using a bit more extra memory, but otherwise this is a harmless operation as far as I can tell. And we're adding calculation, but it's running faster. Andrej Karpathy, on padding the vocabulary to 50304, 2:12:55

GPT-2, we have the weights but no details. GPT-3, we have lots of details but no weights. Andrej Karpathy, 2:15:59

Sometimes you could get a spike in the norm, and that means there's some kind of an issue or an instability. Andrej Karpathy, on why you should log the gradient norm, 2:19:03

I don't love to use abstractions where they're kind of inscrutable and then I don't know what they're doing. So, personal style. Andrej Karpathy, on writing his own learning rate scheduler, 2:23:07

Why are you doing batch sizes of like millions when, if you do a batch size of 32k, you're basically getting the exact same gradient early on in the training? Andrej Karpathy, on skipping GPT-3's batch size ramp, 2:27:43

The problem is I can't come in here and set this to 488, because my GPU would explode. Andrej Karpathy, on GPT-3's half million token batch, 2:35:56

There's like a subtle and deep issue here, and this is actually incorrect. So I invite you to think about why this is not yet sufficient. Andrej Karpathy, before the gradient accumulation normalization fix, 2:39:29

Now is the time to bring out the heavy weapons. You've noticed that so far we've only been using a single GPU for training, but actually I am paying for eight GPUs here. Andrej Karpathy, 2:46:38

As you read the code now you have to imagine there's eight Python interpreters running down these lines of code, and the only difference between them is that they have a different DDP rank. Andrej Karpathy, 2:51:41

This is a naughty thing to do, because they could probably change the DDP and this variable will go away. Andrej Karpathy, on toggling require_backward_grad_sync by hand instead of using no_sync, 3:04:25

It's not a very high quality dataset all by itself, because it is extremely noisy. This is a completely random subset of the internet and it's much worse than you think. Andrej Karpathy, on Common Crawl, 3:11:09

The filters here, by the way, were applied automatically using Llama 3 70B, I believe. And so basically LLMs are judging which content is educational, and that ends up making it through the filter. Andrej Karpathy, on FineWeb-Edu, 3:14:48

If this works then this is basically a serious pre-training run. Andrej Karpathy, launching the real run, 3:21:29

The model is still a young baby. Andrej Karpathy, on the samples at step 1000, 3:26:14

I will be releasing all this code, and actually I've been very careful about making git commits every time we add something. Andrej Karpathy, on why build-nanogpt has one commit per step, 3:27:45

HellaSwag is a smooth eval, and it is an eval that offers quote unquote early signal. Andrej Karpathy, on why this benchmark and not another, 3:31:59

Our models are actually slightly handicapped in this way, that they are not going to see the other options. Andrej Karpathy, on running HellaSwag as token completion rather than multiple choice, 3:35:05

Interestingly, we were able to do so with only training on 10 billion tokens, while GPT-2 was trained on 100 billion tokens. Andrej Karpathy, on surpassing GPT-2 124M, 3:45:00

The other thing I wanted to address briefly is, look at this loss curve. This looks really wrong here. I don't actually know 100 percent what this is. Andrej Karpathy, on the periodicity in his own results, 3:46:33

It's too bad that I didn't sleep slightly longer. I think if this was a five epoch run we may have gotten here. Andrej Karpathy, on almost reaching GPT-3 124M, 3:49:41

For some reason the GPT-3 hyperparameters that we are inheriting are actually extremely conservative, and you can actually get away with a higher learning rate and it would train faster. Andrej Karpathy, 3:51:14

Everything we've built here, this is only the pre-training step. The GPT here is, it dreams documents, it just predicts the next token. You can't talk to it like you can talk to ChatGPT. Andrej Karpathy, 3:55:17

I don't have full confidence that I exactly squeezed out all the juice from the PyTorch implementation. Andrej Karpathy, on llm.c being faster than his own PyTorch code, 3:58:54

I think it's getting way longer than I anticipated, but we did cover a lot of ground, and we built everything from scratch. Andrej Karpathy, 3:59:24

A note on names, from the captions

The caption track mangles technical names relentlessly, so a few corrections are worth stating once rather than silently fixing. Each of these is the correct spelling of what he actually says:

Two numbers in the captions are also wrong by a dropped digit, and the page uses the correct ones. The one epoch run is 19,073 steps, not "1973", which you can check against 10 billion divided by 2 to the 19 and against the "19073 times 0.33" he reads out moments later. And the overnight run's final step is around 76,290, consistent with four times 19,073.

Resources mentioned

The repositories

The papers

The models and datasets

The tools

The rest of the series

An honest footnote

Four things are worth saying once, at the end, for anyone planning to run this rather than watch it.

The benchmark comparison is weaker than the headline, and he says so three times. The validation loss comparison against GPT-2 124M is on FineWeb-Edu's validation split, which is not the distribution GPT-2 was trained on, and he calls it "not an exactly fair comparison" himself. The HellaSwag comparison is the better one because it is held out and standard, and even there he raises the possibility that HellaSwag content leaked into FineWeb, in which case "we are basically looking at the training curve instead of the validation curve". The honest reading of the 10 times token efficiency result is that it is real but multiply caused: better filtered data, a narrower distribution with no multilingual or code or math to pay for, and an unknown amount of benchmark contamination.

Two of the numbers in the video will not reproduce today, in opposite directions. The vocabulary padding gives him 4 percent on PyTorch nightly and he says explicitly that on 2.3.1 and earlier it was about 30 percent, so the win you measure depends entirely on your PyTorch version. And torch.compile breaking generation and HellaSwag was a live bug he could not fix on camera, which he expected to be resolved by the time people read the repo. If you are following along on a current PyTorch, expect the step times in the chapter titles to be a rough guide rather than targets.

The learning rate he ships is knowingly too low. He says at the end that you can go roughly three times higher on the max learning rate and the GPT-3 hyperparameters he inherited are "extremely conservative". That is in the video, after the runs, so the recipe in the table above is the faithful GPT-3 recipe rather than his recommended one. If you want faster convergence, that is the first dial to turn, and he says so.

And the data loader has a known defect that shows up in his own plot. It serializes every token and iterates them in a fixed order with no permutation, which is why the loss curve has a periodicity he could not fully explain in four hours, and why the periodicity becomes obviously per epoch in the overnight run. The fix he names, and did not implement on camera, is to permute the documents within every shard on every new epoch and possibly permute the shards too. If you are planning multi epoch runs, do that before you start, not after you see the curve.

Full transcript
[00:00:00] hi everyone so today we are going to be continuing our Zero to Hero series and in particular today we are going to reproduce the gpt2 model the 124 million version of it so when openi released gpt2 this was 2019 and they released it with this blog post on top of that they released this paper and on top of that they released this code on GitHub so open a/ gpt2 now when we talk about reproducing gpt2 we have to be careful because in particular in this video we're going to [00:00:30] be reproducing the 124 million parameter model so the thing to realize is that there's always a miniseries when these are releases are made so there are the gpt2 miniseries made up of models at different sizes and usually the biggest model is called the gpt2 but basically the reason we do that is because you can put the model sizes on the x-axis of plots like this and on the Y AIS you put a lot of uh Downstream metrics that you're interested in like translation summarization question answering and so on and you can chart [00:01:00] out these scaling laws so basically as the model size increases you're getting better and better at Downstream metrics and so in particular for gpt2 if we scroll down in paper there are four models in the gpt2 miniseries starting at 124 million all the way up to 1558 million now the reason my numbers the way I say them disagree with this table is that this table is wrong if you actually go to the uh gpt2 uh GitHub repo they sort of say that um [00:01:32] there was an error in how they added up the parameters but basically this is the 124 million parameter model Etc so the 124 million parameter had 12 layers in the Transformer and it had 768 channels in the Transformer 768 dimensions and I'm going to be assuming some familiarity with what these terms mean because I covered all of this in my previous video let's build gpt2 uh let's build GPT from scratch so I covered that in the previous video in this playlist now if we do everything correctly and everything works out well by the end of [00:02:03] this video we're going to see something like this where we're looking at the validation loss which basically um measures how good we are at predicting the next token in a sequence on some validation data that the model has not seen during training and we see that we go from doing that task not very well because we're initializing from scratch all the way to doing that task quite well um by the end of the training and hopefully we're going to beat the gpt2 uh 124 M model now previously when they were working on this this is already 5 years ago so this [00:02:35] was probably a fairly complicated optimization at the time and the gpus and the compute was a lot smaller today you can reproduce this model in roughly an hour or probably less even and it will cost you about 10 bucks if you want to do this on the cloud uh Cloud Compu a sort of computer that you can all rent and if you pay $10 for that computer you wait about an hour or less you can actually achieve a model that is as good as this model that open ey released and uh one more thing to mention is unlike many other models open ey did release [00:03:06] the weights for gpt2 so those weights are all available in this repository but the gpt2 paper is not always as good with all of the details of training so in addition to the gpt2 paper we're going to be referencing the gpt3 paper which is a lot more Concrete in a lot of the hyp parameters and optimization settings and so on um and it's not a huge departure in the architecture from the GPT 2 uh version of the model so we're going to be referencing both gpt2 and gpt3 as we try to reproduce gpt2 124 [00:03:36] M uh so let's go so the first thing I would like to do is actually start at the end or at the Target so in other words let's load the GPT to 124 M model as it was released by openi and maybe take it for a spin let's sample some tokens from it now the issue with that is when you go into the code base of gpt2 and you go into the source and you click in on the model. pi you'll realize that actually this is using tensorflow so the original gpt2 code here was written in tensor flow which is [00:04:06] um you know not let's just say not used as much anymore um so we'd like to use pytorch uh because it's a lot friendlier easier and I just personally like a lot more the problem with that is the initial code is intenser flow we'd like to use pytorch so instead uh to get the target we're going to use the hugging face Transformers um code which I like a lot more so when you go into the Transformers source Transformers models gpt2 modeling gpt2 Pi you will see that they have the gpt2 implementation of that Transformer here in this [00:04:37] file um and it's like medium readable but not fully readable um but what it does is it did all the work of converting all those weights uh from tensor flow to pytorch Friendly and so it's much easier to load and work with so in particular we can look at the gpt2 um model here and we can load it using hugging face Transformers so swinging over this is what that looks like from Transformers import the DP GT2 [00:05:07] LM head model and then from pre-train gpt2 uh now one awkward thing about this is that when you do gpt2 as the model that we're loading this actually is the 124 million parameter model if you want the actual the gpt2 the 1.5 billion then you actually want to do- XL so this is the 12 4 M our Target now what we're doing is when we actually get this we're initializing the uh pytorch NN module as [00:05:37] defined here in this class from it I want to get just the state dict which is just a raw tensors so we just have um the tensors of that file and by the way here this is a jupyter notebook uh but this is jupyter notebook running inside vs code uh so I like to work with it all in a single sort of interface so I like to use vs code so this is the jupyter notebook extension inside the es code so when we get the state dick this is just a dict so we can print the key [00:06:09] and the value which is the tensor and let's just look at the shapes so these are sort of the uh different parameters inside the gbt2 model and their shape so the W weight for token embedding is of size 50257 by 768 where this is coming from is that we have 50257 tokens in the gpt2 vocabulary um and the tokens by the way these are [00:06:39] exactly the tokens that we spoken about in the previous video on my tokenization Series so the previous videos just before this I go into a ton of detail on tokenization gpt2 tokenizer happens to have this many tokens for each token we have a 768 dimensional embedding that is the distributed representation that stands in for that token so each token is a little string piece and then the 768 numbers are the vector that represents that token and so this is just our lookup [00:07:10] table for tokens and then here we have the lookup table for the positions so because gbt2 has a maximum sequence length of 1024 we have up to 1,24 positions that each token can be attending to in the past and every one of those positions in gpd2 has a fixed Vector of 768 that is learned by optimization um and so this is the position embedding and the token embedding um and then everything here is [00:07:41] just the other weights and biases and everything else of this Transformer so when you just take for example the positional embeddings and flatten it out and take just the 20 elements you can see that these are just the parameters these are weights floats just we can take and we can plot them so these are the position embeddings and we get something like this and you can see that this has structure and it has structure because what we what we have here really is every Row in this visualization is a different position a [00:08:12] fixed absolute position in um the range from 0 to 1024 and each row here is the representation of that position and so it has structure because these positional embeddings end up learning these sinusoids and cosiness um that sort of like represent each of these positions and uh each row here stands in for that position and is processed by the Transformer to recover all the relative positions and uh sort of realize which token is where and um [00:08:44] attend to them depending on their position not just their content so when we actually just look into an individual column inside these and I just grabbed three random columns you'll see that for example here we are focusing on every every single um Channel and we're looking at what that channel is doing as a function of uh position from one from Z to 1223 [00:09:14] really and we can see that some of these channels basically like respond more or less to different parts of the position Spectrum so this green channel uh really likes to fire for everything after 200 uh up to 800 but not less a lot less and has a sharp drop off here near zero so who knows what these embeddings are doing and why they are the way they are you can tell for example that because they're a bit more Jagged and they're kind of noisy you can tell that this model was not fully trained and the more trained this model was the more you [00:09:45] would expect to smooth this out and so this is telling you that this is a little bit of an undertrained model um but in principle actually these curves don't even have to be smooth this should just be totally random noise and in fact in the beginning of the optimization it is complete random noise because this position embedding table is initialized completely at random so in the beginning you have jaggedness and the fact that you end up with something smooth is already kind of impressive um that that just falls out of the optimization because in principle you shouldn't even be able to get any single graph out of [00:10:16] this that makes sense but we actually get something that looks a little bit noisy but for the most part looks sinusoidal like um in the original Transformer um in the original Transformer paper the attention is all you need paper the positional embeddings are actually initialized and fixed if I remember correctly to sinusoids and cosiness of uh different frequencies and that's the positional coding and it's fixed but in gpt2 these are just parameters and they're trained from scratch just like any other parameter uh and that seems to work about as well and [00:10:46] so what they do is they kind of like recover these sinusoidal like features during the optimization we can also look at any of the other matrices here so here I took the first layer of the Transformer and looking at like one of its weights and just the first block of 300 by 300 and you see some structure but like again like who knows what any of this is if you're into mechanistic interpretability you might get a real kick out of trying to figure out like [00:11:16] what is going on what is this structure and what does this all mean but we're not going to be doing that in this video but we definitely see that there's some interesting structure and that's kind of cool what we're mostly interested in is we've loaded the weights of this model that was released by open Ai and now using the hogging face Transformers we can not just get all the raw weights but we can also get the um what they call Pipeline and sample from it so this is the prefix hello I'm a language model comma and then we're sampling uh 30 [00:11:47] tokens and we getting five sequences and I ran this and this is what it produced um hell language model but what I'm really doing is making a human readable document there are other languages but those are dot dot dot so you can read through these if you like but basically these are five different completions of the same prefix from this uh gbt 2124m now uh if I go here I took this example from here and sadly even though we are fixing the seed we are getting [00:12:18] different Generations from the snippet than what they got so presumably the code changed um but what we see though at this stage that's important is that we are getting coherent text so we've loaded the model successfully we can look at all its parameters and the keys tell us where in the model these come from and we want to actually write our own gpt2 class so that we have full understanding of what's happening there we don't want to be working with something like uh the modeling gpt2 Pi [00:12:49] because it's just too complicated we want to write this from scratch ourselves so we're going to be implementing the GPT model here in parallel and as our first task let's load the gpt2 124 M into the class that we're going to develop here from scratch that's going to give us confidence that we can load the open ey model and therefore there's a setting of Weights that exactly is the 124 model but then of course what we're going to do is we're going to initialize the model from scratch instead and try try to train it ourselves um on a bunch of documents [00:13:20] that we're going to get and we're going to try to surpass that model so we're going to get different weights and everything's going to look different hopefully better even um but uh we're going to have a lot of confidence that because we can load the openi model we are in the same model family and model class and we just have to ReDiscover a good setting of the weights uh but from scratch so let's now write the gbt2 model and let's load the weights and make sure that we can also generate text that looks coherent okay so let's now swing over to the attention is all un need paper that started [00:13:51] everything and let's scroll over to the model architecture the original Transformer now remember that gpt2 is slightly modified from the or or Transformer in particular we do not have uh the encoder gpt2 is a decoder only Transformer as we call it so this entire encoder here is missing in addition to that this cross attention here that was using that encoder is also missing so we delete this entire part everything else stays almost the same but there are some differences that we're going to uh sort [00:14:21] of look at here so there are two main differences when we go to the gb2 page under 2.3 model we notice that first there's a reshuffling of the layer Norms so they change place and second an additional layer normalization was added here to the final self detention block so basically all the layer Norms here instead of being after the MLP or after the attention they SN before it and an additional layer Norm gets added here [00:14:52] right before the final classifier so now let's Implement some of the first sort of skeleton NN module modules here in our GPT NN module and in particular we're going to try to match up this schema here that is used by hugging face Transformers because that will make it much easier to load these weights from this state dict so we want something that reflects uh this schema here so here's what I came up with um basically we see that the main [00:15:22] container here that has all the modules is called Transformer so I'm reflecting that with an NN module dict and this is basically a module that allows you to index into the subm modules using keys just like a dictionary uh strings within it we have the weights of the token embeddings WT and that's an N embedding and the weights of the position embeddings which is also just an N embedding and if you remember n embedding is really just a fancy little wrapper module around just a single um [00:15:53] single array of numbers a single uh block of numbers just like this it's a single tensor and an embedding is a glorified um wrapper around a tensor that allows you to access its elements uh by indexing into the rows now in addition to that we see here that we have a h and then there's a this is index using numbers instead of indexed using strings so there's a h. 0 1 2 Etc all the way up till h. 11 and [00:16:23] that's because there are 12 layers here in this Transformer so to reflect that I'm creating also an H I think that probably stands for hidden and instead of a module dict this is a model list so we can index it using integers exactly as we see here 01 2 Etc and the modular list has a n layer blocks and the blocks are yet to be defined in a module in a bit in addition to that following the gpt2 paper we have we need an additional [00:16:53] final layer Norm that we're going to put in there and then we have the final classifier uh the language model head which um projects from 768 the number of embedding dimensions in this GPT all the way to the vocab size which is 50257 and gpt2 uses no bias for this final uh sort of projection so this is the skeleton and you can see that it reflects this so the wte is the token embeddings here it's called output [00:17:24] embedding but it's really the token embeddings the PE is the positional codings uh those two pieces of information as we saw previously are going to add and then go into the Transformer the H is the all the blocks in Gray and the LNF is this new layer that gets added here by the gpt2 model and LM head is this linear part here so that's the skeleton of the gpt2 we now have to implement the block okay so let's now recurse to the block itself so [00:17:55] we want to define the block um so I'll start putting them here so the block I like to write out like this uh these are some of the initializations and then this is the actual forward pass of what this block computes and notice here that there's a change from the Transformer again that is mentioned in the gpt2 paper so here the layer normalizations are after the application of attention or feed forward in addition to that note that the normalizations are inside the residual [00:18:26] stream you see how feed forward is applied and this arrow goes through and through the normalization so that means that your residual pathway has normalizations inside them and this is not very good or desirable uh you actually prefer to have a single uh clean residual stream all the way from supervision all the way down to the inputs the tokens and this is very desirable and nice because the gradients that flow from the top if you remember from your microad addition just [00:18:56] distributes gradients during the backwards state to both of its branches equally so addition is a branch in the gradients and so that means that the gradients from the top flows straight to the inputs the tokens through the residual Pathways unchanged but then in addition to that the gradient also flows through the blocks and the blocks you know contribute their own contribution over time and kick in and change the optimization over time but basically clean residual pathway is desirable from an optimization perspective and then the [00:19:28] this is the pre-normalization version where you see that RX first goes through the layer normalization and then the attention and then goes uh back out to go to the L ration number two and the multia perceptron sometimes also referred to as a feed forward Network or an FFN and then that goes into the residual stream again and the one more thing that is kind of interesting to note is that recall that attention is a communication operation it is where all the tokens and there's 1,24 tokens lined [00:19:58] up in a sequence and this is where the tokens communicate this is where they exchange information so attention is a um aggregation function it's a pooling function it's a weighted sum function it is a reduce operation whereas MLP this uh MLP here happens at every single token individually there's no information being collected or exchanged between the tokens so the attention is the reduce and the MLP is the map and what you end up with is that the [00:20:28] Transformer just ends up just being a repeated application of map produce if you want to think about it that way so um this is where they communicate and this is where they think individually about the information that they gathered and every one of these blocks uh iteratively refines the um representation is at the residual stream so this is our block um slightly modified from this picture Okay so let's now move on to the MLP so the MLP block uh I implemented as follows [00:20:59] it is relatively straightforward we basically have two linear projections here that are sandwiched in between the G nonlinearity so nn. G approximate is 10h now when we swing on uh swing over to the Pyro documentation this is n.g and it has this format and it has two versions the original version of G which we'll step into into in a bit and the approximate version of Galo which we can request using 10 so as you can see just as a preview here G is a basically like a reu except [00:21:32] there's no flat exactly Flat Tail here at exactly zero but otherwise it looks very much like a slightly smoother reu it comes from this paper here Gan error linear units and uh you can step through this paper and there's some mathematical calac reasoning that leads to an interpretation that leads to the specific formulation it has to do with stochastic radial risers and the expectation of a modification to Adaptive dropout so you can read through all of that if you'd like here and there's a little bit of history as to [00:22:03] why there is an an approximate version of G and that comes from this issue here as far as I can tell and in this issue Daniel Hendrix mentions that at the time when they developed this nonlinearity the Earth function which you need to evaluate the exact G was very slow in tensor flow so they ended up basically developing this approximation and this approximation that then ended up being picked up by Bert and by GP P2 Etc but today there's no real good reason to use the approximate version you'd prefer to [00:22:33] just use the exact version um because I my expectation is that there's no big difference anymore and this is kind of like a historical um kind of Quirk um but we are trying to reproduce gpt2 exactly and gpt2 used the 10h approximate version so we prefer to stick with that um now one other reason to actually just intuitively use G instead of veru is previously in the in videos in the past we've spoken about the dead reu neuron problem where in this tale of a [00:23:04] reu if it's exactly flat at zero any activations that fall there will get exactly zero gradient there's no change there's no adaptation there's no development of the network if any of these activations end in this flat region but the G always contributes a local gradient and so there's always going to be a change always going to be an adaptation and sort of smoothing it out ends up empirically working better in practice as demonstrated in this paper and also as demonstrated by it being picked up by the bird paper gbt2 paper and so on so for that reason we [00:23:35] adopt this nonlinearity uh here in the 10 in the gbt2 reproduction now in more modern networks also like llama 3 and so on this nonlinearity also further changes uh to swiglo and other variants like that uh but for gpt2 they Ed this approximate G okay and finally we have the attention operation so let me paste in my attention so I know this is a lot so I'm going to go through this a bit quickly a bit slowly but not too slowly because we [00:24:05] have covered this in the previous video and I would just point you there um so this is the attention operation now in the previous video you will remember this is not just attention this is um multi-headed attention right and so in the previous video we had this multi-headed attention module and this implementation made it obvious that these heads are not actually that complicated uh there's basically in parallel inside every attention block there's multiple heads and they're all functioning in parallel and uh their [00:24:36] outputs are just being concatenated and that becomes the output of the multi-headed attention so the heads are just kind of like parallel streams and their outputs get concatenated and so it was very simple and made the head be kind of like U fairly straightforward in terms of its implementation what happens here is that instead of having two separate modules and indeed many more modules that get concatenated all of that is just put into a single uh self attention uh module and instead I'm being very [00:25:07] careful and doing a bunch of transpose split um tensor gymnastics to make this very efficient in pych but fundamentally and algorithmically nothing is different from the implementation we saw before um in this uh give repository so to remind you very briefly and I don't want to go in this uh into this in too many in too much time but we have these tokens lined up in a sequence and there's 1,20 of them and then each token at this stage of the attention [00:25:37] emits three vectors the query key and the value and first what happens here um is that the queries and the keys have to multiply each other to get sort of the attention um amount like how interesting they find each other so they have to interact multiplicatively so what we're doing here is we're calculating the qkv we splitting it and then there's a bunch of gymnastics as I mentioned here and the way this works is that we're basically making the number of heads and H into a batch Dimension and so it's a [00:26:10] batch Dimension just like B so that in these operations that follow pytorch treats B and NH as batches and it applies all the operations on all of them in parallel in both the batch and the heads and the operations that get applied are number one the queries and the keys intera to give us her attention this is the autoaggressive mask that makes sure that the tokens only attend to tokens before them and never to tokens in the future the softmax here normalizes the [00:26:41] attention so it sums to one always and then recall from the previous video that doing the attention Matrix multiply with the values is basically a way to do a weighted sum of the values of the tokens that we found interesting at every single token and then the final transpose conf VI and view is just reassembling all of that again and this actually performs the concatenation operation so you can step through this uh slowly if you'd like um but it is equivalent mathematically to our previous implementation is just more [00:27:12] efficient in P torch so that's why I chose this implementation instead now in addition to that I'm being careful with how I name my variables so for example cattin is the same as seaten and so actually our keys should basically exactly follow the schema of the hugging face train Transformers code and that will make it very easy for us to now Port over all the weights from exactly this sort of naming conventions because all of our variables are named the same thing but um at this point we have finished the gpt2 implementation and what that allows [00:27:44] us to do is we don't have to basically use uh this file from hugging face which is fairly long um this is uh 2,000 lines of code um instead we just have a less than 100 lines of code and this is the complete uh gpd2 implementation so at this stage we should just be able to take over all the weights set them and then do generation so let's see what that looks like okay so here I've also changed the GPT config so that the numbers here the H parameters agree with the gpt2 124 M [00:28:15] model so the maximum sequence length which I call block size here is 124 the number of tokens is 50250 257 which if you watch my tokenizer video know that this is 50,000 m merges BP merges 256 bite tokens the leaves of the BP tree and one special end of text token that delimits different documents and can start generation as well and there are 12 layers there are 12 heads in the attention and the dimension of the [00:28:45] Transformers was 768 so here's how we can now load the parameters from hugging face to uh our code here and initialize the GPT class with those parameters so let me just copy paste a bunch of code here and I'm not going to go through this code too slow too quickly too slowly because um honestly it's not that interesting it's not that exciting we're just loading the weights so it's kind of dry but as I mentioned there are four models in this miniseries of gpt2 this [00:29:15] is some of the Jupiter code um code that we had here on the right I'm just pting it over these are the hyper parameters of the gpt2 models uh we're creating the config object and creating our own model and then what's Happening Here is we're creating the state dict both for our model and for the hugging face model um and then what we're doing here is we're going over the hugging face model keys and we're copying over those tensors and in the process we are kind [00:29:45] of ignoring a few of the buffers they're not parameters they're buffers so for example attention dobias uh that's just used for the autoaggressive mask and so we are ignoring some of those masks and uh that's it and then then one additional kind of annoyance is that this comes from the tensorflow repo and I'm not sure how this is a little bit annoying but some of the weights are transposed from what pytorch would want and so manually I hardcoded the weights that should be transposed and then we transpose them if that is so and then we [00:30:15] return this model so the from pre-trained is a Constructor or class method in Python that Returns the GPT object if we just give it the model type which in our case is gpt2 the smallest model that we're interested in so this is the code and this is how you would use it and um we can pop open the terminal here in vs code and we can python train gbt2 pi and fingers [00:30:46] crossed okay so we didn't crash and so we can load the weights and the biases and everything else into our Ann module but now let's also get additional confidence that this is working and let's try to actually generate from this model okay now before we can actually generate from this model we have to be able to forward it we didn't actually write that code yet so here's the forward function so the input to the forward is going to be our indices our tokens uh token indices and they are always of [00:31:16] shape B BYT and so we have batch dimension of B and then we have the time dimension of up to T and the T can't be more than the block size the block size is is the maximum sequence length so B BYT indices arranged is sort of like a two-dimensional layout and remember that basically every single row of this is of size up to uh block size and this is T tokens that are in a sequence and then we have B independent sequences stacked [00:31:46] up in a batch so that this is efficient now here we are forwarding the position embeddings and the token embeddings and this code should be very recognizable from the previous lecture so um we basically use uh a range which is kind of like a version of range but for pytorch uh and we're iterating from Z to T and creating this uh positions uh sort of uh indices um and then we are making sure that they're in the same device as idx because we're not going to be training on only CPU that's going to be too [00:32:16] inefficient we want to be training on GPU and that's going to come in in a bit uh then we have the position embeddings and the token embeddings and the addition operation of those two now notice that the position embed are going to be identical for every single row of uh of input and so there's broadcasting hidden inside this plus where we have to create an additional Dimension here and then these two add up because the same position embeddings apply at every single row of our example stacked up in a batch then we forward the Transformer [00:32:46] blocks and finally the last layer norm and the LM head so what comes out after forward is the logits and if the input was B BYT indices then at every single B by T we will calculate the uh logits for what token comes next in the sequence so what is the token B t+1 the one on the right of this token and B app size here is the number of possible tokens and so [00:33:16] therefore this is the tensor that we're going to obtain and these low jits are just a softmax away from becoming probabilities so this is the forward pass of the network and now we can get load and so we're going to be able to generate from the model imminently okay so now we're going to try to set up the identical thing on the left here that matches hug and face on the right so here we've sampled from the pipeline and we sampled five times up to 30 tokens with the prefix of hello I'm a language model and these are the [00:33:46] completions that we achieved so we're going to try to replicate that on the left here so number turn sequences is five max length is 30 so the first thing we do of course is we initialize our model then we put it into evaluation mode now this is a good practice to put the model into eval when you're not going to be training it you're just going to be using it and I don't actually know if this is doing anything right now for the following reason our model up above here contains no modules or layers that actually have a different uh Behavior at training or evaluation [00:34:16] time so for example Dropout batch norm and a bunch of other layers have this kind of behavior but all of these layers that we've used here should be identical in both training and evaluation time um so so potentially model that eval does nothing but then I'm not actually sure if this is the case and maybe pytorch internals uh do some clever things depending on the evaluation mode uh inside here the next thing we're doing here is we are moving the entire model to Cuda so we're moving this all of the tensors to GPU so I'm sshed here to a [00:34:47] cloud box and I have a bunch of gpus on this box and here I'm moving the entire model and all of its members and all of its tensors and everything like that everything gets shipped off to basically a whole separate computer that is sitting on the GPU and the GPU is connected to the uh CPU and they can communicate but it's basically a whole separate computer with its own computer architecture and it's really well catered to parallel processing tasks like those of running neural networks so I'm doing this so that the model lives on the GPU a whole separate computer and [00:35:19] it's just going to make our code a lot more efficient because all of this stuff runs a lot more efficiently on the gpus so that's the model itself now uh the next thing we want to do is we want to start with this as the prefix when we do the generation so let's actually create those prefix tokens so here's the code that I've written we're going to import the tich token library from open Ai and we're going to get the gpt2 encoding so that's the tokenizer for gpt2 and then we're [00:35:51] going to encode this string and get a list of integers which are the tokens uh now these integers here should actually be fairly straightforward because we can just copy paste this string and we can sort of inspect what it is in tick tokenizer so just pasting that in these are the tokens that are going to come out so this list of integers is what we expect tokens to become and as you recall if you saw my video of course all the tokens they're just little string chunks right so these are this is the [00:36:21] chunc of this string into gpt2 tokens so once we have those tokens it's a list of integers we can create a torch tensor out of it in this case it's eight tokens and then we're going to replicate these eight tokens for five times to get five rows of eight tokens and that is our initial um input X as I call it here and it lives on the GPU as well so X now is this idx that we can put into forward [00:36:52] to get our logits so that we know what comes as the sixth token uh sorry as the ninth token in every one of these five rows okay and we are now ready to generate so let me paste in one more code block here um so what's happening here in this code block is we have this x which is of size B BYT right so batch by time and we're going to be in every iteration of this loop we're going to be adding a column of new indices into each one of [00:37:22] these rows right and so these are the new indices and we're appending them to the the sequence as we're sampling so with each Loop iteration we get one more column into X and all of the operations happen in the context manager of torch. nograd this is just telling pytorch that we're not going to be calling that backward on any of this so it doesn't have to cach all the intermediate tensors it's not going to have to prepare in any way for a potential backward later and this saves a lot of space and also possibly uh some time so [00:37:52] we get our low jits we get the loow jits at only the last location we throw away all the other low jits uh we don't need them we only care about the last columns low jits so this is being wasteful uh but uh this is just kind of like an inefficient implementation of sampling um so it's correct but inefficient so we get the last column of loow jits pass it through soft Max to get our probabilities then here I'm doing top case sampling of 50 and I'm doing that because this is the hugging face default so just looking at the [00:38:23] hugging face docks here of a pipeline um there's a bunch of quarks that go into hugging face and I mean it's it's kind of a lot honestly but I guess the important one that I noticed is that they're using top K by default which is 50 and what that does is that uh so that's being used here as well and what that does is basically we want to take our probabilities and we only want to keep the top 50 probabilities and anything that is lower than the 50th probability uh we just [00:38:54] clamp to zero and renormalize and so that way we are never sampling very rare tokens uh the tokens we're going to be sampling are always in the top 50 of most likely tokens and this helps keep the model kind of on track and it doesn't blabber on and it doesn't get lost and doesn't go off the rails as easily uh and it kind of like um sticks in the vicinity of likely tokens a lot better so this is the way to do it in pytorch and you can step through it if you like I don't think it's super insightful so I'll speed through it but roughly speaking we get this new column [00:39:24] of of tokens we append them on x and basically The Columns of X grow until this y Loop gets tripped up and then finally we have an entire X of size um 5 by 30 in this case in this example and we can just basically print all those individual rows so I'm getting all the rows I'm getting all the tokens that were sampled and I'm using the decode function from Tik tokenizer to get back the string which we can print and so [00:39:55] terminal new terminal and let me python train gpt2 okay so these are the generations that we're getting hello I'm a language model not a program um new line new line Etc hello I'm a language model and one of the main things that bothers me when they create languages is how easy it becomes to create something that I me so this will [00:40:26] just like blabber on right in all these cases now one thing you will notice is that these Generations are not the generations of hugging face here and I can't find the discrepancy to be honest and I didn't fully go through all these options but probably there's something else hiding in on addition to the top P so I'm not able to match it up but just for correctness um down here Below in the juper notebook and using the hugging face model so this is the hugging face model here I was I replicated the code [00:40:56] and if I do this and I run that then I am getting the same results so basically the model internals are not wrong it's just I'm not 100% sure what the pipeline does in hugging face and that's why we're not able to match them up but otherwise the code is correct and we've loaded all the um tensors correctly so we're initializing the model correctly and everything here works so long story short uh We've Port it all the weights we initialize the gpt2 this is the exact opening gpt2 and it can generate [00:41:27] sequences and they look sensible and now here of course we're initializing with gbt2 model weights but now we want to initialize from scratch from random numbers and we want to actually train a model that will give us sequences as good as or better than these ones in quality and so that's what we turn to next so it turns out that using the random model is actually fairly straightforward because pytorch already initializes our model randomly and by default so when we create the GPT model [00:41:58] and the Constructor this is all um all of these layers and modules have random initializers that are there by default so when these linear layers get created and so on there's default Constructors for example using the Javier initialization that we saw in the past uh to construct the weights of these layers and so creating a random model instead of a gpt2 model is actually fairly straightforward and we would just come here and instead we would create model equals GPT and then we want to use [00:42:28] the default config GPT config and the default config uses the 124 M parameters so this is the random model initialization and we can run it and we should be able to get uh results now the results here of course are total garbage carbal and that's because this is random model and so we're just getting all these random token string pieces chunked up totally at random so that's what we have right now uh now one more thing I wanted to [00:42:59] point out by the way is in case you do not have Cuda available because you don't have a GPU you can still follow along with uh with what we're doing here uh to some extent uh and probably not to the very end because by the end we're going to be using multiple gpus and actually doing a serious training run uh but for now you can actually follow along decently okay uh so one thing that I like to do in pytorch is I like to autod detect the device that is available to you so in particular you could do that like this so here we are trying to detect a device [00:43:30] to run on that has the highest compute capability you can think about it that way so by default we start with CPU which of course is available everywhere because every single computer will have a CPU but then we can try to detect do you have a GPU you so use a Cuda and then if you don't have a Cuda uh do you at least have MPS MPS is the back end for Apple silicon so if you have a Macbook that is fairly new you probably have apple silicon on the inside and then that has a GPU that is actually fairly capable uh depending on which MacBook you have and so you can use MPS [00:44:01] which will be potentially faster than CPU and so we can print the device here now once we have the device we can actually use it in place of Puda so we just swap it in and notice that here when we call model on X if this x here is on CPU instead of GPU then it will work fine because here in the forward which is where P to will come when we create a pose we were careful to use the device of idx to create this tensor as [00:44:31] well and so there won't be any mismatch where one tensor is on CPU one is on GPU and uh that you can't combine those but here we are um carefully initializing on the correct device as indicated by the input to this model so this will autod detect device for me this will be of course GPU so using device Cuda uh but uh you can also run with um as I mentioned another device and it's not going to be too much slower so if I [00:45:01] override device here oops if I override device equals CPU then we'll still print Cuda of course but now we're actually using CPU one 2 3 4 5 6 okay about 6 seconds and actually we're not using torch compile and stuff like that which will speed up everything a lot faster as well but you can follow even on a CPU I think to a decent extent um so that's note on that okay so I do [00:45:32] want to loop around eventually into what it means to have different devices in pytorch and what it is exactly that pytorch does in the background for you when you do something like module. 2 device or where you take a torch tensor and do A2 device and what exactly happens and how that works but for now I'd like to get to training and I'd like to start training the model and for now let's just say the device makes code go fast um and let's go into how we can actually train the model so to train the model we're going to need some data set [00:46:02] and for me the best debugging simplest data set that I like to use is the tiny Shakespeare data set um and it's available at this URL so you can W get it or you can just search tiny Shakespeare data set and so um I have in my file system as just LS input.txt so I already downloaded it and here I'm reading the data set getting the first 1,000 characters and printing the first 100 now remember that gpt2 has uh roughly a compression ratio the tokenizer has a [00:46:32] compression ratio of rly 3 to1 so th000 characters is roughly 300 tokens here uh that will come out of this in the slice that we're currently getting so this is the first few uh characters and uh if you want to get a few more statistics on this we can do work count on input.txt so we can see that this is uh 40,000 lines about 200,000 words in this data set and about 1 million bytes in this file and knowing that this file is only asky characters there's no crazy unic [00:47:03] code here as far as I know and so every asky character is encoded with one bite and so this is uh the same number roughly a million characters inside this data set so that's the data set size uh by default very small and minimal data set for debugging to get us off the ground in order to tokenize this data set we're going to get Tik token encoding for gbt2 encode the data uh the first um 1,000 characters and then I'm only going to print the first 24 tokens [00:47:33] so these are the tokens as a list of integers and if you can read gpt2 tokens you will see that 198 here you'll recognize that as the slashing character so that is a new line and then here for example we have two new lines so that's 198 twice here uh so this is just a tokenization of the first 24 tokens so what we want to do now is we want to actually process these token sequences and feed them into a Transformer and in particular we want them we want to rearrange these tokens into this idx [00:48:05] variable that we're going to be feeding into the Transformer so we don't want a single very long onedimensional sequence we want an entire batch where each sequence is up to uh is basically T tokens and T cannot be larger than the maximum sequence length and then we have these t uh tlong uh sequences of tokens and we have B independent examples of sequences so how can we create a b BYT tensor that we can feed into the forward out of these onedimensional sequences so here's my favorite way to [00:48:36] to achieve this uh so if we take torch and then we create a tensor object out of this list of integers and just the first 24 tokens my favorite way to do this is basically you do a do view of um of uh for example 4x6 which multiply to 24 and so it's just a two-dimensional rearrangement of these tokens and you'll is that when you view this onedimensional sequence as two-dimensional 4x6 here the first six uh tokens uh up to here end up being the [00:49:06] first row the next six tokens here end up being the second row and so on and so basically it's just going to stack up this the um every six tokens in this case as independent rows and it creates a batch of tokens in this case and so for example if we are token 25 in the Transformer when we feed this in and this becomes the idx this token is going to see these three tokens and it's going to try to predict that 198 comes next so in this way we are able to [00:49:39] create this two-dimensional batch that's that's quite nice now in terms of the label that we're going to need for the Target to calculate the loss function how do we get that well we could write some code inside the forward pass because we know that the next uh token in a sequence which is the label is just to the right of us but you'll notice that actually we for this token at the very end 13 we don't actually have the next correct token because we didn't load it so uh we actually didn't get enough information here so I'll show you [00:50:09] my favorite way of basically getting these batches and I like to personally have not just the input to the Transformer which I like to call X but I also like to create the labels uh tensor which is of the exact same size as X but contains the targets at every single position and so here's the way that I like to do that I like to make sure that I fetch plus one uh token because we need the ground Truth for the very last token uh for 13 and then when we're creating the [00:50:39] input we take everything up to the last token not including and view it as 4x6 and when we're creating targets we do the buffer but starting at index one not index zero so we're skipping the first element and we view it in the exact same size and then when I print this here's what happens where we see that basically as an example for this token 25 its Target was 198 and that's now just stored at the exact same position in the Target tensor which is 198 and [00:51:10] also this last token 13 now has its label which is 198 and that's just because we loaded this plus one here so basically this is the way I like to do it you take long sequences you uh view them in two- dimensional terms so that you get batch of time and then we make sure to load one additional token so we basically load a buffer of tokens of B * t+ one and then we sort of offset things and view them and then we have two tensors one of them is the input to the [00:51:41] Transformer and the other exactly is the labels and so let's now reorganize this code and um create a very simple data loader object that tries to basically load these tokens and um feed them to the Transformer and calculate the loss okay so I reshuffled the code here uh accordingly so as you can see here I'm temporarily overwriting U to run a CPU and importing TI token and all of this should look familiar we're loading a th000 characters I'm setting BT to just be 4 and 32 right now just because we're [00:52:13] debugging we just want to have a single batch that's very small and all of this should now look familiar and follows what we did on the right and then here we get the we create the model and get the lits and so so here as you see I already ran this only runs in a few seconds but because we have a batch of uh 4X 32 our lits are now of size 4X 32x 50257 so those are the lit for what comes next at every position and now we [00:52:43] have the labels which are stored in y so now is the time to calculate the loss and then do the backward pass and then the optimization so let's first calculate the loss okay so to calculate the loss we're going to adjust the forward function of this NN module in the model and in particular we're not just going to be returning logits but also we're going to return the loss uh and we're going to not just pass in the input in thees but also the targets uh in y and now we will print not Lo just. shape anymore we're [00:53:14] actually going to print the loss function and then c. exit of zero so that we skip some of the sampling logic so now let's swing up to the forward function which gets called there because now we also have these optional targets and when we get the targets we can also calculate uh the loss and remember that we want to basically return uh log just loss and loss by default is none but um let's put this here if uh targets is [00:53:45] not none then we want to calculate loss and co-pilot is already getting excited here and calculating the what looks to be correct loss it is using the cross entropy loss as is documented here uh so this is a function in pytorch under the functional now what is actually happening here because it looks a little bit scary uh basically uh the F that cross entropy does not like multi-dimensional inputs it can't take a b BYT by vocap size so what's happening [00:54:15] here is that we are flattening out this three-dimensional tensor into just two Dimensions the First Dimension is going to be calculated automatically and it's going to be B * T and then the last Dimension is vocap size so basically this is uh flattening out this three-dimensional tensor of logits to just be two- dimensional B * T all individual examples and vocap size on uh in terms of the length of each row and then it's also flattening out the targets which are also two- dimensional at this stage but we're going to just [00:54:46] flatten them out so they're just a single tensor of B * T and this can then pass into cross entropy to calculate a loss which we return so this should basically at this point run because this is not too complicated so let's run it and let's see if we should be printing the loss and here we see that we printed 11 uh roughly and so [00:55:16] um and notice that this is the tensor of a single element which is this number 11 now we also want to be able to calculate a reasonable uh kind of starting point for a random rationalized Network so we covered this in previous videos but our vocabulary size is 50257 at initialization of the network you would hope that um every vocab element is getting roughly a uniform probability uh so that we're not favoring at initialization any token way too much we're not confidently wrong at [00:55:47] initialization so what we're hoping is that the probability of any arbitrary token is roughly 1 over 50,2 57 and now we can sanity check the loss because remember that the cross entropy loss is just basically the negative um log likelihood so if we now take this probability and we take it through the natural logarithm and then we do the negative that is the loss we expect at initialization and we covered this in previous videos so I would expect something around 10.82 and we're seeing [00:56:17] something around 11 so it's not way off this is roughly the probability I expect at initialization so that tells me that the at initialization or probability distribtion is roughly diffused it's a good starting point and we can now uh perform the optimization and tell the network which elements you know should follow correctly in what order so at this point we can do a l step backward calculate the gradients and do an optimization so let's get to that okay so let's do the optimization now um so here we [00:56:47] have the loss is this is how we get the loss but now basically we want a load for Loop here so 4 I in range let's do 50 steps or something like that uh let's create an Optimizer object in pytorch um and so here we are using the atom um Optimizer which is an alternative to the stochastic radian descent Optimizer SGD that we were using so SGD is a lot simpler atom is a bit more involved and I actually specifically like the atom W variation [00:57:17] because in my opinion it kind of just like fixes a bug um so adom w is a bug fix of atom is what I would say when we go to the documentation for atom W oh my gosh we see um that it takes a bunch of hyper parameters and it's a little bit more complicated than the SGD we were looking at before uh because in addition to basically updating the parameters with the gradient uh scaled by the Learning rate it keeps these buffers around and it keeps two buffers the m and the V which it calls the first and [00:57:48] the second moment so something that looks a bit like momentum and something that looks a bit like RMS prop if you're familiar with it but you don't have to be it's just kind of a normalization that happens on each gradient element individually and speeds up the optimization especially for language models but I'm not going to go into the detail right here we're going to treat it as a bit of a black box and it just optimizes um the objective faster than SGD which is what we've seen in the previous lectures so let's use it as a black box in our case uh create the [00:58:18] optimizer object and then go through the optimization the first thing to always make sure the co-pilot did not forget to zero the gradients so um always remember that you have to start with a zero gradient then when you get your loss and you do a DOT backward dot backward adds to gradients so it deposits gradients it it always does a plus equals on whatever the gradients are which is why you must set [00:58:48] them to zero so this accumulates the gradient from this loss and then we call the step function on the optimizer to um update the parameters and to um decrease the loss and then we print a step and the loss do item is used here because loss is a tensor with a single element do item will actually uh convert that to a single float and this float will live not will will live on the CPU so this gets to some of the internals again of the devices but loss is a is a tensor [00:59:20] with a single element and it lifts on GPU for me because I'm using gpus when you call item P torch behind the scenes will take that one-dimensional tensor ship it back to the CPU uh memory and convert it into a float that we can just print so this is the optimization and this should probably just work let's see what happens actually sorry let me instead of using CPU override let me delete that so [00:59:50] this is a bit faster for me and it runs on Cuda oh expected all tensors to be on the same device but found at least two devices Cuda zero and CPU so Cuda zero is the zeroth GPU because I actually have eight gpus on this box uh so the zeroth GPU in my box and CPU and model we have moved to device but when I was writing this code I actually introduced a bug because buff we never moved to [01:00:21] device and you have to be careful because you can't just do buff dot two of device um it's not stateful it doesn't convert it to be a device it instead uh returns pointer to a new memory which is on the device so you see how we can just do model that two a device that does not apply to tensors you have to do buff equals um b.2 device and then this should work okay so what do we expect to see we [01:00:52] expect to see a reasonable loss in the beginning and then we continue to optimize just the single batch and so we want to see that we can overfit this single batch we can we can crush this little batch and we can perfectly predict the indices on just this little batch and indeed that is roughly what we're seeing here so um we started off at roughly 10.82 11 in this case and then as we continue optimizing on this single batch without loading new examples we are making sure that we can overfit a single batch and we are getting to very very low loss so the Transformer is memorizing this [01:01:24] single individual batch and one more thing I didn't mention is uh the learning rate here is 3 E4 which is a pretty good default for most uh optimizations that you want to run at a very early debugging stage so this is our simple inter Loop and uh we are overfitting a single batch and this looks good so now what uh what comes next is we don't just want to overfit a single batch we actually want to do an optimization so we actually need to iterate these XY batches and create a little data loader uh that makes sure [01:01:54] that we're always getting a fresh batch and that we're actually optimizing a reasonable objective so let's do that next okay so this is what I came up with and I wrote a little data loader light um so what this data loader does is we're importing the token up here we're reading the entire text file from this single input.txt tokenizing it and then we're just printing the number of tokens in total and the number of batches in a single Epoch of iterating over this data set so how many unique batches do we output [01:02:24] before we loop back around the beginning of the document and start reading it again so we start off at position zero and then we simply walk the document in batches of B * T so we take chunks of B * T and then always Advance by B * T and um it's important to note that we're always advancing our position by exactly B * T but when we're fetching the tokens we're actually fetching from current position to B * t + 1 and we need that plus one because remember uh we need the [01:02:55] target token um for the last token in the current batch and so that way we can do um the XY exactly as we did it before and if we are to um run out of data we'll just loop back around to zero so this is one way to write a very very simple data loader um that simply just goes through the file in chunks and is good enough for us uh for current purposes and we're going to complexify it later and now we'd like to come back around here and [01:03:26] we'd like to actually use our data loader so the import Tik token has moved up and actually all of this is now useless so instead we just want a train loader for the training data and we want to use the same hyper parameters for four so B size was four and time was 32 and then here we need to get the XY for the current batch so let's see if copal gets it because this is simple enough uh so we call the next batch and then we um make sure that we have to [01:03:57] move our tensors from CPU to the device so here when I converted the tokens notice that I didn't actually move these tokens to the GPU I left them on CPU which is the default um and that's just because I'm trying not to waste too much memory on the GPU in this case this is a tiny data set and it would fit uh but it's fine to just uh ship it to GPU right now for for our purposes right now so we get the next batch we keep the data loader simple CPU class and then [01:04:29] here we actually ship it to the GPU and do all the computation and uh let's see if this runs so python train gbt2 pi and what do we expect to see before this actually happens what we expect to see is now we're actually getting the next batch so we expect to not overfit a single batch and so I expect our loss to come down but not too much and that's because I still expect it to come down because in the 50257 tokens many of those tokens never [01:05:00] occur in our data set so there are some very easy gains to be made here in the optimization by for example taking the biases of all the loits that never occur and driving them to negative infinity and that would basically just it's just that all of these crazy unic codes or different languages those tokens never occur so their probability should be very low and so the gains that we should be seeing are along the lines of basically deleting the usage of tokens that never occur that's probably most of the loss gain that we're going to see at this scale right now uh but we shouldn't [01:05:30] come to a zero uh because um we are only doing 50 iterations and I don't think that's enough to do an eoch right now so let's see what we got we um we have 338,000 tokens which makes sense with our 3:1 compression ratio because there are 1 million uh characters so one Epoch with the current setting of B and T will take 2, 600 batches and we're only doing 50 batches of optimization in [01:06:01] here so we start off in a familiar territory as expected and then we seem to come down to about 6.6 so basically things seem to be working okay right now with respect to our expectations so that's good okay next I want to actually fix a bug that we have in our code um it's not a major bug but it is a bug with respect to how gpt2 training uh should happen um so the buck is the following we were not being careful enough when we were loading the weights from hugging face [01:06:31] and we actually missed a little detail so if we come here notice that um the shape of these two tensors is the same so this one here is the token embedding at the bottom of the Transformer right so and this one here is the language modeling head at the top of the Transformer and both of these are basically two-dimensional tensors and they shape is identical so here the first one is the output embedding the token embedding and the second one is [01:07:02] this linear layer at the very top the classifier layer both of them are of shape 50257 X 768 um this one here is giving us our token embeddings at the bottom and this one here is taking the 768 channels of the Transformer and trying to upscale that to 50, 257 to get the Lis for the next token so they're both the same shape but more than that actually if you look at um comparing their elements um [01:07:33] in pytorch this is an element wise equality so then we use do all and we see that every single element is identical and more than that we see that if we actually look at the data pointer uh this is what this is a way in pytorch to get the actual pointer to the uh data and the storage we see that actually the pointer is identical so not only are these two separate tensors that happen to have the same shape and elements they're actually pointing to the identical tensor so what's happening here is that this is a common weight [01:08:03] tying scheme uh that actually comes from the original um from the original attention is all you need paper and actually even the reference before it so if we come here um eddings and softmax in the attention is all you need paper they mentioned that in our model we shared the same weight Matrix between the two embedding layers and the pre softmax linear transformation similar to 30 um so this [01:08:34] is an awkward way to phrase that these two are shared and they're tied and they're the same Matrix and the 30 reference is this paper um so this came out in 2017 and you can read the full paper but basically it argues for this weight tying scheme and I think intuitively the idea for why you might want to do this comes from from this paragraph here and basically you you can observe that um you actually want these two [01:09:04] matrices to behave similar in the following sense if two tokens are very similar semantically like maybe one of them is all lowercase and the other one is all uppercase or it's the same token in a different language or something like that if you have similarity between two tokens presumably you would expect that they are uh nearby in the token embedding space but in the exact same way you'd expect that if you have two tokens that are similar semantically you'd expect them to get the same probabilities at the output of a transformer because they are [01:09:35] semantically similar and so both positions in the Transformer at the very bottom and at the top have this property that similar tokens should have similar embeddings or similar weights and so this is what motivates their exploration here and they they kind of you know I don't want to go through the entire paper and and uh you can go through it but this is what they observe they also observe that if you look at the output embeddings they also behave like word embeddings um if you um if you just kind [01:10:06] of try to use those weights as word embeddings um so they kind of observe this similarity they try to tie them and they observe that they can get much better performance in that way and so this was adopted and the attention is all need paper and then it was used again in gpt2 as well so I couldn't find it in the Transformers implementation I'm not sure where they tie those embeddings but I can find it in the original gpt2 code U introduced by open aai so this is um [01:10:36] openai gpt2 Source model and here where they are forwarding this model and this is in tensorflow but uh that's okay we see that they get the wte token embeddings and then here is the incoder of the token embeddings and the position and then here at the bottom they Ed the WT again to do the lits so when they get the loits it's a math Mo of uh this output from the Transformer and the wte tensor is reused um and so the wte tensor [01:11:08] basically is used twice on the bottom of the Transformer and on the top of the Transformer and in the backward pass we'll get gradients contributions from both branches right and these gradients will add up um on the wte tensor um so we'll get a contribution from the classifier list and then at the very end of the Transformer we'll get a contribution at the at the bottom of it float floating again into the wte uh tensor so we want to we are currently not sharing WT and [01:11:38] our code but we want to do that um so weight sharing scheme um and one way to do this let's see if goil gets it oh it does okay uh so this is one way to do it uh basically relatively straightforward what we're doing here is we're taking the wte do weight and we're simply uh redirecting it to point to the LM head [01:12:08] so um this basically copies the data pointer right it copies the reference and now the wte weight becomes orphaned uh the old value of it and uh pytorch will clean it up python will clean it up and so we are only left with a single tensor and it's going to be used twice in the forward pass and uh this is to my knowledge all that's required so we should be able to use this and this should probably train uh we're just [01:12:39] going to basically be using this exact same sensor twice and um we weren't being careful with tracking the likelihoods but uh according to the paper and according to the results you'd actually expect slightly better results doing this and in addition to that one other reason that this is very very nice for us is that this is a ton of parameters right uh what is the size here it's 768 * 50257 so This Is 40 million parameters and this is a 124 million parameter [01:13:09] model so 40 divide 124 so this is like 30% of the parameters are being saved using this weight time scheme and so this might be one of the reasons that this is working slightly better if you're not training the model long enough because of the weight tying uh you don't have to train as many parameters and so you become more efficient um in terms of the training process uh because you have fewer parameters and you're putting in this inductive bias that these two embeddings should share similarities between tokens [01:13:40] so this is the way time scheme and we've saved a ton of parameters and we expect our model to work slightly better because of the scheme okay next I would like us to be a bit more careful with the initialization and to try to follow the way gpt2 initialized their model now unfortunately the gpt2 paper and the gpt3 paper are not very explicit about initialization so we kind of have to read between the lines uh and instead of going to the paper which is quite vague um there's a bit of information in the code that open I released so when we go to the model.py we see that when they [01:14:11] initialize their weights they are using the standard deviation of 0.02 and that's how they they so this is a normal distribution for the weights and the standard deviation is 0.02 for the bias they initialize that with zero and then when we scroll down here why is this not scrolling um the token embeddings are initialized at 0.02 and position embeddings at 0.01 for some reason so those are the [01:14:42] initializations and we'd like to mirror that in gpt2 uh in our module here so here's a snippet of code that I sort of came up with very quickly so what's happening here is at the end of our initializer for the GPT module we're calling the apply function of NN module and that iterates all the sub modules of this module and uh applies in it weights function on them and so what's happening here is that we're in we're iterating all the modules [01:15:13] here and if they are an nn. linear module then we're going to make sure to initialize the weight using a normal with the standard deviation of 0.02 if there's a bias in this layer we will make sure to initialize that to zero note that zero initialization for the bias is not actually the pyto default um by default the bias here is initialized with a uniform so uh that's interesting so we make sure to use zero and for the embedding we're just going to use 0.02 and um keep it the same um [01:15:43] so we're not going to change it to 0.01 for positional because it's about the same and then if you look through our model the only other layer that requires initialization and that has parameters is the layer norm and the fighter defer initialization sets the scale in the layer Norm to be one and the offset in the layer Norm to be zero so that's exactly what we want and so we're just going to uh keep it that way and so this is the default initialization if we are following the um where is it the uh gpt2 [01:16:14] uh source code that they released I would like to point out by the way that um typically the standard deviation here on this initialization if you follow the Javier initialization would be one of over the square root of the number of features that are incoming into this layer but if you'll notice actually 0.02 is basically consistent with that because the the model sizes inside these Transformers for gpt2 are roughly 768 1600 Etc so 1 over the square root of for example 768 gives us [01:16:44] 0.03 if we plug in 600 1,600 we get 0.02 if we plug in three times that 0.014 Etc so basically 0.02 is roughly in the vicinity of reasonable values for the for um for these initializations anyway so so it's not uh completely crazy to be hard coding 0.02 here uh but you'd like typically uh some something that grows with the model size instead but we will keep this because that is [01:17:15] the gpt2 initialization per their source code but we are not fully done yet on initialization because there's one more caveat here so here a mod initialization which accounts for the accumulation on the residual path with model depth is used we scale the weight of residual layers of initialization by factor of one over squ of n where n is the number of residual layers so this is what gbt2 paper says so we have not implemented that yet and uh we can do so now now I'd like to actually kind of like motivate a little bit what they mean here I think um so [01:17:47] here's roughly what they mean if you start out with zeros in your residual stream remember that each residual stream is a is of this form where we continue adding to it X is X plus something some kind of contribution so every single block of the residual uh Network contributes some uh amount and it gets added and so what ends up happening is that the variance of the activations in the residual stream grows [01:18:18] so here's a small example if we start at zero and then we for 100 times uh we have sort of this residual stream of of 768 uh zeros and then 100 times we add um random which is a normal distribution zero mean one standard deviation if we add to it then by the end the residual stream has grown to have standard deviation of 10 and that's just because um we're always adding um these numbers and so this scaling factor that they use [01:18:50] here exactly compensates for that growth so if we take n and we basically um scale down every one of these contributions into the residual stream by one over theare Ro of n so 1 over theun of n is n to the 0.5 right because n the5 is the square root and then one over the square root is n.5 if we scale it in this way then we see that we actually get um [01:19:20] one so this is a way to control the growth of of activations inside the residual stream in the forward pass and so we'd like to initialize in the same way where these weights that are at the end of each block so this C uh layer uh the gbt paper proposes to scale down those weights by one over the square root of the number of residual layers so one crude way to implement this is the following I don't know if this is uh pyro sanctioned but it works [01:19:50] for me is we'll do in the initialization see that s that do special nanog GPT uh scale in it is one so we're setting um kind of like a flag for this module there must be a better way in py torch right but I don't know okay so we're basically attaching this flag and trying to make sure that it doesn't conflict with anything previously and then when we come down [01:20:20] here this STD should be 0.02 by default but then if haat um module of this thing then STD * equals um copal is not guessing correctly uh so we want one over the square root of the number of layers so um the number of residual layers here is twice times Salt out config layers and then [01:20:52] this times .5 so we want to scale down that standard deviation and this should be um correct and Implement that I should clarify by the way that the two times number of layers comes from the fact that every single one of our layers in the Transformer actually has two blocks that add to the ridal pathway right we have the attention and then the MLP so that's where the two times comes from and the other thing to mention is that uh what's slightly awkward but we're not going to fix it is that um [01:21:23] because we are weight sharing the wte and the LM head in this iteration of our old subm modules we're going to actually come around to that tensor twice so we're going to first initialize it as an embedding with 0.02 and then we're going to come back around it again in a linear and initialize it again using 0.02 and it's going to be 0.02 because the LM head is of course not not scaled so it's not going to come here it's just it's going to be basically initialized twice using the identical same initialization but that's okay and then scrolling over [01:21:56] here I added uh some code here so that we have reproducibility um to set the seeds and now we should be able to python train gpt2 pi and let this running and as far as I know this is the gpt2 initialization uh in the way we've implemented it right now so this looks uh reasonable to me okay so at this point we have the gpt2 model we have some confidence that it's correctly implemented we've initialized it properly and we have a data loader [01:22:26] that's iterating through data batches and we can train so now comes the fun part I'd like us to speed up the training by a lot so we're getting our money's worth with respect to the hardware that we are uh using here and uh we're going to speed up the training by quite a bit uh now you always want to start with what Hardware do you have what does it offer and are you fully utilizing it so in my case if we go to Nvidia SMI we can see that I have eight gpus and each one of [01:22:57] those gpus is an a100 sxm 80 gb so this is the GPU that I have available to me in this box now when I look when I use um to spin up these kinds of Boxes by the way my favorite place to go to is Lambda Labs um they do sponsor my development and that of my projects uh but I this is my favorite place to go and this is where you can spin up one of these machines and you pay per hour and it's very very simple so I like to spin them up and then connect vsod to it and that's how I [01:23:28] develop now when we look at the A1 100s that are available here a100 80 GB sxm is the um GPU that I have here and we have a bunch of numbers here for um how many calculations you can expect out of this GPU so when I come over here and I break in right after here so python trity so I'm breaking in right after we calculate the loit and laws and the interesting thing I'd like you to note is when I do lit. dtype this [01:24:02] prints a torch. FL 32 so by default iny torch when you create tensors um and this is the case for all the activations and for the parameters of the network and so on by default everything is in float 32 that means that every single number activation or weight and so on is using a float representation that has 32 bits and uh that's actually quite a bit of memory and it turns out empirically that for deep learning as a computational workload this is way too much and deep learning and the training [01:24:32] of these networks can tolerate significantly lower precisions um not all computational workflows can tolerate small Precision so for example um if we go back to to the data sheet you'll see that actually these gpus support up to fp64 and this is quite useful I understand for a lot of um scientific Computing applications and there really need this uh but we don't need that much Precision for deep learning training So currently we are here fp32 and with this code as it is right [01:25:04] now we expect to get at at most 19.5 Tera flops of performance that means we're doing 19.5 trillion operations floating Point operations so this is floating Point multiply add most um most likely and so these are the floating Point operations uh now notice that if we are willing to go down in Precision so tf32 is a lower Precision format we're going to see in a second you can actually get an 8X [01:25:34] Improvement here and if you're willing to go down to float 16 or B float 16 you can actually get time 16x performance all the way to 312 Tera flops you see here that Nvidia likes to site numbers that have an asterisk here this asterisk uh says with sparsity uh but we are not going to be using sparsity in R code and I don't know that this is very widely used in the industry right now so most people look at this number here uh without sparcity and you'll notice that we could have got even more here but [01:26:05] this is int 8 and int 8 is used for inference not for training uh because int 8 has a um it basically has um uniform spacing um and uh we actually require a float so that we get a better match to the uh normal distributions that occur during training of neural networks where both activations and weights are distributed as a normal distribution and so uh floating points are really [01:26:35] important to to match that uh representation so we're not typically using int 8 uh for training but we are using it for inference and if we bring down the Precision we can get a lot more Terra flops out of the tensor course available in the gpus we'll talk about that in a second but in addition to that if all of these numbers have fewer bits of representation it's going to be much easier to move them around and that's where we start to get into the memory bandwidth and the memory of the model so not only do we have a finite capacity of [01:27:06] the number of bits that our GPU can store but in addition to that there's a speed with which you can access this memory um and you have a certain memory bandwidth it's a very precious resource and in fact many of the deep learning uh work workloads for training are memory bound and what that means is actually that the tensor cores that do all these extremely fast multiplications most of the time they're waiting around they're idle um because we can't feed them with data fast enough we can't load the data [01:27:37] fast enough from memory so typical utilizations of your Hardware if you're getting 60% uh utilization you're actually doing extremely well um so half of the time in a well-tuned application your tensor cores are not doing multiplies because the data is not available so the memory bandwidth here is extremely important as well and if we come down in the Precision for all the floats all the numbers weights and activations suddenly require less memory so we can store more and we can access it faster so everything speeds up and [01:28:07] it's amazing and now let's reap the benefits of it um and let's first look at the tensor float 32 format okay so first of all what are tensor cores well tensor course tensor core is just an instruction in the a100 architecture right so so what it does is it does basically a little 4x4 Matrix multiply so uh this is just matrix multiplication here of 4x4 matrices and there are multiple configurations as to [01:28:38] what Precision any of these matrices are it in what Precision the internal accumulate happens and then what is the output Precision input precisions Etc so there's a few switches but it's basically a 4x4 multiply and then anytime we have any operations that require Magic multiplication uh they get broken up into these into this instruction of little 4x4 multiply and so everything gets broken up into this instruction because it's the fastest way to multiply matrices and it turns out that most of the computational work that [01:29:08] we're doing up above uh all of it really is matrix multiplication most of the work computationally happens in the linear layers um linear linear Etc there's a few things sandwiched in between so there's some additions in residuals there's some G nonlinearities there's some layer Norms Etc but if you just time them you'll see that these are nothing like basically the in Transformer is just a bunch of Matrix multiplications really um and especially at this small scale 124 million [01:29:39] parameter model actually the biggest matrix multiplication by far is the classifier layer at the top that is a massive Matrix multiply of going from 768 to 50257 and that Matrix multiply dominates anything else that happens in that Network roughly speaking so it's Matrix multiplies that become a lot faster which are hidden inside our linear layers and they're accelerated through tensor course now the best reference I would say for tensor course is basically [01:30:09] just go to the um a 100 architecture white paper and then it's pretty detailed and but I think people it's like relatively readable mostly if you half understand what's happening um so figure 9 tensor float 32 so this is the explanation basically for tf32 and what happens here and you see that there's many configuration options here available so the input operands and what precisions are they in the accumulator and um what um basically [01:30:41] the um the internal representation within the instruction when you do the accumulate of this matrix multiplication so the intermediate plus equals um of the intermediate little vector multiplies here that all happens in fp32 and then uh this is an aex improvement as I mentioned to the Ops that we get so tf32 specifically we're looking at this row here and the way this works is um normally fp32 has 32 bits [01:31:14] tf32 is the exact same bits we have one sign bit we have eight exponent bits except the mantisa bits get cropped in the float and so basically um we end up with just 19 bits instead of 32 bits because the last 133 bits get truncated they get dropped um and all this is internal to the instruction so none of it is visible to anything in our pytorch uh none of our pytorch code will change all of the numbers will look identical [01:31:45] it's just that when you call the tensor core um instruction internally in the hardware it will crop out these 13 bits and that allows it to uh calculate this little Matrix multiply significantly faster 8X faster now of course this speed up comes at a cost and the cost is that we are reducing the Precision our accumulate is still an fp32 our output is fp32 our inputs are fp32 but internally things get truncated in the operand to perform the operation faster [01:32:17] and so our results are starting to be a bit more approximate but empirically when you actually train with this you basically can't tell the difference so the reason I like tf32 is because if you can tolerate a little bit of a Precision fudge um then this is free like none of your codes sees this it's fully internal to the operation and the operation to you just go 8X faster and it's a bit more approximate and so it's a pretty sweet spot I would say in optimization and uh let's see what that looks like first so I've set up our Cod [01:32:48] to just time the uh iterations so import time I changed the hyper parameters so that we have something a bit more that reflects uh kind of workload that we want to run uh because we want to do a fairly large run at the end of this so let's use batch size 16 and let's now use the actual gpt2 um maximum sequence length of 10,24 tokens uh so this is the configuration and then for 50 iterations I'm just doing something very lazy here I'm doing time. time to get the current [01:33:19] time and then this is the optimization Loop and now I want to time how long this takes now one issue with working with gpus is that as your CPU um when your CPU runs it's just scheduling work on GPU it's ordering some work right and so it send a request and then it continues running and so we can actually it can happen sometimes that we sort of um speed through this and we queue up a lot of kernels to run [01:33:50] on the GPU and then the CPU sort of like gets here and takes time at time but actually the GPU is still running because it takes it time to actually work through the work that was scheduled to run and so you're just building up a queue for the GPU and so actually if you need to you want to wait toat data synchronize and this will wait for the GPU to finish all the work that was scheduled to run up above here and then we can actually take the time so basically we're waiting for the GPU to stop this iteration take time and then [01:34:22] we're going to just print it so so here I'm going to run the training Loop and here on the right I'm watching Nvidia SMI so we start off at zero um we're not using the GPU and then by default P will use gpu0 so we see that it gets filled up and we're using 35 GB out of 80 gabt available and then here on the left we see that because we've cranked up the batch size now it's only 20 batches to do a single Epoch on our tiny Shakespeare [01:34:54] and we see that we're seeing roughly a th000 milliseconds per iteration here right so the first iteration sometimes is slower and that's because pytorch might be doing a lot of initializations here on the very first iteration and so it's probably initializing all these uh tensors and buffers to hold all the gradients and I'm not 100% sure all the work that happens here but uh this could be a slower iteration when you're timing your logic you always want to be careful with that but basically we're seeing a th000 milliseconds per iteration [01:35:24] um and so this will run for roughly 50 seconds as we have it right now so that's our Baseline in flo 32 one more thing I wanted to mention is that if this doesn't fit into your GPU and you're getting out of memory errors then start decreasing your batch size until things fit so instead of 16 try eight or four or whatever you need to fit um the batch into your GPU and if you have a bigger GPU you can actually potentially get away with 32 and so on uh by default you want to basically max out has Max [01:35:54] Max out the batch size that fits on your GPU and you want to keep it nice numbers so use numbers that have lots of powers of two in them so 16 is a good number 8 24 32 48 These are nice numbers but don't use something like 17 uh because that will run very inefficiently on a GPU uh and we're going to see that a bit later as well so for now let's just stick with 16124 and uh the one thing that I added also here and I ran it again is I'm [01:36:25] calculating a tokens per second throughput during training because we might end up changing the backat size around over time but tokens per second is the objective measure that we actually really care about how many tokens of data are we training on and what is the throughput of tokens that we're getting in our optimization so right now we're processing and training on 163,000 tokens per second roughly and that's a bit more objective metric okay so let's now enable tf32 now luckily pytorch makes this fairly easy [01:36:56] for us and uh to enable tf32 you just need to do a single line and is this and when we go to the py documentation here for this function basically this tells pych what kind of kernels to run and by default I believe it is highest highest Precision for mat M and that means that everything happens in float 32 just like it did before but if we set it to high as we do right now Matrix multiplications will not use tensor flow 32 when it's [01:37:26] available my GPU is a100 so it's an ampere series and therefore tf32 is available if you have an older GPU this might not be available for you but for my GPU it's available and so what I expect P to do is that every single place where we see an nn. linear inside there there's a matrix multiplication and I expect that matrix multiplication now to be um running on tensor course utilizing the TF 32% so this is the single line of change [01:37:58] that is I believe necessary and let's rerun this now we saw that um in terms of the throughput that is promised to us we're supposed to be getting 8X roughly so let's see what happens and that 8X came from here right um 8X and it also came from looking at it um here 156 T flops instead of of 19.5 okay so what actually happened uh so we're seeing that our throughput [01:38:29] roughly 3x not aex so we are going we're from 1,000 milliseconds we're going down to 300 milliseconds and our throughput is now about 50,000 tokens per second so we have a roughly 3x instead of 8X so what happened and basically What's Happening Here is again a lot of these workloads are memory bound and so even though the tf32 offers in principle a lot faster throughput all of these numbers [01:38:59] everywhere are still float 32s and it's float 32 numbers that are being shipped all over the place through the memory system and is just costing us way too much time to shuttle around all this data and so even though we've made the multiply itself much faster uh we are memory bound and we're not actually seeing the full benefit uh that would come from uh this napkin math here uh that said we are getting one a 3X faster throughput and this is free um single line of code in P torch all your variables are still float 32 everywhere [01:39:30] it just runs faster and it's slightly more approximate but we're not going to notice it basically uh so that's tf32 okay so let's now continue so we've exercised this row and um we saw that we can crop out some of the Precision inside the operation itself but we saw that we're still memory bound we're still moving around all these floats right otherwise and we're paying that cost because of this so let's now decrease the amount of stuff that we're going to be moving around and we're going to do that by dropping down to B [01:40:01] float 16 so we're only going to be maintaining 16 bits per float and we're going to use the B flat 16 and I'll explain in a bit uh fp16 difference and uh we're going to be in this row so when we go back to the documentation here for the a 100 um we see here the precisions that are are available and this is the original fp32 the tf32 crops out the Precision and then here in bf16 you see that it is very similar to [01:40:33] tf32 but it's even more aggressive in cropping off of the Precision the mantisa of this float so the important thing with B float 16 is that the exponent bits and the sign bit of course remain unchanged so if you're familiar with your float numbers and I think this should should probably be an entire video by itself the exponent sets the range that you can represent of your numbers and the Precision is how much Precision you have for your numbers and so the range of [01:41:04] numbers is identical but we can we have fewer possibilities within that range because we are truncating the Mena so we have less Precision in that range what that means is that things are actually fairly nice because we have the original range of numbers that are representable in float but we just have less Precision for it and the difference with fp16 is that they actually touch and change the range so fp16 cannot represent the full range of fp32 it has [01:41:35] a reduced range and that's where you start to actually run into issues because now you need uh these gradient scalers and things like that and I'm not going to go into the detail of that in this video because that's a whole video by itself but fb16 actually historically came first that was available in the Volta series before Amper and so fp16 came first and everyone started to train in fp16 but everyone had to use all these gradient scaling operations which are kind of annoying and it's an additional source of state and [01:42:05] complexity and the reason for that was because the exponent range was reduced in fp16 so that's the i e fp16 spec and then they came out with bf16 and the Ampere and they made it much simpler because we're just truncating manessa we have the exact same range and we do not need gradient scalers so everything is much much simpler now when we do use bf16 though we are impacting the numbers that we might be seeing in our pytorch code these this change is not just local [01:42:35] to the operation itself so let's see how that works um there's some documentation here that so I think this is probably the best best page to explain how to use mixed Precision in pytorch um because there are many other tutorials and so on even within pitor documentation that are a lot more confusing and so I recommend specifically this one because there's five other copies that I would not recommend and then when we come here ignore everything about everything [01:43:05] ignore everything about gradient scalers and only look at torch. AutoCast and basically also this comes to a single line of code at the end so this is the context manager that we want and we want to use that in our Network when you click into the torch. AutoCast autocasting it has a few more uh a bit more guideline for you so it's telling you do not call B flat 16 on any of your tensors just use AutoCast and [01:43:36] only surround the uh forward pass of the model and the loss calculation and that's the only two things that you should be surrounding leave the backward and the optimizer step alone so that's the guidance that comes from the P team so we're going to follow that guidance and for us because the L calculation is inside of the model forward pass for us we are going to be doing this and then we don't want to be using torch Flo 16 because if we do that we need to start using gradient scalers as well so we are going to be using B float [01:44:06] 16 this is only possible to do an ampere uh but this means that the changes are extremely minimal like basically just this one line of code um let me first break in to here before we actually run this so right after logits I'd like to show you that different from the tf32 that we saw this is actually going to impact our tensors so this Lis tensor if we now look at [01:44:36] this and we look at the dtype we suddenly see that this is now B float 16 uh it's not float 32 anymore so our activations have been changed the activations tensor is now B FL 16 but not everything has changed so model. Transformer wte uh this is the weight uh token embedding table it has a weight inside it and the dtype of this weight this parameter is still torch float 32 so our [01:45:06] parameters seem to still be in float 32 but our activations the loits are now in P 16 so clearly this is why we get the mixed Precision some things pytorch is keeping inlow 32 some things pytorch is converting to lower Precision um and what gets converted at what point is not super clear I remember scrolling down is it here okay I can't find [01:45:37] it I I thought it was here okay there we go so there are a few docks on when you're using this AutoCast what gets converted to B FL 16 and and when so for example only these Matrix multiply like operations get converted to float 16 but a lot of operations remain in float 32 so in particular a lot of normalizations like layer norms and things like that not all of those layers might be converted um so only some layers selectively would be running B flat 16 [01:46:07] but things like softmax uh layer Norms uh log um log soft Max so loss function calculations a lot of those things might remain in float 32 because they are more susceptible to Precision changes major multiplies are fairly um robust to Precision changes uh so some parts of the network are um impacted more or less by the Precision change um so basically only some parts of the of the model are running in reduced Precision let's take it for a [01:46:38] spin and let's actually see what kind of improvement we achieve here okay so we used to be 333 milliseconds we're now 300 and we used to be somewhere around 50,000 tokens per second we're now at 55 so we're definitely running faster but maybe not a lot faster and that's because there are still many many bottlenecks in our gbt2 we're just getting started but we have dropped down the precision as far as we can with my [01:47:09] current GPU which is a100 we're using pytorch AutoCast unfortunately I don't actually exactly know what pytorch AutoCast do uh does I don't actually know exactly what's in B flat 16 what's in float 32 we could go in and we could start to scrutinize it um but these are the kinds of rules that pytorch has internally and unfortunately they don't documented very well uh so we're not going to go into that into in too much detail but for now we are training in B flow 16 we do not [01:47:39] need a gradient scaler and the reason things are running faster is because um we are able to run tensor course in B FL 16 now that means we are in this row but uh we are also paying in Precision for this uh so um we expect slightly less accurate results with respect to the original fp32 but empirically in many cases this is a worth it uh kind of tradeoff because it allows you to run faster and you could for example train longer and make up for the uh for that [01:48:10] Precision decrease so um that's b46 for now okay so as we can see we are currently at about 300 milliseconds uh per iteration and we're now going to reach for some really heavy weapons in the pie torch Arsenal and in particular we're going to introduce torch. compile so torch. compile is really quite incredible infrastructure from the pytorch team and it's basically a compiler for neural networks like it's almost like GCC for CN C++ code this is just this GCC of neural nuts so came out [01:48:42] a while ago and extremely simple to use um the way to use torch compile is to do this it's a single line of code to compile your model and return it now this line of code will cost you compilation time but as you might guess it's going to make the code a lot faster so let's actually run that because this will take some time to run but currently remember we're at 300 milliseconds and we'll see what happens now while this is running I'd like to explain a little bit of what torch. compile does under the hood uh so feel free to read this page [01:49:15] of P torch but basically there's no real good reason for you to not use torch compile in your pie torch I kind of feel like you should be using almost by default if you're not uh unless you're debugging and you want your code to run really fast and there's one line here in torch compile that I found that actually kind of like gets to why this is faster speed up mainly comes from reducing python overhead and GPU read wrs so let me unpack that a little bit um okay here we are okay so we went from 300 milliseconds we're now running at 129 [01:49:46] milliseconds so this is uh 300 129 about 2.3x Improvement from a single line of code in py torch uh so quite incredible so what is happening what's happening under the hood well when you pass the model to torch compile what we have here in this NN module this is really just the algorithmic description of what we'd like to happen in our Network and torch compile will analyze the entire thing and it will look at what operations You' like to use and with the benefit of [01:50:18] knowing exactly what's going to happen it doesn't have to run in What's called the e mode it doesn't have to just kind of like go layer by layer like the python interpreter normally would start at the forward and the python interpreter will go okay let's do this operation and then let's do that operation and it kind of materializes all the operations as it goes through uh so these um calculations are dispatched and run in this order and the python interpreter and this code doesn't know what kind of operations are [01:50:49] going to happen later but torch compile sees your entire code at the same time and it's able to know what operations you intend to run and it will kind of optimize that process the first thing it will do is will it will take out the python interpreter from the forward pass entirely and it will kind of compile this entire neural net as a single object with no python interpreter involved so it knows exactly what's going to run and we'll just run that and it's all going to be running in efficient code uh the second thing that happens is uh this read write that they mentioned [01:51:21] very briefly so a good example of that I think is the G nonlinearity that we've been looking at so here we use the n and G now this here is me uh basically just breaking up the inang Galu uh which you remember has this formula so this here is the equivalent implementation to what's happening inside g algorithmic l it's identical Now by default if uh we just we using this instead of ending. G here what would happen without torch compile [01:51:51] well the python interpreter would make its way here and then it would be okay well there's an input well let me first let me raise this input to the third power and it's going to dispatch a kernel that takes your input and raises it to the third power and that kernel will run and when this kernel runs what ends up happening is this input is stored in the memory of the GPU so here's a helpful example of the layout of what's happening right you have your CPU this is in every single computer [01:52:21] there's a few cores in there and you have your uh Ram uh your memory and the CPU can talk to the memory and this is all well known but now we've added the GPU and the GPU is a slightly different architecture of course they can communicate and it's different in that it's got a lot more course than a CPU all of those cores are individually a lot simpler too but it also has memory right this high bandwidth memory I'm sorry if I'm botching it hbm I don't even know what that stands for I'm just [01:52:51] realizing that but uh this is the memory and it's very equivalent to uh RAM basically in the computer and what's happening is that input is living in the memory and when you do input cubed this has to travel to the GPU to the course and to all the caches and registers on the actual chip of this GPU and it has to calculate the all the elements to the third and then it saves the result back to the memory and it's [01:53:22] this uh travel time that actually causes a lot of issues so here remember this memory bandwidth we can communicate about 2 terabytes per second which is a lot but also we have to Traverse this link and it's very slow so here on the GPU we're on chip and everything is super fast within the chip but going to the memory is extremely expensive takes extremely long amount of time and so we load the input do the calculations and load back the output and this round trip takes a lot of time [01:53:53] and now right after we do that we multiply by this constant so what happens then is we dispatch another kernel and then the result travels back all the elements get multiplied by a constant and then the results travel back to the memory and then we take the result and we add back input and so this entire thing again travels to the GPU adds the inputs and gets written back so we're making all these round trips from the memory to actually where the comput happens because all the tensor cores and [01:54:24] alus and everything like that is all stored on the chip in the GPU so we're doing a ton of round trips and pytorch uh without using torch compile doesn't know to optimize this because it doesn't know what kind of operations you're running later you're just telling it raise the power to the third then do this then do that and it will just do that in that sequence but torch compile sees your entire code it will come here and it will realize wait all of these are elementwise operations and actually what I'm going to do is I'm going to do a single trip of input to the GPU then [01:54:56] for every single element I'm going to do all of these operations while that memory is on the GPU or chunks of it rather and then I'm going to write back a single time so we're not going to have these round trips and that's one example of what's called kernel fusion and is a major way in which everything is sped up so basically if you have your benefit of onet and you know exactly what you're going to compute you can optimize your round trips to the memory and you're not going to pay the the memory bandwidth cost and that's fundamentally what makes some of these operations a lot faster [01:55:27] and what they mean by read writes here so let me erase this because we are not using it and yeah we should be using torch compile and our code is now significantly faster and we're doing about 125,000 tokens per second but we still have a long way to go before we move on I wanted to supplement the discussion a little bit with a few more figures uh because this is a complic topic but it's worth understanding on a high level uh what's happening here and I could probably spend an entire video of like [01:55:58] two hours on this but just the preview of that basically so this chip here that is uh the GPU this chip is where all the calculations happen mostly but this chip also does have some memory in it but most of the memory by far is here in the high bandwidth memory hbm and is connected they're connected um but these are two separate chips basically now here this is a zoom in of kind of this cartoon diagram of a GPU and what [01:56:30] we're seeing here is number one you see this hbm I I realize it's probably very small for you but on the sides here it says hbm and so that that's the links to the hbm now the hbm is again off chip on the chip there are a large number of these streaming multiprocessors uh every one of these is an SM there's 120 of them in total and this is where the a lot of the calculations happen and this is a zoom in of a single individual as it has these four quadrants and see for example tensor core this is where a lot of the [01:57:00] Matrix multiply stuff happens but there's all these other units to do all different kinds of calculations for fp64 fp32 and for integers and so on now so we have all this uh logic here to do the calculations but in addition to that on the chip there is memory sprinkled throughout the chip so L2 cache is some amount of memory that lives on the chip and then on the SMS themselves there's L1 cache I realized it's probably very small for you but this blue bar is L1 [01:57:31] and there's also registers um and so there is memory stored here but the way this memory is stored is very different from the way memory is stored in hbm uh this is a very different implementation uh using um just in terms of like what the Silicon looks like it's a very different implementation um so here you would using transistors and capacitors and here it's a very different implementation uh with SRAM and what that looks like but long story short is [01:58:01] um there is um memory inside the chip but it's not a lot of memory that's the critical point so this is some C this is a example diagram of a slightly different GPU just like here where it shows that for example typical numbers for CPU Dam memory which is this thing here you might have one tab of this right but it would be extremely expensive to access especially for a GPU you have to go through the CPU here now next we have the hbm so we have tens of gigabytes of hbm memory on a typical GPU [01:58:33] here but it's as I mentioned very expensive to access and then on the chip itself everything is extremely fast within the chip but we only have couple 10 megabytes of memory collectively throughout the Chip And so there's just not enough space because the memory is very expensive on the chip and so there's not a lot of it but it is lightning fast to access in relative terms and so basically whenever we have these kernels um the more accurate picture of what's Happening Here is that [01:59:03] we take these inputs which live by default on the global memory and now we need to perform some calculation so we start streaming the data from the um Global memory to the uh chip we perform the calculations on the chip and then stream it back and store it back to the global memory right and so if we are if we don't have torch compile we are streaming the data through the chip doing the calculations and saving to the memory and we're doing those round trips many many times but uh if it's torch compiled then [01:59:33] we start streaming the memory as before but then while we're on the chip we're we're we have a chunk of the uh data that we're trying to process so that chunk now lives on the chip while it's on the chip it's extremely fast to operate on so if we have kernel Fusion we can do all the operations right there in an element-wise fashion and those are very cheap and then we do a single round trip back to the global memory so operator Fusion basically allows you to keep your chunk of data on the Chip And do lots of calculations on it before you [02:00:04] write it back and that gives huge savings and that's why torch compile ends up being a lot faster or that's one of the major reasons uh so again just a very brief intro to the memory hierarchy and roughly what torch compile does for you now torch compile is amazing but there are operations torch compile will not find and an amazing example of that is Flash attention to which we turn next so flash attention comes from this paper from uh Stanford in 2022 and it's this incredible algorithm [02:00:36] for performing attention so um and running it a lot faster so flash attention will come here and we will take out these four lines and Flash attention implements these four lines really really quickly and how does it do that well flash attention is a kernel Fusion operation so you see here we have um in this diagram they're showing P torch and you have these four operations uh they're including Dropout but we are not using [02:01:06] Dropout here so we just have these four lines of code here and instead of those we are fusing them into a single fused kernel of flash attention so it's an it's a it's a kernel Fusion algorithm but it's a kernel Fusion that torch compile cannot find and the reason that it cannot find it is that it um requires an algorithmic rewrite of how attention is actually implemented here in this case and what's remarkable about it is that uh flash attention actually if you just count the number of flops flash attention does [02:01:37] more flops than this attention here but flash attention is actually significantly faster in fact they site 7. six times faster potentially and that's because it is very mindful of the memory hierarchy as I described it just now and so it's very mindful about what's in high bandwidth memory what's in the shared memory and it is very careful with how it orchestrates the computation such that we have fewer reads and writes to the high bandwidth memory and so even though we're doing [02:02:08] more flops the expensive part is they load and store into hbm and that's what they avoid and so in particular they do not ever materialize this end byend attention Matrix this ATT here a flash attention is designed such that this Matrix never gets materialized at any point and it never gets read or written to the hbm and this is a very large Matrix right so um because this is where all the queries and keys interact and we're sort of getting um for each head for each batch element [02:02:40] we're getting a t BYT Matrix of attention which is a Million numbers even for a single head at a single batch index at like so so basically this is a ton of memory and and this is never materialized and the way that this is achieved is that basically the fundamental algorithmic rewrite here relies on this online softmax trick which was proposed previously and I'll show you the paper in a bit and the online softmax trick coming from a previous paper um shows how you can [02:03:10] incrementally evaluate a soft Max without having to sort of realize all of the inputs to the softmax to do the normalization and you do that by having these intermediate variables M and L and there's an update to them that allows you to evaluate the softmax in an online manner um now flash attention actually so recently flash attention 2 came out as well so I have that paper up here as well uh that has additional gains to how it calculates flash attention and the original paper that this is based on [02:03:40] basically is this online normalizer calculation for softmax and remarkably it came out of Nvidia and it came out of it like really early 2018 so this is 4 years before flash attention and this paper says that we propose a way to compute the classical softmax with fewer memory accesses and hypothesize that this reduction in memory accesses should improve softmax performance on actual hardware and so they are extremely correct in this hypothesis but it's really fascinating [02:04:10] to me that they're from Nvidia and that they had this realization but they didn't actually take it to the actual flash attention that had to come four years later from Stanford so I don't fully understand the historical how this happened historically um but they do basically propose this online update to the softmax uh right here and this is fundamentally what they reuse here to calculate the softmax in a streaming Manner and then they realize they can actually fuse all the other operations with the online sofx calculation into a [02:04:40] single fused kernel flash attention and that's what we are about to use so great example I think of being aware of um memory hierarchy the fact that flops don't matter uh the entire memory access pattern matters and that torch compile is amazing but there are many optimizations that are still available to us that potentially torch compile cannot find maybe maybe one day it could but right now it seems like a lot to ask so here's what we're going to do we're going to use Flash attention and the way to do that basically in pytorch is we [02:05:11] are going to comment out these four lines and we're going to replace them with a single line and here we are calling this compound operation in pytorch called scale that product attention and uh pytorch will call flash attention when you use it in this way I'm not actually 100% sure why torch compile doesn't realize that these four lines should just call flash attention in this exact way we have to do it again for it which in my opinion is a little bit odd but um here we are so you have [02:05:46] to use this compound up and uh let's wait for a few moments before torch comp compile gets around to it and then let's remember that we achieved 6.05 661 I have it here that's the loss we were expecting to see and we took 130 milliseconds uh before this change so we're expecting to see the exact same result by iteration 49 but we expect to see faster runtime because Flash attention is just a an algorithmic rewrite and it's a faster kernel but it [02:06:16] doesn't actually change any of the computation and we should have the exact same optimization so okay so we're a lot faster we're at about 95 milliseconds and we achiev 6.58 okay so they're basically identical up to a floating Point fudge Factor so it's the identical computation but it's significantly faster going from 130 to roughly 90 96 and so this is um 96 divide 130ish so this is maybe 27 is% [02:06:48] Improvement um so uh really interesting and that is Flash retention okay we are now getting to one of my favorite optimizations and it is simultaneously the dumbest and the most brilliant optimization and it's always a little bit surprising to me um anyway so basically I mentioned a few minutes ago that there are some numbers that are nice and some numbers that are ugly so 64 is a beautiful nice number 128 is even nicer 256 is beautiful what makes [02:07:20] these numbers beautiful is that there are many powers of two inside them you can divide by two many times and uh examples of ugly numbers are like 13 and 17 and something like that prime numbers numbers that are not even and so on and so pretty much you always want to use nice numbers in all of your code that deals with neural networks or Cuda because everything in Cuda Works in sort of like powers of two and lots of kernels are written in terms of powers of Two And there are lots of blocks of sizes 16 and uh 64 and so on so [02:07:50] everything is written in those terms and you always have special case handling for all kinds of uh logic that U when your inputs are not made of nice numbers so let's see what that looks like basically scan your code and look for ugly numbers is roughly theistic so three times is kind of ugly um I'm not 100% sure maybe this can be improved but this is uh this is ugly and not ideal um four times is nice so that's uh [02:08:20] that's nice 1024 is very nice that's a power of two 12 is a little bit suspicious um not too many powers of two 768 is great 50, 257 is a really really ugly number um it's first of all it's odd so uh and there's no not too many powers of two in there so this is a very ugly number and it's highly suspicious and then when we scroll down all these numbers are nice and then here we have mostly nice [02:08:50] numbers except for 25 so in this configuration of gpt2 XL a number of heads is 25 uh that's a really ugly number that's an odd number and um actually this did cause a lot of headaches for us recently when we're trying to optimize some kernels uh to run this fast um and required a bunch of special case handling so basically these numbers are we have some ugly numbers and some of them are easier to fix than others and in particular the voap size being 50257 that's a very ugly number very suspicious and we want to fix it [02:09:20] now when you when you fix these things uh one of the easy ways to do that is you basically um increase the number until it's the nearest power of two that you like so here's a much nicer number it's 50304 and why is that because 50304 can be divided by 8 or by 16 or by 32 64 it can even be divided by 128 I think yeah so it's a very nice number um so what we're going to do here is the GPT [02:09:51] config and you see that we initialized B cap size to 50257 Let's override just that um element to be 50304 okay so everything else stays the same we're just increasing our vocabulary size so we're adding it's almost like we're adding fake tokens uh so that book up size has powers of two inside it now actually what I'm doing here by the way is I'm increasing the amount of computation that our network will be doing if you just count the the [02:10:21] flops on like do the math of how many flops we're doing we're going to be doing more flops and we still have to think through whether this doesn't break anything but if I just run this uh let's see what we get uh currently this ran in maybe 96.5 milliseconds per step I'm just kind of like eyeballing it and let's see what kind of a result we're going to get uh while this is compiling let's think through whether our code actually [02:10:51] works okay when we increase the vocap size like this let's look at where vocap size is actually used so we swing up to the inet and we see that it's used inside the embedding table of course so all the way at the bottom of the Transformer and it's used at the classifier layer all the way at the top of the Transformer so in two places and let's take a look and we're running at 93 so 93 milliseconds instead of 96.5 so we are seeing a roughly yeah 4% Improvement here uh by doing more [02:11:22] calculations and the reason for this is we fixed we've made an ugly number into a nice number let's I'm going to come into the explanation for that a little bit again but for now let's just convince ourselves that we're not breaking anything when we do this so first of all we've made the the wte the embedding table for the tokens we've made it larger it's almost like we introduced more tokens at the bottom and these tokens are never used because the gbt tokenizer only has tokens up to $50,000 256 and so we'll never index into the [02:11:55] rows that we've added so we're wasting a little bit of space here by creating memory that's never going to be accessed never going to be used Etc now that's not fully correct because this wte weight ends up being shared and ends up being used in the classifier here at the end so what is that doing to the classifier right here well what what that's doing is we're predicting additional Dimensions at the classifier now and we're predicting probabilities for tokens that will of course never be present in the training set um and so [02:12:25] therefore the network has to learn that these probabilities uh have to be driven to zero and so the logits that the network produces have to drive those dimensions of the output to negative Infinity but it that's no different from all the other tokens that are already in our data set um or rather that are not in our data set so Shakespeare only probably uses let's say a th000 tokens out of 50,000 to 57 tokens so most of the tokens are already being driven to zero probability by the optimization we' just introduced a few more tokens now [02:12:55] that in a similar manner will never be used and have to be driven to zero in probability um so functionally though nothing breaks we're using a bit more extra um memory but otherwise this is a harmless operation as far as I can tell but and we're adding calculation but it's running faster and it's running faster because as I mentioned in Cuda so many kernels use uh block tiles and these block towels are usually nice numbers uh so powers of two so [02:13:25] calculations are done in like chunks of 64 or chunks of 32 and when your um when your desired calculation doesn't neatly fit into those block tiles um there are all kinds of boundary kernels that can kick in to like do the last part so basically in a lot of kernels they will chunk at up your input and they will do the nice part first and then they have a whole second second phase where they come back to any that like uh remains uh and then they process the remaining part [02:13:56] and the kernels for that could be very inefficient and so you're basically um spinning up all this extra compute and is extremely inefficient so you might as well pad your inputs and um make it fit nicely and usually that empiric lens up actually running faster um so this is another example of a 4% Improvement that we've added and this is something that also torch compile did not find for us you would hope that torch compile at some point could figure an optimization like this out uh but for now uh this is [02:14:27] it and I also have to point out that we're using pytorch nightly so that's why we're only seeing 4% if you're using pytorch 2.3.1 or earlier you would actually see something like 30% Improvement just from this change from changing it to from 50,000 to 57 to 50304 so again one of my favorite examples also of having to understand the under the hood and how it all works and to know what kinds of things to Tinker with to push the performance of your code okay so at this point we have improved the performance by about 11x [02:14:58] right because we started at about 1,000 milliseconds per step and we're now down to like 93 milliseconds so that's uh quite good and we're uh doing a much better job of utilizing our GPU resources so I'm going to now turn to more algorithmic changes uh and improvements to the actual optimization itself and what we would like to do is we would like to follow the hyper parameters that are mentioned in the GP G2 or gpt2 gpt3 paper now sadly gpt2 is uh doesn't actually say too much it's [02:15:28] very nice of them that they released the model weights and the code but the paper itself is extremely vague as to the optimization details uh the code itself that they released as well the code we've been looking at this is just the inference code so there's no training code here and very few hyp parameters so this doesn't also tell us too much so for that we have to turn to the gpt3 paper and um in the depending of the gpt3 paper um they have a lot more hyper parameters here for us to use and the gpt3 paper in general is a lot more [02:15:59] detailed as to uh all of the you know small details that go into the model training but gpt3 U models were never released so gbt2 we have the weights but no details and gpt3 we have lots of details but no weights so um but roughly speaking gpt2 and gpt3 architectures are very very similar and um basically there are very few changes the context length was expanded from 1024 to 2048 and that's kind of like the major change uh and some of the hyper parameters around [02:16:29] the Transformer have changed but otherwise they're pretty much the same model it's just that gpt3 was trained for a lot longer on a bigger data set and uh has a lot more thorough evaluations uh and the gpt3 model is 175 billion instead of 1.6 billion um in the gpt2 so long story short we're going to go to gp3 paper to follow along some the hyper parameters so to train all the versions of gpt3 we use atom with beta 1 beta 2 of9 and .95 so let's swing over [02:17:00] here and make sure that the betas parameter which you can see here defaults to 0.9 and 999 is actually set to 0.9 and .95 and then the Epsilon parameter uh you can see is the default is 1 in8 and this is also one in8 let's just uh put it in so that works expit uh now next up they say we clip the gra Global Norm of the gradient at 1.0 so what this is referring to is that [02:17:30] once we calculate the gradients right after l. backward um we basically have the gradients at all the parameter tensors and what people like to do is basically uh clip them to have some kind of a maximum Norm so in pytor this is fairly easy to do uh it's one line of code here that we have to insert right after we calcul Cal the gradients and what this utility function is doing is um it's calculating the global Norm of the parameters so every single par um [02:18:01] gradient on all the parameters you square it and you add it all up and you take a big square root of that and that's the norm of the parameter V Vector basically it's the it's the length of it if you if you'd like to look at it that way and we are basically making sure that its length is no more than 1.0 and we're going to clip it and the reason that people like to use this is that uh sometimes you can get unlucky during your optimization maybe it's a bad data batch or something like that and if you get very unlucky in the [02:18:31] batch you might get really high loss and really high loss could lead to a really high gradient and this could basically uh shock your model and shock the optimization so people like to use a gradient Norm clipping uh to prevent the model from um basically getting too big of shocks in terms of the gradient magnet ude and uh the upper bound it in this way it's a bit of a hacky solution it's about like a patch on top of like deeper issues uh but uh people still do it fairly frequently now the clip grad [02:19:03] Norm Returns the norm of the gradient which I like to always visualize uh because um it is useful information and sometimes you can look at the norm of the gradient and if it's well behaved things are good if it's climbing things are bad and they're destabilizing during training sometimes you could get a spike in the norm and that means there's some kind of an issue or an instability so the norm here will be a norm uh and let's do a uh 4f or [02:19:33] something like that and I believe this is just a float and so we should be able to uh print that uh so that's Global gradient clipping now they go into the details of the learning rate uh scheduler so they don't just use a fixed learning rate like we do here for 3 E4 but there's actually basically a cosine DK learning rate schedule um it's got a warm-up and it's got a cosine DEC to 10% over some [02:20:04] Horizon um and so we're going to implement uh this in a second I just like to see Norm printed here okay there we go so what happened here is the norm is actually really high in the beginning 30 or so and you see that as we continue training it kind of like stabilizes um at values below one um and this is not that crazy uncommon for the norm to be high in the very first few stages basically What's Happening Here is the model is completely random and so [02:20:35] there's a ton of learning happening very early in the network but that learning is kind of like um you know it's mostly learning the biases of the output tokens and so it's a bit of an unstable time uh but the network usually stabilizes in a very few iterations so this looks very relatively reasonable to me except usually I would expect this looks a little bit funky that we go from 28 to 6 to 2 and then to 10 um it's not completely insane but it's just kind of a little bit funky um okay so let's now get to the [02:21:05] learning rate schuer so the learning rate schedule that's used here in gpt3 is what's called a cosine Decay learning schedule with warmup and the way this looks is that the learning rate is basically starts right at around zero linearly rank s up over some amount of time and then comes down with this cosine sort of form and comes down to some kind of a minimum learning rate that's up to you so here the minimum learning rate is zero but uh here in the paper they said that they use cosine [02:21:35] Decay for learning rate down to 10% of its value over the first 260 billion tokens and then training continues 10% after and there's a linear warmup over the first 375 million tokens so that's about the learn R so let's now implement this uh so I already implemented it here and the way this works is let me scroll down first here I changed our training Loop a little bit so this was a 4i in Max steps I just change it to step now so that we have the notion of a step is [02:22:07] a single optimization step in the in the for Loop and then here I get the LR for this step of the optimization using a new function I call get LR and then in pytorch to set the learning rate I think this is is the way to set the learning rate it's a little bit gnarly um because you have to basically there's a notion of different par parameter groups that could exist in the optimizer and so you actually have to iterate over them even though we currently have a single param group only um and you have to set the LR in this for Loop kind of style is is my [02:22:37] impression right now so we have this look of LR we set the learning rate and then on the bottom I'm also printing it uh so that's all the changes I made to this Loop and then of course the get LR is my scheduler now it's worth pointing out that pytorch actually has learning rate schedulers and you can use them and I believe there's a cosine learning rate schedule in pytorch I just don't really love using that code because honestly it's like five lines of code and I fully understand what's happening inside these [02:23:07] lines so I don't love to use abstractions where they're kind of in screwable and then I don't know what they're doing so personal style so the max learning rate here is let's say 3 E4 but we're going to see that in gpt3 here they have a table of what the maximum learning rate is for every model size so um for for this one basically 12 12 layer 768 gpt3 so the gpt3 small is [02:23:37] roughly like a GPT 2124m we see that here they use a learning rate of 6 E4 so we could actually go higher um in fact we may want to try to follow that and just set the max LR here at six uh then the that's the maximum learning rate the minum learning rate is uh 10% of that per description in the paper some number of steps that we're going to warm up over and then the maximum steps of the optimization which I now use also in the for Loop down here and then you [02:24:07] can go over this code if you like it's not U it's not terribly inside Flor interesting I'm just uh modulating based on the iteration number which learning rate uh there should be so this is the warm-up region um this is the region after the optimization and then this is the region sort of in between and this is where I calculate the cosine learning rate schedule and you can step through this in detail if you'd like uh but this is basically implementing this curve and I ran this already and this is [02:24:38] what that looks like um so when we now run we start at um some very low number now note that we don't start exactly at zero because that would be not useful to update with a learning rate of zero that's why there's an it+ one so that on the zeroth iteration we are not using exactly zero we're using something very very low then we linearly warm up to maximum learning rate which in this case was 34 when I ran it but now would be 6 E4 and then it starts to decay all the way down to um 3 [02:25:11] E5 which was at the time 10% of the original learning rate now one thing we are not following exactly is that they mentioned that um let me see if I can find it again we're not exactly following what they did because uh they mentioned that their training Horizon is 300 billion tokens and they come down to 10% of the initial learning rate of at 260 billion and then they train after 260 with 10% so [02:25:41] basically their Decay time is less than the max steps time whereas for us they're exactly equal so it's not exactly faithful but it's um it's an okay um this is okay for us and for our purposes right now and um we're just going to use this ourselves I don't think it makes too too big of a difference honestly I should point out that what learning rate schedule you use is totally up to you there's many different types um coign learning rate has been popularized a lot by gpt2 and gpt3 but people have come up with all [02:26:12] kinds of uh other learning rate schedules um and this is kind of like an active area of uh research as to which one is the most effective at train these networks okay next up the paper talks about the gradual batch size increase so there's a ramp on the batch size that is linear and you start with very small batch size and you ramp up to a big batch size over time uh we're going to actually skip this and we're not going to work with it and the reason I don't love to use it is that it complicates a lot of the arithmetic because you are [02:26:42] changing the number of tokens that you're processing at every single step of the optimization and I like to keep that math very very simple also my understanding is that that this is not like a major um Improvement and also my understanding is that this is not like an algorithmic optimization Improvement it's more of a systems and speed Improvement and roughly speaking this is because uh in the early stages of the optimization uh again the model is in a very atypical setting and mostly what you're learning is that um you're mostly [02:27:13] learning to ignore the tokens uh that don't come up in your training set very often you're learning very simple biases and and that kind of a thing and so every single example that you put through your network is basically just telling you use these tokens and don't use these tokens and so the gradients from every single example are actually extremely highly correlated they all look roughly the same in the in the OR original parts of the optimization because they're all just telling you that these tokens don't appear and these tokens do appear and so because the [02:27:43] gradients are all very similar and they're highly correlated then why are you doing batch sizes of like Millions when if you do a batch size of 32k you're basically getting the exact same gradient early on in the training and then later in the optimization once you've learned all the simple stuff that's where the actual work starts and that's where the gradients become more decorrelated per examples and that's where they actually offer you sort of statistical power in some sense um so we're going to skip this just because it kind of complicates things and we're [02:28:14] going to go to uh data are sampled without replacement during training um so until an Epoch boundary is reached so without replacement means that they're not sampling from some fixed pool and then uh take a sequence train on it but then also like return the sequence to the pool they are exhausting a pool so when they draw a sequence it's it's gone until the next Epoch of training uh so we're already doing that because our data loader um iterates over chunks of [02:28:44] data so there's no replacement they don't become eligible to be drawn again until the next P so we're basically already doing that um all models use a weight decay of 0.1 to provide a small amount of regularization so let's Implement a weight Decay and you see here that I've already kind of made the changes and in particular instead of creating the optimizer right here um I I'm creating a new configure optimizers function inside the model and I'm passing in some of the [02:29:14] hyper parameters instead so let's look at the configure optimizers which is supposed to return the optimizer object okay so it looks complicated but it's actually really simple and it's just um we're just being very careful and there's a few settings here to go through the most important thing with respect to this line is that you see there's a weight Decay parameter here and I'm passing that into um well I'm passing that into [02:29:44] something called optim groups that eventually ends up going into the addom W Optimizer um and the weight Decay that's by default used in Addam W here is 0.01 so it's it's u 10 times lower than what's used in gpt3 paper here um so the weight dek basically ends up making its way into the ADD and W through the optimizer groups now what else is going on here in this uh function so the two things that are happening here that are important is that I'm splitting up the parameters into those that should be weight decayed [02:30:14] and those that should not be weight decayed so in particular it is common to not weight decay uh biases and any other sort of one-dimensional tensors so the one-dimensional tensors are in the no Decay prams and these are also things like uh layer Norm scales and biases it doesn't really make sense to weight Decay those you mostly want to weight Decay uh the weights that participate in Matrix multiplications and you want to potentially weight Decay the embeddings and uh We've covered in [02:30:46] previous video why it makes sense to Decay the weights because you can sort of the it as a regularization because when you're pulling down all the weights you're forcing the optimization to use more of the weights um and you're not allowing any one of the weights individually to be way too large um you're forcing you're forcing the network to kind of like distribute the work across more channels because there's sort of like a pull of gravity on the weights themselves um so that's why we are separating it in those ways here we're [02:31:16] only decaying the embeddings and the mmal participating ways uh we're printing the number of uh parameters that we decaying and not most of the parameters will be decayed and then one more thing that we're doing here is I'm doing another optimization here and previous add and W did not have this option but later parts of pytorch introduced it and that's why I'm guarding it with an inspect do signature which is basically checking if this fused um quar is present inside atom W [02:31:48] and then if it is present I'm going to end up using it and passing it in here because some earlier versions do not have fused equals so here's adamw fused equals it did not used to exist and it was added later and there's some docks here for what's happening and basically they say that by default they do not use fused because it is relatively new and we want to give it sufficient big time so by default they don't use fused but fused is a lot faster when it is available and when you're running on Cuda and what that does is in instead of [02:32:20] iterating in a for Loop over all the parameter tensors and updating them that would launch a lot of kernels right and so a fused just means that it's a um all those kernels are fused into a single kernel you get rid of a lot of overhead and you a single time on all the parameters call a uh kernel that updates them and so it's just basically a kernel Fusion for the atom W update instead of iterating over all the tensors so that's the configure optimizers function that I like to use [02:32:51] and we can rerun and we're not going to see any major differences from what we saw before but we are going to see some prints uh coming from here so let's just take a look at what they look like so we see that number of Decay tensors is 50 and it's most of the parameters and number of non- deay tensors is 98 and these are the biases and the layer Norm parameters mostly and that's there's only 100,000 of those so most of it is decayed and then we are using the fused implementation of ATM W which will be a lot faster so if you [02:33:22] have it available I would advise you to use it I'm not actually 100% sure why they don't default to it it seems fairly benign and harmless and also because we are using the fused implementation I think this is why we have dropped um notice that the running time used to be 93 milliseconds per step and we're now down to 90 milliseconds per step because of using the fused atom W Optimizer so in a single commit here we are introducing fused atom getting improvements on the time and we're adding or changing the [02:33:54] weight Decay but we're only weight decaying the two dimensional parameters the embeddings and the matrices that participate in linear so that is this and we can take this out and uh yeah that is it for this line one more quick note before we continue here I just want to point out that the relationship between weight Decay learning rate batch size the atom parameters beta 1 beta 2 the Epsilon and so on these are very complicated uh mathematical relationships in the optimization [02:34:24] literature and um for the most part I'm in this video I'm just trying to copy paste the settings that open AI used but this is a complicated topic uh quite deep and um yeah in this video I just want to copy the parameters because it's a whole different video to really talk about that in detail and give it a proper Justice instead of just high level intuitions uh now the next thing that I want to move on to is that uh this paragraph here by the way we're going to turn back around to when we improve our data loader for now I want to swing back [02:34:54] around to this table where you will notice that um for different models we of course have different U hyper parameters for the Transformer that dictate the size of the Transformer Network we also have a different learning rate so we're seeing the pattern that the bigger networks are trained with slightly lower learning rates and we also see this batch size where in in the small networks they use a smaller batch size and in the bigger networks they use a bigger batch size [02:35:26] now the problem with for us is we can't just use 0.5 million batch size because uh if I just try to come in here and I try to set uh this uh B where is my b um b equals where where do I call the DAT okay b equal 16 if I try to set um well well we have to be careful it's not 0.5 million because this is the badge size in the number of tokens every [02:35:56] single one of our rows is24 tokens so 0.5 E6 1 million divide 1024 this would need about a 488 match size so the problem is I can't come in here and set this to 488 uh because my GPU would explode um this would not fit for sure and so but we still want to use this batch size because again as I mentioned the batch size is correlated with all the other optimization hyper parameters and the [02:36:26] learning rates and so on so we want to have a faithful representation of all the hyper parameters and therefore we need to uh use a bat size of .5 million roughly but the question is how do we use .5 million if we only have a small GPU well for that we need to use what's called gradient accumulation uh so we're going to turn to that next and it allows us to simulate in a Serial way any arbitrary batch size that we set and so we can do a batch size of .5 million we just have to run longer and we have to [02:36:56] process multiple sequences and basically add up all the gradients from them to simulate a batch size of .5 million so let's turn to that next okay so I started the implementation right here just by adding these lines of code and basically what I did is first I set the total batch size that we desire so this is exactly .5 million and I used a nice number a power of two uh because 2 to the 19 is 524 288 so it's roughly .5 million it's a nice number now our micro [02:37:26] batch size as we call it now is 16 so this is going to be we still have B BYT in the SE that go into the Transformer and do forward backward but we're not going to do an update right we're going to do many forward backwards we're going to and those gradients are all going to plus equals on the parameter gradients they're all going to add up so we're going to do forward backward grad akum steps number of times and then we're going to do a single update once all that is accumulated so in particular our micro batch size is just now controlling how [02:37:58] many tokens how many rows we're processing in a single go over a forward backward so um here we are doing 16 * 124 we're doing 16 384 um tokens per forward backward and we are supposed to be doing 2 to the 19 whoops what am I doing 2 to the 19 in total so the grat Aon will be 32 uh so therefore gr AUM here will work [02:38:28] out to 32 and we have to do 32 forward backward um and then a single update now we see that we have about 100 milliseconds for a singer forward backward so doing 32 of them will be will make every step roughly 3 seconds just napkin math so that's grum steps but now we actually have to Implement that so we're going to swing over to our training Loop because now this part here and this part here the forward and [02:38:59] the backward we have to now repeat this 32 times before we do everything else that follows so let's uh see how we can Implement that so let's come over here and actually we do have to load a new batch every single time so let me move that over here and now this is where we have the inner loop so for micro step in range graum steps we do this and remember that l. backward always deposits gradients so we're doing inside losta backward there's always a plus equals on the [02:39:29] gradients so in every single L of backward gradients will add up on the gradient tensors um so we lost that backward and then we get all the gradients over there and then we normalize and everything else should just follow um so we're very close but actually there's like subtle and deep issue here and this is actually incorrect so invite I invite you to think about why this is not yet sufficient um and uh let me fix it then [02:39:59] okay so I brought back the jupyter notebook so we can think about this carefully in a simple toy setting and see what's happening so let's create a very simple neural nut that takes a 16 Vector of 16 numbers and returns a single number and then here I'm creating some random uh examples X and some targets uh y Y and then we are using the mean squared loss uh here to calculate the loss so basically what this is is four individual examples and we're just doing [02:40:30] Simple regression with the mean squared loss over those four examples now when we calculate the loss and we lost that backward and look at the gradient this is the gradient that we achieve now the loss objective here notice that in MSE loss the default for the loss function is reduction is mean so we're we're calculating the average mean loss um the the mean loss here over the four examples so this is the exact loss objective and this is the average [02:41:02] the one over four because there are four independent examples here and then we have the four examples and their mean squared error the squared error and then this makes it the mean squared error so therefore uh we are we calculate the squared error and then we normalize it to make it the mean over the examples and there's four examples here so now when we come to the gradient accumulation version of it this uh this here is the gradient accumulation version of it where we have grad acum [02:41:32] steps of four and I reset the gradient we've grum steps of four and now I'm evaluating all the examples individually instead and calling L that backward on them many times and then we're looking at the gradient that we achieve from that so basically now we forward our function calculate the exact same loss do a backward and we do that four times and when we look at the gradient uh you'll notice that the gradients don't match so here we uh did a single batch of four and here we did uh four gradient [02:42:03] accumulation steps of batch size one and the gradients are not the same and basically the the reason that they're not the same is exactly because this mean squared error gets lost this one quarter in this loss gets lost because what happens here is the loss of objective for every one of the loops is just a mean squ error um which in this case because there's only a single example is just this term here so that was the loss in the zeroth eration same in the first third and so on and then [02:42:33] when you do the loss. backward we're accumulating gradients and what happens is that accumulation in the gradient is basically equivalent to doing a sum in the loss so our loss actually here is this without the factor of one quarter outside of it so we're missing the normalizer and therefore our gradients are off and so the way to fix this or one of them is basically we can actually come here and we can say loss equals loss divide [02:43:04] 4 and what happens now is that we're introducing we're we're scaling our loss we're introducing a one quarter in front of all of these places so all the individual losses are now scaled by one quarter and and then when we backward all of these accumulate with a sum but now there's a one quarter inside every one of these components and now our losses will be equivalent so when I run this you see that the U gradients are now identical [02:43:35] so long story short with this simple example uh when you step through it you can see that basically the reason that this is not correct is because in the same way as here in the MSE loss the loss that we're calculating here in the model is using a reduction of mean as well uh so where's the loss after that cross entropy and by default the reduction uh here in Cross entropy is also I don't know why they don't show it but it's the [02:44:05] mean uh the mean uh loss at all the B BYT elements right so there's a reduction by mean in there and if we're just doing this gradient accumulation here we're missing that and so the way to fix this is to simply compensate for the number of gradient accumulation steps and we can in the same way divide this loss so in particular here the number of steps that we're doing is loss equals loss divide gradient accumulation steps so even uh co-pilot s gets the modification but in [02:44:36] the same way exactly we are scaling down the loss so that when we do loss that backward which basically corresponds to a sum in the objective we are summing up the already normalized um loss and and therefore when we sum up the losses divided by grum steps we are recovering the additional normalizer uh and so now these two will be now this will be equivalent to the original uh sort of optimization because the gradient will come out the same okay so I had to do a few more touch-ups and I launched [02:45:07] launched the optimization here so in particular one thing we want to do because we want to print things nicely is well first of all we need to create like an accumulator over the loss we can't just print the loss because we'd be printing only the final loss at the final micro step so instead we have loss ofon which I initialize at zero and then I accumulate a uh the loss into it and I'm using detach so that um uh I'm detaching the tensor uh from the graph and I'm just trying to keep track of the values so I'm making these Leaf nodes [02:45:38] when I add them so that's lakum and then we're printing that here instead of loss and then in addition to that I had to account for the grum steps inside the tokens processed because now the tokens processed per step is B * T * gradient accumulation so long story short here we have the optimization it looks uh reasonable right we're starting at a good spot we calculated the grum steps to be 32 and uh we're getting about 3 seconds here [02:46:08] right um and so this looks pretty good now if you'd like to verify that uh your optimization and the implementation here is correct and your working on a side well now because we have the total patch size and the gradient accumulation steps our setting of B is purely a performance optimization kind of setting so if you have a big GPU you can actually increase this to 32 and you'll probably go a bit faster if you have a very small GPU you can try eight or four but in any case you should be getting the exact same [02:46:38] optimization and the same answers up to like a floating Point error because the gradient accumulation kicks in and um and can um handle everything serially as an Neary so uh that's it for gradient accumulation I think okay so now is the time to bring out the heavy weapons uh you've noticed that so far we've only been using a single GPU for training but actually I am paying for eight gpus here and so uh we should be putting all of them to work and in particular they are going to collaborate and uh you know [02:47:09] optimize over tokens at the same time and communicate so that um uh they're all kind of collaborating on the optimization for this we are going to be using the distributed data parallel from pytorch there's also a legacy data parallel which I recommend you not use and that's kind of like you know Legacy distributed data parallel Works in a very simple way we have eight gpus so we're going to uh launch eight processes and each process is going to be assigned to GPU and for each process the training [02:47:40] Loop and everything we've worked on so far is going to look pretty much the same H GPU as far as it's concerned is just working on exactly what we've built so far but now Secret L there's eight of them and they're all going to be processing slightly different parts of the data and we're going to add one more part where once they all calculate their gradients there's one more part where we do a average of those gradients and so that's how they're going to be collaborating on uh the computational workload here so to use [02:48:10] all eight of them we're not going to be launching our script anymore with just um pytorch train gbt2 piy we're going to be running it with a special command called torrun in pytorch we'll see that in a bit and torrun uh when it runs our python script we'll actually make sure to run eight eight of them in parallel and it creates these environmental variables where each of these processes can look up which uh basically which one of the processes it [02:48:40] is so for example torron will set rank local Rank and World size environmental variables and so this is a bad way to detect whether uh DDP is running so if we're using torch run if DDP is running then uh we have to make sure that K is available because I don't know that you can run this on CPU anymore or that that makes sense to do um this is some um setup code here the important part is that there's a world size which [02:49:10] for us will be eight that's the total number of processes running there's a rank which is um each process will basically run the ex exact same code at the exact same time roughly but all the process the only difference between these processes is that they all have a different dtp rank so the um gpu0 will have DDP rank of zero GPU 1 will have uh rank of one Etc so otherwise they're all running the exact same script it's just that DDP rank will be a slightly [02:49:40] different integer and that is the way for us to coordinate that they don't for example run on the same data we want to we want them to run on different parts of the data and so on now local rank is something that is only used in a multi- node setting we only have a single node with ag gpus and so local rank is the rank of the GPU on a single node so from 0 to seven as an example but for us we're mostly going to be running on a single box so the things we care about are Rank and World size [02:50:10] this is eight and this will be whatever it is depending on the GPU uh that uh that this particular instantiation of the script runs on now here we make sure that according to the local rank we are setting the device to be Cuda colon and colon indicates which GPU to use if there are more than one gpus so depending on the local rank of this process it's going to use just the appropriate GPU so there's no [02:50:40] collisions on which GPU is being used by which process and finally there's a Boolean variable that I like to create which is the DDP rank equ equal Z so the master process is arbitrarily process number zero and it does a lot of the printing logging checkpointing Etc and the other processes are thought of mostly as a compute processes that are assisting and so Master process zero will have some additional work to do all the other processes will uh will mostly just be doing forward backwards and if we're not using DDP and [02:51:10] none of these variables are set we revert back to single GPU training so that means that we only have rank zero the world size is just one uh and and we are the master process and we try to autodetect the device and this is world as normal so so far all we've done is we've initialized DDP and uh in the case where we're running with torrun which we'll see in a bit there's going to be eight copies running in parallel each one of them will have a different Rank and now we have to make sure that everything [02:51:41] happens uh correctly afterwards so the tricky thing with running multiple processes is you always have to imagine that there's going to be eight processes running in parallel so as you read the code now you have to imagine there's eight you know eight python interpreters running down these lines of code and the only difference between them is that they have a different DDP rank so they all come here they all pick the exact same seed they all make all of these calculations completely unaware of the other copies running roughly speaking [02:52:12] right so they all make the exact same calculations and now we have to adjust these calculations to take into account that there's actually like a certain world size and certain ranks so in particular these micro batches and sequence lengths these are all just per GPU right so now there's going to be num processes of them running in parallel so we have to adjust this right because the grum steps now is going to be total B size divide B * T time U DDP R [02:52:43] size because each um process will will do B * T and there's this many of them and so in addition to that we we want to make sure that this fits nicely into total batch size which for us it will because 16 * 124 * 8 8 gpus is 131 uh K and so 524288 this means that our gratum will be four with the current settings right [02:53:13] so there's going to be 16 * 124 process on each GPU and then there's a GP pus so we're going to be doing 131,000 tokens in a single forward backward on the 8 gpus so we want to make sure that this fits nicely so that we can derive a nice gradient accumulation steps and uh yeah let's just adjust the comments here times uh DDP World size okay so each GPU calculates this now [02:53:45] this is where we start to get run into issues right so we are each process is going to come by a print and they're all going to print so we're going to have eight copies of these prints so one way to deal with this is exactly this master process variable that we have so if Master process then guard this and that's just so that we just print this a single time because otherwise all the processes would have computed the exact same variables and there's no need to print this eight times um before getting into the data loader and we're going to have to [02:54:15] refactor it obviously maybe at this point is uh we should do some prints and uh just take it out for a spin and exit at this point so import sis and S start exit and print IM GPU um DDP rank IM GPU DDP Rank and that um print [02:54:46] by so uh so now let's try to run this and just see how this works so let's take it for a spin just so we see what it looks like so normally we use to launch python train gpd2 P like this now we're going to run with torch run and this is what it looks like so torch run Standalone number of processes for example is eight for us because we have eight gpus uh and then change of2 Pi so this is what the command would look like and torch run again we'll run eight of these so let's just see what happens so [02:55:16] first it gets a little busy so there's a lot going on here so first of all there's some warnings from distributed and I don't actually know that these mean anything I think this is just like the code is setting up and the processes are coming online and we're seeing some preliminary failure to collect while the processes come up I'm not 100% sure about that but we start to then get into actual prints so all the processes went down and then the first print actually comes from [02:55:46] process 5 uh just by chance and then it printed so process 5 basically got here first it said I'm process on GPU 5 buy and then this these prints come from the master process so process 5 just finished first for whatever reason it just depends on how the operating system scheduled the processes to run uh then gpu0 ended then GPU 3 and two and then uh probably process 5 or something like that has uh [02:56:17] exited and and DDP really doesn't like that because we didn't properly dispose of uh the multi-gpus um setting and so process group has not been destroyed before we destruct uh so it really doesn't like that and in an actual application we would want to call destroy process group uh so that we clean up DDP properly and so it doesn't like that too much and then the rest of the gpus finish and that's it so basically we can't guarantee when these processes are running it's totally [02:56:48] but they are running in parallel we don't want them to be printing um and next up let's erase this next up we want to make sure that when we create data loader light we need to now make it aware of this multi-process um setting because we don't want all the processes to be loading the exact same data we want every process to get its own chunk of data so that they're all working on different parts of the data set of course so let's adjust that so one particular particularly simple and a [02:57:19] naive way to do this is we have to make sure that we pass in the rank and the size to the data loader and then when we come up here we see that we now take Rank and processes and we save them now the current position will not be zero uh because what we want is we want to stride out all the processes so one way to do this is we basically take S.B times salt. T and then multiply it by the process rank so proc process rank 0 will start [02:57:49] at zero but process rank one now starts at B * T process rank two is starts at 2 * B * D Etc so that is the initialization now we still they still do this identically but now when we advance we don't Advance by B * T we advance by B * T times number of processes right so basically um the total number of tokens that we're um consuming is B * T * number processes [02:58:19] and they all go off to a different Rank and the position has to advance by the entire chunk and then here B * T time uh s. num processes + one would be to exceed number of tokens then we're going to Loop and when we Loop we want to of course Loop in the exact same way so we sort of like reset back uh so this is the simplest change that I can uh find for kind of a very simple distributed data Lo light and um you can notice that [02:58:50] if process rank is zero and non processes is one then uh the whole thing will be identical to what we had before but now we can have actually multiple processes uh running and this should work fine um so that's the data loader okay so next up once they've all initialized the data loader they come here and they all create a GPT model uh so we create eight GPT models on eight processes but because the seeds are fixed here they all create the same identical model they [02:59:20] all move it to the device of their Rank and they all compile the model and because the models are identical there are eight identical compilations happening in parallel but that's okay now none of this uh changes because that is on a per step basis and we're currently working kind of within step because we need to um just uh all the all the changes we're making are kind of like a within step changes now the important thing here is when we construct the M model we actually have a bit of work to to do here get loits is deprecated so uh [02:59:50] create model we need to actually wrap the model into the distributed data parallel container so um this is how we wrap the model into the DDP container and these are the docs for DDP and they're quite extensive and there's a lot of caveats and a lot of things to be careful with because everything complexifies times 10 when multiple processes are involved but roughly speaking this device IDs I believe has to be passed in now unfortunately the docs for what device [03:00:20] IDs is is is extremely unclear uh so when you actually like come here this comment for what device IDs is is roughly nonsensical um but I'm pretty sure it's supposed to be the DDP local rank so not the DDP rank the local rank uh so this is what you pass in here this wraps the model and in particular what DDP does for you is in a forward pass it actually behaves identically so um my understanding of it is nothing should be changed in the forward pass but in the [03:00:51] backward pass as you are doing the backward pass um in the simpl setting once the backp passes over on each independent GPU each independent GPU has the gradient for all the parameters and what DDP does for you is once the backward pass is over it will call what's called all reduce and it basically does an average across all the uh ranks of their gradients and and then it will deposit that average on every single rank so every sing Single rank [03:01:22] will end up with the average on it and so basically that's the communication it just synchronizes and averages the gradients and that's what DDP offers you now DDP actually is a little bit more um it is a little bit more involved than that because as you are doing the backward pass through the layers of the Transformer it actually can dispatch Communications for the gradient while the backward pass is still happening so there's overlap of the uh communication of the gradient and the synchronization of them and uh the backward pass and uh [03:01:52] this is just more efficient and um uh to do it that way so that's what DDP does for you um forward is unchanged and backward is mostly unchanged and we're tacking on this average as we'll see in a bit okay so now let's go to the uh optimization nothing here changes let's go to the optimization here the inner loop and think through the synchronization of uh these gradients in the DP so basically by default what happens as I mentioned is when you do l. backward here it will do the backward [03:02:22] pass and then it will synchronize the gradients um the problem here is because of the gradient accumulation steps Loop here we don't actually want to do the synchronization after every single La step backward because we are just depositing gradients and we're doing that serially and we just want them adding up and we don't want to synchronize every single time that would be extremely wasteful so basically we want to add them up and then on the the very last uh it's only on the very last step when micro when micro step becomes [03:02:53] gratak steps minus one only at that last step do we want to actually do the alberu uh to average up the gradients so to do that we come here and um the official sanctioned way by the way is to do this no sync context manager so pytorch says this is a context manager to disable gradient synchronization across DDP processes So within this context gradient will be accumulated and basically when you do no sync there will be no communication so [03:03:24] they are telling us to do with DDP no sync uh do the gradient accumulation accumulate grats and then they are asking us to do DDP again with another input and that backward and I just really don't love this I I just really don't like it uh the fact that you have to copy paste your code here and use a context manager and this is just super ugly so when I went to this source code here you can see that when you enter you simply toggle this variable this require backward grat sync and this is [03:03:54] uh being toggled around and changed and this is the variable that basically uh if you step through it is being toggled to determine if the gradient is going to be synchronized so I actually just kind of like to use that directly uh so instead what I like to do is the following right here before the L back backward if we are using the DDP then um then basically we only want to synchronize we only want this variable [03:04:25] to be true when it is the final iteration in all the other iterations inside the micr steps we want to be false so I just toggle it like this so required backward graph sync should only turn on when the micro step is the last step and so I'm toggling this variable directly and I hope that that impacts last St backwards and this is a naughty thing to do because you know they could probably change the DDP and this variable will go away but for now I believe this this [03:04:55] works and it allows me to avoid the use of context managers and code duplication I'm just toggling the variable and then Lop backward will not synchronize most of the steps and it will synchronize the very last step and so once this is over uh and we come out every single um rank will suddenly magically have the average of all the gradients that were stored on all the ranks so now we have to think through whether that is what we want and also um if this suffices and whether how [03:05:29] it works with the loss and what is loss AUM so let's think through through that now and the problem I'm getting at is that we've averaged the gradients which is great but the loss AUM has not been impacted yet and the and this is outside of the DDP container so that is not being averaged um and so here when when we are printing Los AUM well presumably we're only going to be printing on the master process uh rank zero and it's just going to be printing the losses that it saw on its process but instead we want it to print the loss over all [03:06:00] the processes and the average of that loss because we did average of gradients so we want the average of loss as well so simply here after this uh this is the code that I've used in the past um and instead of LF we want Lum so if DDP again then this is a p torch distributed I import it where do I import it uh oh gosh so this file is starting [03:06:30] to get out of control huh so if uh so import torch. distributed as dist so dist. ALU and we're doing the average on Lum and so this lakum tensor exists on all the ranks when we call all use of average it creates the average of those numbers and it deposits that average on all the ranks so all the ranks after this um call will now contain L AUM uh averaged up and so when we print here on [03:07:00] the master process the L AUM is identical in all the other ranks as well so here if Master process oops we want to print like this okay and finally we have to be careful because we're not processing even more tokens so times DDP World size that's number of tokens that we've processed up above and everything else should be fine uh the only other thing to be careful with is as I mentioned you want to destroy [03:07:31] the process group so that we are nice to nickel and it's not going to uh to uh to DDP and it's not going to complain to us uh when we exit here so that should be it let's try to take it for a spin okay so I launched the script and it should be uh printing here imminently we're now training with 8 gpus at the same time so the gradient accumulation steps is not 32 it is now divide 8 and it's just four uh so um otherwise this is what the optimization now looks like and wow we're going [03:08:01] really fast so we're processing 1.5 million tokens uh per second now so these are some serious numbers and the tiny shakespare data set is so tiny that we're just doing like so many Epoch over it most likely but this is roughly what looks like um one thing that I had to fix by the way is that this was model. configure optimizers which Now doesn't work because model now is a DDP model so instead this has to become raw model. configure optimizers where raw [03:08:32] model is something I create here so right after I wrap the model into DDP uh I have to create the raw model which in the case of DDP is a model. module is where it stores the raw and then module of gpt2 as we have it which contains the uh configure optimizers function that we want to call so that's one thing that I have to fix otherwise this seems to run now one thing you'll notice is that when you actually compare this run and the numbers in it to the just running a single GPU you'll notice that this is [03:09:04] single GPU run with 32 gratum the numbers won't exactly match up and uh that's kind of a boring reason for why that happens uh the reason for that is that in the data loader we're basically just iterating through batches and slightly different way because now we're looking for an entire page of data and if that page uh for all the gpus if that chunk exceeds the number of tokens we just Loop and so actually the single GPU and the H GPU process will end up um resetting in a slightly different Manner [03:09:35] and so our batches are slightly different and so we get slightly different numbers but one way to convince yourself that this is okay it just make the total batch size much smaller and the b and a t and then um so I think I used uh 4 * 124 * 8 so I used 32768 as a total patch size and then um so I made sure that the single GPU will do eight creting accumulation steps and then the multi-gpu and then you're reducing the boundary effects of the data loader and you'll see that the [03:10:06] numbers match up so long story short we're now going really really fast the optimization is mostly consistent with gpt2 and three hyper parameters and uh we have outgrown our tiny Shakespeare file and we want to upgrade it so let's move to next to that next so let's now take a look at what data sets were used by gpt2 and gpt3 so gbt2 used this web Text data set that was never released um there's an attempt at reproducing it called open web text uh so basically roughly speaking what they say here in the paper is that they scraped all [03:10:37] outbound links from Reddit and then uh with at least three Karma and that was kind of like their starting point and they collected all the web P all the web pages and all the text in them and so this was 45 million links and this ended up being 40 GB of text so uh so that's roughly what gpt2 says about its data set so it's basically outbound links from Reddit now when we go over to gpt3 there's a training data set section and that's where they start to talk about um common coll which is a lot more uh used [03:11:09] actually I think even gpt2 talked about common coll um but basically it's not a very high quality data set all by itself because it is extremely noisy this is a completely random subset of the internet and it's much worse than you think so people go into Great Lengths to filter common craw because there's good stuff in it but most of it is just like ad spam random tables and numbers and stock tickers and uh it's just total mess so that's why people like to train on these data mixtures that they curate and [03:11:41] uh are careful with so a large chunk of these data mixtures typically will be common C like for example 50% of the tokens will be comic but then here in gpt3 they're also using web text to from before so that's Reddit outbound but they're also adding for example books and they're adding Wikipedia there's many other things you can decide to add now this data set for gpt3 was also never released so today some of the data sets that I'm familiar with that are quite good and would be representative of something along these lines are number one the red pajama data set or [03:12:12] more specifically for example the slim pajama subset of the red pajama data set which is a cleaned and D duplicated version of it and just to give you a sense again it's a bunch of common crawl um C4 which is also as far as I know more common craw but processed differently and then we have GitHub books archive Wikipedia stack exchange these are the kinds of data sets that would go into these data mixtures now specifically the one that I like that came out recently is called Fine web data set uh so this is an attempt to [03:12:43] basically collect really high quality common coll data and filter it in this case to 15 trillion tokens and then in addition to that more recently huggingface released this fine web edu subset which is 1.3 trillion of educational and 5.4 trillion of high educational content so basically they're trying to filter common C to very high quality educational subsets and uh this is the one that we will use there's a long uh web page here on fine web and [03:13:14] they go into a ton of detail about how they process the data which is really fascinating reading by the way and I would definitely recommend if you're interested into Data mixtures and so on and how data gets processed at these scales a look at this uh page and more specifically we'll be working with the fine web edu I think and it's basically educational content from the internet uh they show that training on educational content in in their metrics um uh works really really well and we're going to use this sample 10 billion [03:13:46] tokens subsample of it because we're not going to be training on trillions of tokens uh we're just going to train on uh 10 billion sample of the fine web edu because empirically in my previous few experiments this actually suffices to really get close to gpt2 Performance and it's um simple enough to work with and so let's work with the sample 10 uh BT so our goal will be to download it process it and make sure that our data loader can work with it so let's get to that okay so I introduced another um [03:14:18] file here that will basically download Fine web edu from huging face data sets it will pre-process and pre- tokenize all of the data and it will save data shards to a uh folder on um local disk and so while this is running uh just wanted to briefly mention that you can kind of look through the data set viewer here just to get a sense of what's in here and it's kind of interesting I mean it's a it basically looks like it's working fairly well like it's talking [03:14:48] about nuclear energy in France it's talking about Mexican America some mac PJs Etc so actually it seems like their filters are working pretty well uh the filters here by the way were applied automatically using um llama 370b I believe and so uh basically llms are judging which content is educational and that ends up making it through the filter uh so that's pretty cool now in terms of the script itself I'm not going to go through the full script because it's not as interesting [03:15:19] and not as llm Centric but when you run this basically number one we're going to load the data set uh which this is all huging face code running this you're going to need to uh pip install data sets um so it's downloading the data set then it is tokenizing all of the documents inside this data set now when we tokenize the documents you'll notice that um to tokenize a single document uh we first start the tokens with the end of text [03:15:49] token and this is a special token in the gpt2 tokenizer as you know so 50256 is the ID of the end of text and this is what begins a document even though it's called end of text but this is uh the first token that begins a document then we extend with all of the tokens of that document then we create a numpy array out of that we make sure that all the tokens are between oh okay let me debug this okay so apologies for that uh it just [03:16:19] had to do with me using a float division in Python it must be integer division so that this is an INT and everything is nice um okay but basically the tokenization here is relatively straightforward returns tokens in mp. un6 uh we're using .16 to save a little bit of space because 2 to the 16us 1 is 65,000 so the gpt2 max token ID is well below that and then here there's a bunch of multiprocessing code and it's honestly not that exciting so I'm not going to step through it but we're [03:16:50] loading the data set we're tokenizing it and we're saving everything to shards and the shards are numpy files uh so just storing a numpy array and uh which is very very similar to torch tensors and the first Shard 0000 is a Val a validation Shard and all the other shards are uh training shards and as I mentioned they all have 100 million tokens in them exactly um and and that just makes it easier to work with as to [03:17:20] Shard the files because if we just have a single massive file sometimes they can be hard to work with on the disk and so sharting it is just kind of um nicer from that perspective and uh yeah so we'll just let this run this will be probably um 30ish minutes or so and then we're going to come back to actually train on this data and we're going to be actually doing some legit pre-training in this case this is a good data set we're doing lots of tokens per second we have 8 gpus the code is ready and so we're actually [03:17:50] going to be doing a serious training run so let's get P it back in a bit okay so we're back so uh if we LS edu fine web we see that there's now 100 charts in it um and that makes sense because each chart is 100 million tokens so 100 charts of that is 10 billion tokens in total now swinging over to the main file I made some adjustments to our data loader again and that's because we're not running with uh Shakespeare anymore we want to use the fine web shards and [03:18:20] so you'll see some code here that additionally basically can load these shards uh we load the um un6 numpy file we convert it to a torch. long tensor which is what a lot of the layers up top expect by default and then here we're just enumerating all the shards I also added a split to data load of light so we can uh load the split train but also the split Val uh the zero split and then we can load the shards and then here we also have not just the current position now but also the [03:18:51] current Shard so we have a position inside A Shard and then when we uh run out of tokens in A Single Shard we first Advance The Shard and loop if we need to and then we get the tokens and readjust the position so this data loader will now iterate all the shards as well so I Chang that and then the other thing that I did while uh the data was processing is our train loader now has split train of course and down here I set up some I set up some numbers [03:19:21] so we are doing 2 to the 9 uh tokens per uh per um per step and we want to do roughly 10 billion tokens um because that's how many unique tokens we have so if we did 10 billion tokens then divide that by 29 we see that this is 1973 steps so that's where that's from and then the GPT three paper says that they warm up the learning rate over 375 million tokens so I came here and [03:19:53] 375 E6 tokens divide uh 2 to the 19 is 715 steps so that's why warm-up steps is set to 715 so this will exactly match um the warm-up schedule that gpt3 used and I think 715 by the way is very uh mild and this could be made significantly more aggressive probably even like 100 is good enough um but it's okay let's leave it for now so that we have the exact hyper parameters of gpt3 so I fix that and then um that's [03:20:25] pretty much it we can we can run so we have our script here and we can launch and actually sorry let me do one more thing excuse me for my GPU I can actually fit more batch size and I believe I can fat I can fit 60 4 on my GPU as a micro bash size so let me try that I could be misremembering but that [03:20:57] means 64 * 124 per GPU and then we have a gpus so that means we would not even be doing gradient accumulation if this fits because uh this just multi multiplies out to uh the full total bat size so no gradient accumulation and that would run pretty quickly if that fits let's go let's go I mean if this works [03:21:29] then this is basically a serious pre-training run um we're not logging we're not evaluating the validation split we're not running any evaluations yet so it's not we haven't crossed our te's and dotted our eyes but uh if we let this run for a while we're going to actually get a pretty good model and the model that might even be on par with or better than gpt2 124 M okay so it looks like everything is going great we're processing 1.5 million tokens per second uh everything here looks good [03:22:03] we're doing 330 milliseconds per iteration and we have to do a total of uh where are we printing that 1973 so 19073 times 0.33 is this many seconds this many minutes so this will run for 1.7 hours uh so one and a half hour run uh like this and uh we don't even have to use gradient accumulation which is nice and you might not have that luxury in [03:22:33] your GPU in that case just start decreasing the batch size until things fit but keep it to nice numbers um so that's pretty exciting we're currently warming up the learning rate so you see that it's still very low one4 so this will ramp up over the next few steps all the way to 6 e Nega uh 4 here very cool so now what I'd like to do is uh let's cross the T and do our eyes let's evaluate on the validation split and let's try to figure out how we can run evals how we can do logging how [03:23:05] we can visualize our losses and all the good stuff so let's get to that before we actually do the run okay so I've adjusted the code so that we're evaluating on the validation split so creating the Val loader just by passing in Split equals Val that will basically create a data loader just for the uh validation Shard um the other thing I did is in the data loader I introduced a new function reset which is called at init and it basically resets the data loader and that is very useful because when we come to the main training Loop now so this is [03:23:37] the code that I've added and basically every 100th iteration including the zeroth iteration we put the model into evaluation mode we reset the Val loader and then um no gradients involved we're going to basically accumulate the gradients over say 20 steps and then average it all up and print out the validation loss and so that basically is the exact same logic as the training Loop roughly but there's no loss that backward it's only [03:24:07] inference we're just measuring the loss we're adding it up everything else otherwise applies and is exactly as we've seen it before and so this will print the validation laws um every 100th iteration including on the very first iteration uh so that's nice that will tell us some amount some a little bit about how much we're overfitting that said like uh we have roughly Infinity data so we're mostly expecting our train and Val loss to be about the same but the other reason I'm kind of interested in this is because we can take the GPT 2124m as openi released it we can [03:24:39] initialize from it and we can basically see what kind of loss it achieves on the validation loss as well and that gives us kind of an indication as to uh how much that model would generalize to 124 M but it's not an sorry to fine web edu validation split that said it's not a super fair comparison to gpt2 because it was trained on a very different data distribution but it's still kind of like an interesting data point and in any case you would always want to have a validation split in a training run like this so that you can make sure that you are not um overfitting and this is [03:25:11] especially a concern if we were to make more Epoch in our training data um so for example right now we're just doing a single Epoch but if we get to a point where we want to train on 10 epochs or something like that we would be really careful with maybe we are memorizing that data too much if we have a big enough model and our validation split would be one way to tell whether that is happening okay and in addition to that if you remember at bottom of our script we had all of this orphaned code for sampling from way back when so I deleted that code and I moved it up um to here [03:25:43] so once in a while we simply value validation once in a while we sample we generate samples and then uh we do that only every 100 steps and we train on every single step so that's how I have a structure right now and I've been running this for 10,000 iterations so here are some samples on neration 1,000 um hello I'm a language model and I'm not able to get more creative I'm a language model and languages file you're learning about here is or is the beginning of a [03:26:14] computer okay so this is all like pretty uh this is still a garble uh but we're only at ration 1,000 and we've only just barely reached maximum learning rate uh so this is still learning uh we're about to get some more samples coming up in 1,00 okay um okay this is you know the model is still is still a young baby okay so uh basically all of this sampling code that [03:26:44] I've put here everything should be familiar with to you and came from before the only thing that I did is I created a generator object in pytorch so that I have a direct control over the sampling of the random numbers don't because I don't want to impact the RNG state of the random number generator that is the global one used for training I want this to be completely outside of the training Loop and so I'm using a special sampling RNG and then I make sure to seed it that every single rank has a different seed and then I pass in [03:27:14] here where we sort of consumer in the numbers in multinomial where the sampling happens I make sure to pass in the generator object there otherwise this is identical uh now the other thing is um you'll notice that we're running a bit slower that's because I actually had to disable torch. compile to get this to sample and um so we're running a bit slower so for some reason it works with no torch compile but when I torch compile my model I get a really scary error from pytorch and I have no idea how to resolve it right now so probably by the time you see this code released [03:27:45] or something like that maybe it's fixed but for now I'm just going to do end false um and I'm going to bring back toor compile and you're not going to get samples and I I think I'll fix this later uh by the way um I will be releasing all this code and actually I've been very careful about making get commits every time we add something and so I'm going to release the entire repo that starts completely from scratch all the way to uh now and after this as well and so everything should be exactly documented in the git commit history um [03:28:16] um and so I think that will be nice so hopefully by the time you go to GitHub uh this is removed and it's working and I will have fixed the bug okay so I have the optimization running here and it's stepping and we're on step 6,000 or so so we're about 30% through training now while this is training I would like to introduce one evaluation that we're going to use to supplement the validation set and that is the H swag eval so hos swag comes from this paper back in 2019 so it's a 5-year-old eval now and the way H swag works is there is [03:28:47] basically a sentence completion data set so it's a multiple choice for every one of these questions we have uh basically a shared context like a woman is outside with a bucket and a dog the dog is running around trying to avoid bath she a Rises the bucket off with soap and blow dry the dog's head B uses a hose to keep it from getting soapy C gets the dog wet and it runs away again or D gets into a bathtub with the dog and so basically the idea is that these [03:29:19] multiple choice are constructed so that one of them is a natural continuation of the um sentence and the others are not and uh the others might not make sense like uses the host to keep it from getting soaped that makes no sense and so what happens is that models that are not trained very well are not able to tell these apart but models that have a lot of World Knowledge and can tell uh which um and can tell a lot about the world will be able to create these [03:29:50] completions and these sentences are sourced from activity net and from Wiki how and at the bottom of the uh paper there's kind of like a cool chart of the kinds of domains in Wiki house so there's a lot of sentences from computers and electronics and Homes and Garden and it has kind of a broad coverage of the kinds of things you need to know about the world in order to find the most likely completion and um the identity of that of that completion one [03:30:22] more thing that's kind of interesting about H swag is the way it was constructed is that the incorrect um options are deliberately um adversarially sourced so they're not just random sentences they're actually sentences generated by language models and they're generated in such a way that language models basically find them difficult but humans find them easy and so they mentioned that humans have a 95% accuracy on this set but at the time the state-of-the-art language models had only 48% and so at the time this was a [03:30:54] good Benchmark now you can read the details of this paper to to learn more um the thing to point out though is that this is 5 years ago and since then what happened to H swag is that it's been totally just uh um solved and so now the language models here are 96% so basically the 4% the last 4% is probably errors in the data set or the questions are really really hard and so basically this data set is kind of crushed with respect to language models but back then the best language model was only at about 50% uh but this [03:31:27] is how far things got but still the the reason people like H swag and it's not used by the way in gpt2 but in gpt3 there is H swag eval and lots of people use H swag and so for gpt3 we have results here that are cited so we know what percent accuracies gpt3 um attains at all these different model checkpoints for H swag eval and the reason people like it is because H swag is a smooth eval and it [03:31:59] is an eval that offers quote unquote early signal uh so early signal means that even small language models are going to start at the random chance of 25% but they're going to slowly improve and you're going to see 25 26 27 Etc and uh you can see slow Improvement even when the models are very small and it's very early so it's smooth it has early signal and um it's been around for a long time so that's why people kind of like this [03:32:29] eval uh now the way that we're going to evaluate this is as follows as I mentioned we have a shared context and this is kind of like a multiple choice task but instead of giving the model a multiple choice question and asking it for A B C or D uh we can't do that because these models when they are so small as we are seeing here the models can't actually do multiple choice they don't understand the concept of associating a label to one of the options of multiple choice uh they don't understand that so we have to [03:32:59] give it to them in a native form and the native form is a token completion so here's what we do we construct a batch of four rows and uh T tokens whatever that t happens to be then the shared context that is basically the context for the for choices the tokens of that are shared across all of the rows and then we have the four options so we kind of like lay them out and then only one of the options is correct in this case label three option three and so um this [03:33:30] is the correct option and option one two and for are incorrect now these options might be of different lengths so what we do is we sort of like take the longest length and that's the size of the batch B BYT and then some of these uh here are going to be pded Dimensions so they're going to be unused and so we need the tokens we need the correct label and we need a mask that tells us which tokens are active and the mask is then zero for these uh padded areas so that's how we [03:34:01] construct these batches and then in order to get the language model to predict A B C or D the way this works is basically we're just going to look at the tokens their probabilities and we're going to pick the option that gets the lowest or the highest average probability for the token so for the tokens because that is the most likely completion according to the language model so we're just going to look at the um probabilities here and average them [03:34:33] up across the options and pick the one with the highest probability roughly speaking so this is how we're going to do H swag um and this is I believe also how uh gpt3 did it um this is how gpt3 did it as far as I know but you should note that some of the other evals where you might see H swag may not do it this way they may do it in a multiple choice format where you sort of uh give the the context a single time and then the four completions and so the model is able to [03:35:05] see all the four options before it picks the best possible option and that's actually an easier task for a model because you get to see the other options when you're picking your choice um but unfortunately models at our size can't do that only models at a bigger size are able to do that and so our models are actually slightly handicapped in this way that they are not going to see the other options they're only going to see one option at a time and they just have to assign probabilities and the correct option has to win out in this metric all right so let's now implement this very [03:35:36] briefly and incorporate it into our script okay so what I've done here is I've introduced a new file called hell swag. py that you can take a look into and I'm not going to to step through all of it because uh this is not exactly like deep code deep code it's kind of like a little bit tedious honestly because what's happening is I'm downloading hsac from GitHub and I'm rendering all of its examples and there are a total of 10,000 examples I am rendering them into this format um and so here at the end of this render [03:36:07] example function you can see that I'm returning the tokens uh the tokens of this um 4xt uh array of Tokens The Mask which tells us which parts are the options and everything else is zero and the label that is the correct label and so that allows us to then iterate the examples and render them and I have an evaluate function here which can load a um gpt2 from huging face and it runs the eval here um and it basically just calculates [03:36:40] uh just as I described it predicts the option that has the lowest or the highest prob ility and the way to do that actually is we can basically evaluate the cross entropy loss so we're basically evaluating the loss of predicting the next token in a sequence and then we're looking at the row that has the lowest average loss and that's the uh option that we pick as the prediction and then we do some stats and prints and stuff like that so that is a way to evaluate L swag now if you go up [03:37:11] here I'm showing that for GPT 2124m if you run this script you're going to see that H swag gets 29.5% um so that's the performance we get here now remember that random Chan is 25% so we haven't gone too far and gpt2 XL which is the biggest the gpt2 gets all the way up to 49% roughly so uh these are pretty low values considering that today's state-ofthe-art is more like 95% uh so these are definitely older models by now and then there's one [03:37:42] more thing called Uther harness which is a very piece of infrastructure for running evals for language models and they get slightly different numbers and I'm not 100% sure what the discrepancy is for these um it could be that they actually do the multiple choice uh instead of just the completions and that could be the um uh the discrepancy but I'm not 100% sure about that i' have to take a look but for now our script reports 2955 and so that is the number that we'd like to beat if we are training a GPD 2124m from scratch and [03:38:13] ourselves um so now I'm going to go into actually incorporating this eval into our main training script and um and basically because we want to evaluate it in a periodic manner so that we can track H swag and how it evolves over time and see when when and if we cross uh this 2955 um sort of region so let's now walk through some of the changes to train gpt2 thatp the first thing I did here is [03:38:45] I actually made use compile optional kind of and I disabled it by default and the problem with that is the problem with compile is that unfortunately it does make our code faster but it actually breaks the evaluation code and the sampling code it gives me a very gnarly message and I don't know why so hopefully by the time you get to the codebase when I put it up on GitHub uh we're going to fix that by then but for now I'm running without torch compile which is why you see this be a bit slower so we're running without torch compile I also create cre a log [03:39:15] directory log where we can place our log.txt which will record the train loss validation loss and the H swag accuracies so a very simple text file and we're going to uh open for writing so that it sort of starts empty and then we're going to append to it I created a simple variable that um helps tell us when we have a last step and then basically periodically inside this Loop every 250th iteration or at the last step we're going to evaluate [03:39:46] the validation loss and then every 250th iteration um we are going to evaluate H swag but only if we are not using compile because compile breaks it so I'm going to come back to this code for evaluating H swag in a second and then every 250th iteration as well we're also going to sample from the model and so you should recognize this as our ancient code from way back when we started the video and we're just sampling from the model and then finally here um these are if [03:40:18] we're not after we validate sample and evaluate hell swag we actually do a training step here and so this is one step of uh training and you should be pretty familiar with all of what this does and at the end here once we get our training laws we write it to the file so the only thing that changed that I really added is this entire section for H swag eval and the way this works is I'm trying to get all the gpus to collaborate on the H swag and so we're iterating all the examples and then each [03:40:48] process only picks the examples that assigned to it so we sort of take I and moded by the world size and we have to make it equal to rank otherwise we continue and then we render an example put it on the GPU we get the low jits then I create a helper function that helps us basically predict the option with the lowest loss so this comes here the prediction and then if it's correct we sort of keep count and then if multiple processes were collaborating on all this then we need to synchronize [03:41:18] their stats and so the way one way to do that is to package up our statistics here into tensors which we can then call this. alberon and sum and then here we sort of um unwrap them from tensors so that we just have ins and then here the master process will print and log the hellis swag accuracy so that's kind of the that's kind of it and that's what I'm running right here so you see this optimization here and uh [03:41:50] we just had a generation and this is Step 10,000 out of about 20,000 right so we are halfway done and these are the kinds of samples that uh we are getting at this stage so let's take a look hello I'm a language model so I'd like to use it to generate some kinds of output hello I'm a language model and I'm a developer for a lot of companies Al language model uh let's see if I can find fun one [03:42:28] um I don't know you can go through this yourself but certainly the predictions are getting less and less random uh it seems like the model is a little bit more self-aware and using language uh that is a bit more uh specific to it being language model hello I'm a language model and like how the language is used to communicate I'm a language model and I'm going to be speaking English and German okay I don't know so let's just wait until this optimization finishes and uh we'll see what kind of samples we get and we're also going to look at the [03:42:59] train Val and the hway accuracy and see how we're doing with respect to gpt2 okay good morning so focusing For a Moment On The jupyter Notebook here on the right I created a new cell that basically allows us to visualize the the train Val and Hela and um the hel score and you can step through this it basically like parses the log file that we are writing and um a lot of this is just like boring ma plot lip code but basically this is what our optimization [03:43:30] looks like so we ran for 19,731 billion tokens which is whoops oh my gosh which is one Epoch of the sample 10B of webd on the left we have the loss and the in blue we have the training loss in Orange we have the validation loss and red as a horizontal line we have the opening IG gpt2 124 M model checkpoint when it's just evaluated on [03:44:00] the validation set of um of this fine web edu uh so you can see that we are surpassing this orange is below the red so we're surpassing the validation set of this data set and like I mentioned the data set distribution is very different from what gpt2 trained on so this is not an exactly fair comparison but it's a good cross check uh to uh to look at now we would ideally like something that is withheld and comparable and somewhat standard um and [03:44:30] so for us that is helis swag and so on here we see the H swag progress we made from 25% all the way here in red we see the open gpt2 124 M model in red so it achieves this h bag here and the the gpt3 model 124 M which was trained on 300 billion tokens achieves green so that's over here so you see that we basically surpassed the gbt2 24m uh [03:45:00] model right here uh which is uh really nice now interestingly we were able to do so with only training on 10 billion tokens while gpt2 was trained on 100 billion tokens so uh for some reason we were able to get away with significantly fewer tokens for training there are many possibilities to as to why we could match or surpass this accuracy um with only 10 million training so number one um it could be that opening gbt2 was [03:45:30] trained on a much wider data distribution so in particular fine web edu is all English it's not multilingual and there's not that much math and code um and so math and code and multilingual could have been stealing capacity from the original gpt2 model and um basically that could be partially the reason why uh this is not working out there's many other reasons um so for example the H swag eval is fairly old uh maybe 5 years or so it is possible that aspects of H [03:46:02] swag in some way or even identically have made it into the training Set uh of fine web we don't know for sure but if that was the case then we are basically looking at the training curve instead of the validation curve so long story short this is not a perfect eval and there's some caveats here uh but at least we have some confidence that that we're not doing something completely wrong and um and uh it's probably the case that when people try to create these data sets they try to make sure that test sets that are very common are not part of the training set for example uh when [03:46:33] hugging face created the fine web BDU they use H swag as an eval so I would hope that they make sure that they D duplicate and that there's no hella swag in the training set but we can't be sure uh the other thing I wanted to address briefly is look at this loss curve this looks really this looks really wrong here I don't actually know 100% what this is and I suspect it's because the uh 10 billion sample of fine web edu was not properly shuffled um and there's some issue here uh with the data that I [03:47:04] don't fully understand yet and there's some weird periodicity to it um and because we are in a very lazy way sort of serializing all the tokens and just iterating all them from scratch without doing any permutation or any random sampling ourselves I think we're inheriting some of the ordering that they have in the data set so uh this is not ideal but hopefully by the time you get to this repo uh some of these things by the way will hopefully be fixed and I will release this build n GPT repo and [03:47:35] right now it looks a little ugly and preliminary uh so hopefully by the time you get here it's nicer but down here I'm going to show aada and I'm going to talk about about some of the things that happened after the video and I expect that we will have fixed uh the small issue uh but for now basically this shows that uh our training is not uh completely wrong and it shows that uh we're able to surpass the accuracy with only 10x the token budget um and possibly it could be also that the data [03:48:05] set may have improved so uh the original uh gpt2 data set was web text it's possible that not a lot of care and attention went into the data set this was very early in llms whereas now there's a lot more scrutiny on good practices around uh D duplication filtering uh quality filtering and so on and it's possible that the data that we're training on is just of higher quality per token and that could be giving us a boost as well so a number of cave has to think about but for now uh we're pretty happy with this um and yeah [03:48:36] now the next thing I was interested in is as you see it's a morning now so there was an overnight and I wanted to basically see how far I could push the result so uh to do an overnight run I basically did instead of one Epoch which took roughly two hours I just did a times four so that that would take eight hours while I was sleeping and so we did four Epoch or roughly 40 billion uh tokens of training and I was trying to see how far we could get um and so this was the only change and I reran the script and when I point uh and read the log file at uh at the 40b uh this is [03:49:08] what the curve look like okay so to narrate this number one we are seeing this issue here here with the periodicity through the different Epoch and something really weird with the fine web edu data set and that is to be determined uh but otherwise we are seeing that the H swag actually went up by a lot and we almost we almost made it uh to the GPT 324m accuracy uh up here uh but not quite so uh it's too bad that I didn't sleep slightly longer um and uh [03:49:41] I think if this was an uh five Epoch run we may have gotten here now one thing to point out is that if you're doing multi Epoch runs uh we're not actually being very careful in our data loader and we're not um I this data loader goes through the data in exactly the same format and exactly the same order and this is kind of suboptimal and you would want to look into extensions where you actually permute the data uh randomly you permute the documents around in Every Single Shard on every single new [03:50:12] Epoch um and po even permute the shards and that would go a long way into decreasing the pricity and it's also better for the optimization so that you're not seeing things ident in the identical format and you're introducing some of the some uh Randomness in how the documents follow each other because you have to remember that in every single row these documents follow each other and then there's the end of text token and then the next document so the documents are currently glued together in the exact same identical manner but we actually want to break break up the [03:50:43] documents and shuffle them around because the order of the documents shouldn't matter and they shouldn't um basically we want to break up that dependence because it's a kind of a spous correlation and so our data lad is not currently doing that and that's one Improvement uh you could think of making um the other thing to point out is we're almost matching gpt3 accuracy with only 40 billion tokens gpt3 trained on 300 billion tokens so again we're seeing about a 10x um Improvement here with respect to learning efficiency uh [03:51:14] the other thing I wanted to and I don't actually know exactly what to attribute this to other than some of the things that I already mentioned previously for the previous run uh the other thing I wanted to briefly mention is uh the max LR here I saw some people already play with this a little bit in a previous related repository um and it turns out that you can actually almost like three xas so it's possible that the maximum learning rate can be a lot higher and for some reason the gpt3 hyper parameters that we are inheriting are actually extremely conservative and you [03:51:44] can actually get away with a Higher Learning rate and it would train faster so a lot of these hyper parameters um are quite tunable and feel free to play with them and they're probably not set precisely correctly and um it's possible that you can get away with doing this basically and if you wanted to exactly be faithful to gpt3 you would also want to make the following difference you'd want to come here and the sequence length of gpt3 is 2x it's 20 48 instead [03:52:15] of 1,24 so you would come here change this to 248 for T and then if you want the exact same number of tokens uh half a million per iteration or per step you want to then decrease this to 32 so they still multiply to half a mil so that would give your model sequence length equal to that of gpt3 and in that case basically the um the models would be roughly identical as far as I'm as far as I'm aware because again gpt2 and gpt3 are very very similar models now we can also look [03:52:47] at some of the samples here from the model that was trained overnight so this is the optimization and you see that here we stepped all the way to 76290 also or so and these are the hos mag we achieved was 33.2 4 and these are some of the samples from the model and you can see that if you read through this and pause the video briefly you can see that they are a lot more coherent uh so um and they're actually addressing the [03:53:17] fact that it's a language model almost so uh hello I'm a language model and I try to be as accurate as possible um I'm a language model not a programming language I know how to communicate uh I use Python um I don't know if you pause this and look at it and then compare it to the one to the model that was only trained for 10 billion uh you will see that these are a lot more coherent and you [03:53:47] can play with this uh yourself one more thing I added to The Code by the way is this chunk of code here so basically right after we evaluate the validation loss if we are the master process in addition to logging the validation loss every 5,000 steps we're also going to save the checkpoint which is really just the state dictionary of the model and so checkpointing is nice just because uh you can save the model and later you can uh use it in some way if you wanted to resume the optimiz ation then in addition to saving the model we have to [03:54:17] also save the optimizer State dict because remember that the optimizer has a few additional buffers because of adom so it's got the m and V and uh you need to also resume the optimizer properly you have to be careful with your RNG seeds uh random number generators and so on so if you wanted to exactly be able to resume optimization you have to think through the state of the of the training process but if you just want to save the model this is how you would do it and one one nice reason why you might want to do this is because you may want to [03:54:47] evaluate the model a lot more carefully so here we are only kind of like winging the hell swag eval but you may want to use something um nicer like for example the Luther uh Luther evaluation hardness evaluation hardness hardness um so this is a way to also evaluate language models and um so it's possible that um you may want to use basically different infrastructure to more thoroughly [03:55:17] evaluate the models on different um evaluations and compare it to the opening gbt2 model on many other um tasks like for example that involve math code or different languages and so on so this is a nice functionality to have as well um and then the other thing I wanted to mention is that everything we've built here this is only the pre-training step so um the GPT here is a it dreams documents it just predicts the next to you can't talk to it like you can talk to chat GPT uh chat GPT if you wanted to [03:55:49] talk to the model we have to fine-tune it into the chat format and it's not actually like that complicated if you're looking at supervised fine-tuning or sft really what that means is we're just swapping out a data set into a data set that is a lot more conversational and there's a user assistant user assistant kind of structure and we just fine-tune on it and then we um we basically fill in the user tokens and we sample the assistant tokens it's not a lot more deeper than that uh but basically we swap out the data set and continue training uh but for now we're going to [03:56:19] stop at uh pre-training one more thing that I wanted to briefly show you is that of course what we've built up today was building towards nanog GPT which is this repository from earlier uh but also there's actually another nanog GPT implementation and it's hiding in a more recent project that I've been working on called llm Doc and lm. C is a pure Cuda implementation of gpt2 or gpt3 training and it just directly uses uh Cuda and is written as Cuda now the nanog gbt here [03:56:51] acts as reference code in pytorch to the C implementation so we're trying to exactly match up the two but we're hoping that the C Cuda is faster and of course currently that seems to be the case um because it is a direct optimized implementation so train gpt2 Pi in LL M.C is basically the nanog GPT and when you scroll through this file you'll find a lot of things that very much look like um things that we've built up in this lecture and then when you look at train [03:57:21] gpt2 docu uh this is the C Cuda implementation so there's a lot of MPI nickel GPU Cuda cc++ and you have to be familiar with that but uh um when this is built up we can actually run the two side by side and they're going to produce the exact same results but lm. C actually runs faster so let's see that so on the left I have pytorch a nanog GPT looking thing on the right I have the llmc call and [03:57:52] here I'm going to launch the two both of these are going to be running on a single GPU and here I'm putting the lm. C on GPU 1 and this one will grab uh gpu0 by default and then we can see here that lm. c compiled and then allocate space and it's stepping so basically uh meanwhile P torch is still compiling because torch compile is a bit slower here than the lm. C nbcc Cuda [03:58:24] compile and so this program has already started running and uh we're still waiting here for torch compile now of course uh this is a very specific implementation to gpt2 and 3 a pytorch is a very general neural network framework so they're not exactly comparable but if you're only interested in training gpt2 and 3 lm. C is very fast it takes less space it's faster to start and it's faster per step and so P started to Stepping here and as you can see we're running at [03:58:54] about 223,000 tokens per second here and about 185,000 tokens per second here um so quite a bit slower but I don't have full confidence that I exactly squeezed out all the juice from the pytorch implementation but the important thing here is notice that if I Aline up the steps you will see that the losses and Norms that are printed between these two are identical so on the left we have the pie torch and on the right this C [03:59:24] implementation and they're the same except this one runs faster uh so that's kind of I wanted to show you also briefly lm. C and this is a parallel implementation and it's also something that you may want to uh play with or look at and um it's kind of interesting okay so at this point I should probably start wrapping up the video because I think it's getting way longer than I anticipated uh but we did Cover a lot of ground and we built everything from scratch so as a brief summary we were looking at the gpt2 and GPT 3 [03:59:55] papers we were looking at how you set up these training runs uh and all the considerations involved we wrote everything from scratch and then we saw that over the duration of either a 2-hour training run or an overnight run we can actually match the 124 million parameter checkpoints of gbt2 and gpt3 uh to a very large extent um in principle the code that we wrote would be able to train even bigger models if you have the patients or the Computing resources uh and so you could potentially think about training some of the bigger checkpoints as well um there [04:00:26] are a few remaining issues to address what's happening with the loss here which I suspect has to do with the fine web edu data sampling uh why can't we turn on Torch compile uh it currently breaks generation and H swag what's up with that in the data loader we should probably be permuting our data when we reach boundaries so there's a few more issues like that and I expect to be documenting some of those over time in the uh build n GPT repository here which I'm going to be releasing with this video if you have any questions or like [04:00:57] to talk about anything that we covered please go to discussions tab uh so we can talk here uh or please go to issues or pull request pull requests um depending on what you'd like to contribute or also have a look at the uh Zero to Hero Discord and uh I'm going to be hanging out here on N GPT um otherwise for now I'm pretty happy about where we got um and I hope you enjoyed the video and I will see you later