Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

GPT from “scratch” in Flax

TLDR:

  • This single notebook contains a full GPT that you can train at home.

  • The things to pay attention to are:

    • The tokenizer

    • The model structure and definition

    • How the model performance changes over various checkpoints

The trained model and the initial model

Before we dive in, here’s the final model. It was trained on the Tiny Shakespeare dataset.

MENENIUS:
COMINIUS:
Why, sir, or hast been so what I did;
Respect is his, and go with holds,
And backs hangment tomb, Camillo true,
To speak no away in this son of roopiness;
Whithe that is all for you was, kind how I am
Hope for you as time our place.

LARTIUS:
Alas, only to love, to ho,
Him it with the cause to one this, bitter her
May a shepherd haze. Angelo.
But one till the officers to this true,
Thus expect us was unto the; for I am every begue;
And come unto our bild, man, then joy in his Pome.

BENVOLIO:
How down seems be my voiciant is upon himself,
And since him out of a diath. whom

And here’s where we started, a bunch of random characters.

boysZBZMAhCxFaSKeNyDs,WPHVxVWLoH hus
dV,XbV
Dhwha'rJFolY;rRlhRsL;PQsih$w-jYIWO$KT iyFUhrlYT,! xHZ
gUBslha &mHcYO.KmUDaCLjGqg? eVTYSOlllqJh$?$MlSTwj;jAACkPMNsJ-;!NCzrIhgiSwkb;M!VrQLQ:?gN.m:vNFlVf&OK,X-ygBaXYLkxWSu:o3fkrxBd;y:CMQHRuZWbs
VA,jkUuYzq::xXOeCst;qjLLdEKAAZHOU?:pmnzp:rEZRI&v&lPjJ,BVbmc N:wxAZ$AX-LnawOzE; GoyDOHg&xSuFCKTTkwJCxTMh:oFo,&3?izWwD wh!yEVokSP.qesfhfoIV!&.cwmL:KbRofnrdmXDETA'-'GWPURTzwX.ubveT.XMHY;f-wmz:-EqBHFab;gfJVJL-XIQe'WdCc$nguDpF3pTsB:3jI.bCKvYlJPCRCp,o?3atqGI.NPjRbkvBSex,QPdIekjhx! MUjbSYor:BcDDkff&HcbQN,gkv3BvNMdr-kddiRmubfc.XEGsImf'uqqZJqrLzCAgsUKVQnU$xpAgTVbalnVbKUpM

The rest of this notebook shows how we get from our random initial LLM to our trained one.

Why Flax and Jax

Similar to PyTorch, Flax and Jax are production-grade tools that are used from individual research to massive multi-cluster LLMs.

For this guidebook, there are some additional reasons:

  • I find that the separation of state, model definition, and training loops in Flax make it easier to disambiguate the concepts.

  • Being able to print the layer outputs as well helped with understanding and debugging.

  • Deterministic by default makes debugging simpler.

  • Now you have both a PyTorch and Flax/Jax reference!

This notebook dives straight into a neural network model. If you need a primer on Neural Networks, start with NN Quickstart.

Additional Notes

Here are some additional details for easy reference:

B, T, C Meaning

Karpathy uses the shorthand symbols B, T, C in his code and videos. He explains their meanings here, which I’ll write out for easy reference:

  • B - Batch - How many training examples we’re using at once. This is just for computational efficiency.

  • T - Time - In a text input array, either encoded or decoded, the index that is being referenced.

  • C - Channel - The specific token “number”, or index in the embedding array. This matches the vocabulary size, since each token gets its own channel.

Block Size is Context Window

The length of the input sequence the model gets for making the next character prediction.

Suggested Exercises

For additional hands-on learning, I’ve included two additional datasets here, along with all the code for training and text generation.

abcdef A quick brown fox jumped over the lazy dog

These strings have specific properties. The first has no repeating characters, and it’s easy to encode and decode to integers in your head. This means it’s easy to get an overfit model with perfect predictions for debugging, as well as inspecting the parameters. The second has no repeating alphabetic characters, but it does have spaces, adding just one additional layer of complexity.

For additional debugging and understanding, you can perform an ablation study. That means removing parts of the model, making certain parameters smaller or larger, and seeing what happens. Visualize parts of the learned parameters using heatmaps and other tools to see what the model is learning.

qb = QuickBrownFox(block_size = block_size, batch_size=batch_size)
ab = Alphabet(block_size = block_size, batch_size=batch_size)

Imports

!pip install flax==0.7.5 jax==0.4.24 optax

WARNING:absl:Type handler registry overriding type "<class 'float'>" collision on scalar
WARNING:absl:Type handler registry overriding type "<class 'bytes'>" collision on scalar
WARNING:absl:Type handler registry overriding type "<class 'numpy.number'>" collision on scalar
WARNING:absl:Type handler registry overriding type "<class 'bytes'>" collision on scalar
WARNING:absl:Type handler registry overriding type "<class 'numpy.number'>" collision on scalar
WARNING:absl:Tensorflow library not found, tensorflow.io.gfile operations will use native shim calls. GCS paths (i.e. 'gs://...') cannot be accessed.
[CpuDevice(id=0)]

Data Loaders

This text-based class implements the tokenizer, as well as the string encoder and decoder. This is not to be confused with the encoder and decoder portions of a Transformer, which are different things.

Three datasets are included:

  • Tiny Shakespeare - This is the original from the tutorial.

  • Alphabet - Just the alphabet, no character repeats so the LLM should be able to memorize the next token.

  • QuickBrownFox - A sentence where letters don’t repeat, but spaces do, leading to a tiny bit of variability.

The second two datasets are included for debugging purposes. One of the easiest ways to debug an LLM, or any deep learning model, is to overfit a simple dataset. If your model can’t do that, you have problems.

Random Keys

JAX, on which Flax is based, by design requires explicit keys for its randomness. This is counterintuitive but smart for many reasons. Read the docs to learn more.

Model Constants

Load and Encode Data

'First Citizen:\nBefore we proceed any further, hear'
65
Array([[ 1, 61, 43, 43, 42, 57, 6, 1, 61, 47, 58, 46, 1, 53, 60, 43, 56, 61, 46, 43, 50, 51, 47, 52, 45, 1, 40, 56, 53, 61, 57, 6, 0, 15, 59, 50, 50, 47, 52, 45, 1, 53, 44, 1, 57, 47, 51, 54, 50, 43, 57, 11, 1, 51, 43, 39, 45, 56, 43, 1, 61, 43, 56, 43], [ 8, 0, 14, 59, 58, 1, 39, 57, 1, 58, 46, 47, 57, 1, 58, 47, 58, 50, 43, 1, 46, 53, 52, 53, 59, 56, 57, 1, 51, 43, 1, 39, 52, 42, 1, 51, 47, 52, 43, 6, 0, 31, 53, 1, 63, 53, 59, 56, 1, 42, 47, 57, 50, 47, 49, 43, 6, 1, 58, 53, 1, 61, 46, 53]], dtype=int32)
[' weeds, with overwhelming brows,\nCulling of simples; meagre were', '.\nBut as this title honours me and mine,\nSo your dislike, to who']

Model Parameters

(128, 1, 64, 64)

Multi-Head Attention

Now, this is not from scratch, but I chose to use the pre-canned version to show what a production implementation would look like. Andrej already does a great job explaining the internals of attention, so I didn’t feel that repeating it here would add any extra value at the moment.

Test Parameter Initialization

Output shape after applying MultiHeadAttention: (128, 64, 120)

Feedforward Layer

(batch_size, block_size) -> (batch_size, block_size, n_embd)

Output shape after applying FeedForward: (128, 64, 120)
Loading...



Block

INFO:notebook:Output tensor shape after block application: block.apply(block_params, input_x, training=False)['x'].shape=(128, 64, 120)
Loading...



Full Model

DEBUG:notebook:Input tensor shape: (128, 64)
DEBUG:notebook:Input shape in __call__: (128, 64)
dict_keys(['TokenEmbedding', 'Position Embedding', 'Block_0', 'Block_1', 'Block_2', 'Block_3', 'Block_4', 'Block_5', 'LayerNorm', 'Final Dense'])
DEBUG:notebook:Input shape in __call__: (128, 64)
Loading...



Sample Generation

This is with initial parameters that are totally random.

































































brCqDW!cVenexdcDOJhoE.:qwwC'ESzBDoXvJoCFcTO;R!DZ H

Training

dict_keys(['TokenEmbedding', 'Position Embedding', 'Block_0', 'Block_1', 'Block_2', 'Block_3', 'Block_4', 'Block_5', 'LayerNorm', 'Final Dense'])
Training step count: 6

Training Loop

This model was trained on my RTX 4090. It took about 30 minutes to train.

Loading...
Epoch 0: Train Loss 2.9735
Epoch 100: Train Loss 2.5170
Epoch 200: Train Loss 2.3874
Epoch 300: Train Loss 2.2306
Epoch 400: Train Loss 2.0848
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[51], line 22
     19 xb, yb, ix = ts.get_batch(train_key)
     21 # Perform a training step and capture the loss
---> 22 state, loss = train_step(state, xb, yb)
     23 losses.append(loss)
     25 # Periodically evaluate and report the loss

File <string>:1, in <lambda>(_cls, count, mu, nu)

KeyboardInterrupt: 

Training Loss

<Figure size 640x480 with 1 Axes>

Final Results

We can now use our weights to generate text. At a glance, it’s not bad.

MENENIUS:
COMINIUS:
Why, sir, or hast been so what I did;
Respect is his, and go with holds,
And backs hangment tomb, Camillo true,
To speak no away in this son of roopiness;
Whithe that is all for you was, kind how I am
Hope for you as time our place.

LARTIUS:
Alas, only to love, to ho,
Him it with the cause to one this, bitter her
May a shepherd haze. Angelo.
But one till the officers to this true,
Thus expect us was unto the; for I am every begue;
And come unto our bild, man, then joy in his Pome.

BENVOLIO:
How down seems be my voiciant is upon himself,
And since him out of a diath. whom

Comparing Checkpoints

What’s interesting is going back in time and comparing generations from different checkpoints. Since we saved earlier checkpoints, we can load them and see what the LLM had learned up to that point.

Checkpoint 501
o sand Ah tiat,eNyh , t silenod has
d Aobe
Dh,hacr lolipr lh s teisihh mosaaOgn niyaUhrl T,! In

r Bsl a Ime Hot mor CLhtqIw tre Smellosh
t nleawe IiACtl Nssor NCtroh iteke;sacr Ie tg bi:slalrf,sarerygdaaYiku S noS konddiy:iM Hhusote
VA,iktrodh: toweisteuRsodl
AAuHtstopm r :rloRIshtla o,raboc N:w t to
-Ln tOate mo s
Hgr Sub.
TTk h xnehaoao,hehaiAme wheyouok n
ursowfed r .cw s Kboof r ms toA'tiGWtoRfawon
beeTm Mnaufowazirrse tab;gfyosor Ite
Wdochagu pe r se itIr t vol wy Cp omaatit leher
tiBSen,iPd ek tor  Uubheoryan r
f ser m borcses Mdi-k diumubfciOElsamfeunrcisiLun gsh Von REaAITratle bh?e
Checkpoint 1101
Thostou phithat, Nous, theey, ou has deave sow, a'r couth now sthensing mustatgn nind
Irlls, wencir Beloa I you the gard me weere bolllon bothe awngeis
Wonksser! Cordowitike;
What to greimstant for frighe ming andoo kend staioud us bear tikirought
Heeinte; sodllleantst:
my to love nour h, abe it wit to band,
at omonst gravubstink hext have sheais we wheave, andsesth, I mu.

As sooof warstt yould tould thue ep.
Morclowaze-
Andt beavy sor I hat ochesuspe pese it dame Allll my omantinge.

TROFBUCHANP IESTHARD UV:
You me hand me ma, you, midisk dighur:
O that fout cist trecs: Von heave hat now spa

Checkpoint 5901

KING EDWARD IV:
You should be our us;n all some acculous;
Respemions his jotal no in shrl beawen to Bilianca
Angromer Captque true of soldiers, away is
Wondomen to roogion by mertaint, being the sakery
Backing and of thy son. Though say the poover
His stoublt loooks stop not: love he fir,
Him it with seen at a toloner gracuble,
That I have should be when woman.

BENVOLIO:
Why, sir, no must you pray! I thus explumply was:
The tabegot sorrow wedow again; and compract our
Company at their match exputine.


GLOUCESTER:
How farewell,
Is it is to dismublcients, for this man could
Thee a dranged by M