GPT-2 · RoBERTa Lab · Part IV — On your machine
13

OpenAI's original
code

The whole of GPT-2 fits in 174 lines of TensorFlow. Reading the primary source, line by line, against everything you have learned.

Chapter 13 of 15 Prerequisite: chapters 01 to 06

1 What is in repos/

We cloned the primary sources. They are not for running — they are for reading. After twelve chapters, you have the vocabulary to understand every line, and reading the original has a value that no second-hand explanation replaces.

All in repos\.
RepositoryWhat it containsRuns here?
gpt-2/the original GPT-2, in TensorFlow 1.x. 558 lines in total, of which 174 are the model.no — requires TF 1.15 and Python 3.7
GPT-3-Encoder/the BPE tokenizer in JavaScript, with the complete encoder.json and vocab.bpeyes — Node 24 is installed
gpt-3/has no code: it has data. Samples from the 175 B model, corpus statistics, contamination analysis.yes — it is JSON
Why the GPT-2 code no longer runs

It uses tensorflow.contrib, a module removed in TensorFlow 2.0 (2019). And TF 1.15, the last version that has it, does not support Python above 3.7 — we are on 3.14. Making it run would require a separate Python 3.7 environment and a 2019 version of TF.

It is not worth it, and the reason is a good one: the weights are the same. The gpt2 you load with transformers is exactly the model in this repository, converted from a TensorFlow checkpoint to safetensors. You are already running the original GPT-2 — just in an implementation that works on your Python.

The value of this repository is the reading. And for that it is irreplaceable.

2 The whole block, in the original

Here is the block function from gpt-2/src/model.py, not a line altered:

def block(x, scope, *, past, hparams):
    with tf.variable_scope(scope):
        nx = x.shape[-1].value
        a, present = attn(norm(x, 'ln_1'), 'attn', nx, past=past, hparams=hparams)
        x = x + a
        m = mlp(norm(x, 'ln_2'), 'mlp', nx*4, hparams=hparams)
        x = x + m
        return x, present

Read slowly and recognize everything:

Seven lines. It is the entire Transformer block.

3 The causal mask, in the original

def attention_mask(nd, ns, *, dtype):
    """1's in the lower triangle, counting from the lower right corner.
    Same as tf.matrix_band_part(tf.ones([nd, ns]), -1, ns-nd), but doesn't
    produce garbage on TPUs."""
    i = tf.range(nd)[:,None]
    j = tf.range(ns)
    m = i >= j - ns + nd
    return tf.cast(m, dtype)

def mask_attn_weights(w):
    _, _, nd, ns = shape_list(w)
    b = attention_mask(nd, ns, dtype=w.dtype)
    b = tf.reshape(b, [1, 1, nd, ns])
    w = w*b - tf.cast(1e10, w.dtype)*(1-b)
    return w
The line that separates GPT from RoBERTa, in the source

w = w*b - 1e10*(1-b)

Where b=1 (allowed), the weight passes intact. Where b=0 (future), 10¹⁰ is subtracted — which makes exp() return zero in the following softmax.

Note that they use −1e10, not −∞, exactly as chapter 04 describes: a true infinity produces NaN in floating-point arithmetic when multiplied by zero. This is the kind of detail only the primary source teaches.

And notice their comment: "doesn't produce garbage on TPUs". There is an entire hardware-debugging story compressed into that remark.

4 Three findings from reading the original

Finding 1: the GELU is the tanh approximation, written by hand

def gelu(x):
    return 0.5*x*(1+tf.tanh(np.sqrt(2/np.pi)*(x+0.044715*tf.pow(x, 3))))

The exact GELU uses the cumulative distribution function of the normal, which is expensive. This is the hyperbolic-tangent approximation — and that 0.044715 is an empirically tuned constant. It is in the original code, written by hand. If you use PyTorch's nn.GELU() with no arguments, you get the exact version, slightly different from this one.

Finding 2: the three matrices Q, K, V really are a single one

c = conv1d(x, 'c_attn', n_state*3)
q, k, v = map(split_heads, tf.split(c, 3, axis=2))

A single projection to 3 × 768 = 2304, then a cut into three. Exactly what chapter 03 described as an implementation detail — and here is the evidence.

The name conv1d is misleading: read the implementation and you will see it is an ordinary matmul. It is a kernel-1 convolution, which is mathematically identical to a dense layer. A convention inherited from the era.

Finding 3: the KV cache, in two lines

present = tf.stack([k, v], axis=1)
if past is not None:
    pk, pv = tf.unstack(past, axis=1)
    k = tf.concat([pk, k], axis=-2)
    v = tf.concat([pv, v], axis=-2)

Stack the new K and V, concatenate with the old ones. That is the whole optimization which, in lab 04, you measured to be worth a ~1.7× gain in generation — and which grows to orders of magnitude on long texts.

And it is only possible because the model is causal (chapter 11). In an encoder, there would be nothing to cache.

5 The tokenizer: GPT-3-Encoder

Despite the name, it is the GPT-2 tokenizer — GPT-3 reused the same one. And it is useful for two reasons.

First, the vocabulary files are there, in plain text:

You can open these files in an editor. It is the merges.txt of chapter 01, and seeing the first lines is seeing the statistics of English.

Second, it runs — in Node, which you have. It is useful when you need to count tokens in the browser or in a JavaScript service, without loading Python.

6 The gpt-3 repository: data, not code

There is no implementation — GPT-3 was never open-sourced. But there are valuable things:

7 In the lab

.venv\Scripts\python.exe lab\10_original_code.py
  1. Shows excerpts of the original model.py side by side with the equivalent PyTorch implementation, so you can see the line-by-line correspondence.
  2. Opens vocab.bpe and shows the first 20 merges the BPE learned. The first ones are the most frequent in English.
  3. Confirms that the JS tokenizer and the Python one produce the same IDs — runs Encoder.js via Node and compares.
  4. Extracts GPT-3 samples from 175b_samples.jsonl and shows them next to a gpt2-xl generation on the same theme.

Exercise 13 — source reading

  1. Open repos/gpt-2/src/model.py. Find the norm function and confirm, from the formula, that it is the LayerNorm of chapter 05. What is epsilon=1e-5?
  2. In the same function, the learned parameters are called g and b. Which letters did chapter 05 use for them?
  3. Open repos/GPT-3-Encoder/vocab.bpe and look at the first 10 lines. Which character pairs did the BPE find most frequent in English? Does that surprise you?
  4. In model.py, find where the logits are computed at the end. Can you identify the weight tying of chapter 02?
  5. Hard: the attention_mask function uses i >= j - ns + nd instead of simply i >= j. Why this complication? (Hint: think about what happens when past is not null and nd ≠ ns.)
References

The primary sources for the concepts in this chapter. The numbers in the text were measured in the lab; the ideas come from here.

Technical terms here carry their machine-learning meaning. The course glossary shows that meaning next to the others the same words have in IT.

  1. OpenAI (2019). gpt-2: código and modelos do artigo, arquivo src/model.py. Code repository. github.com/openai/gpt-2
  2. Radford, A., Wu, J., Child, R., Luan, D., Amodei, D. and Sutskever, I. (2019). Language Models are Unsupervised Multitask Learners. Technical report, OpenAI. cdn.openai.com

You have read the source. Chapter 14 closes the lab with the last practical piece:
how to serve these models over HTTP — and what TGI and TEI do that our server does not.