Laboratório GPT-2 · RoBERTa · Parte IV — Na sua máquina
13

O código original
da OpenAI

O GPT-2 inteiro cabe em 174 linhas de TensorFlow. Lendo a fonte primária, linha por linha, contra tudo o que você aprendeu.

Apostila 13 de 15 Pré-requisito: apostilas 01 a 06

1 O que está em repos/

Clonamos as fontes primárias. Elas não são para rodar — são para ler. Depois de doze apostilas, você tem o vocabulário para entender cada linha, e ler o original tem um valor que nenhuma explicação de segunda mão substitui.

Todos em repos\.
RepositórioO que temRoda aqui?
gpt-2/o GPT-2 original, em TensorFlow 1.x. 558 linhas no total, das quais 174 são o modelo.não — exige TF 1.15 e Python 3.7
GPT-3-Encoder/o tokenizador BPE em JavaScript, com encoder.json e vocab.bpe completossim — Node 24 está instalado
gpt-3/não tem código: tem dados. Amostras do modelo de 175 B, estatísticas do corpus, análise de contaminação.sim — é JSON
Por que o código do GPT-2 não roda mais

Ele usa tensorflow.contrib, um módulo removido no TensorFlow 2.0 (2019). E o TF 1.15, última versão que o tem, não suporta Python acima do 3.7 — estamos no 3.14. Fazer rodar exigiria um ambiente Python 3.7 separado e uma versão de TF de 2019.

Não vale a pena, e o motivo é bom: os pesos são os mesmos. O gpt2 que você carrega com transformers é exatamente o modelo deste repositório, convertido de checkpoint TensorFlow para safetensors. Você já está rodando o GPT-2 original — só que numa implementação que funciona no seu Python.

O valor deste repositório é a leitura. E para isso ele é insubstituível.

2 O bloco inteiro, no original

Aqui está a função block de gpt-2/src/model.py, sem uma linha alterada:

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

Leia devagar e reconheça tudo:

Sete linhas. É o bloco Transformer inteiro.

3 A máscara causal, no 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
A linha que separa o GPT do RoBERTa, na fonte

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

Onde b=1 (permitido), o peso passa intacto. Onde b=0 (futuro), subtrai-se 10¹⁰ — o que faz exp() devolver zero no softmax seguinte.

Note que eles usam −1e10, não −∞, exatamente como a apostila 04 descreve: infinito de verdade produz NaN em aritmética de ponto flutuante quando multiplicado por zero. Este é o tipo de detalhe que só a fonte primária ensina.

E repare no comentário deles: "doesn't produce garbage on TPUs". Há uma história inteira de depuração de hardware compactada nessa observação.

4 Três achados de quem lê o original

Achado 1: a GELU é a aproximação por tanh, escrita à mão

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

A GELU exata usa a função de distribuição acumulada da normal, que é cara. Esta é a aproximação por tangente hiperbólica — e aquele 0.044715 é uma constante ajustada empiricamente. Está no código original, escrita na mão. Se você usar nn.GELU() do PyTorch sem argumentos, terá a versão exata, ligeiramente diferente desta.

Achado 2: as três matrizes Q, K, V realmente são uma só

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

Uma única projeção para 3 × 768 = 2304, depois um corte em três. Exatamente o que a apostila 03 descreveu como detalhe de implementação — e aqui está a evidência.

O nome conv1d engana: leia a implementação e você verá que é uma matmul comum. É convolução de kernel 1, que é matematicamente idêntica a uma camada densa. Herança de convenção da época.

Achado 3: o cache KV, em duas linhas

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)

Empilha os K e V novos, concatena com os antigos. É toda a otimização que, no lab 04, você mediu valer um ganho de ~1,7× na geração — e que cresce para ordens de grandeza em textos longos.

E ela só é possível porque o modelo é causal (apostila 11). Num encoder, não haveria o que cachear.

5 O tokenizador: GPT-3-Encoder

Apesar do nome, é o tokenizador do GPT-2 — o GPT-3 reusou o mesmo. E é útil por dois motivos.

Primeiro, os arquivos de vocabulário estão ali, em texto puro:

Você pode abrir esses arquivos num editor. É o merges.txt da apostila 01, e ver as primeiras linhas é ver a estatística do inglês.

Segundo, ele roda — em Node, que você tem. É útil quando você precisa contar tokens no navegador ou num serviço JavaScript, sem carregar Python.

6 O repositório gpt-3: dados, não código

Não há implementação — o GPT-3 nunca foi aberto. Mas há coisas valiosas:

7 No laboratório

.venv\Scripts\python.exe lab\10_original_code.py
  1. Mostra trechos do model.py original lado a lado com a implementação PyTorch equivalente, para você ver a correspondência linha a linha.
  2. Abre o vocab.bpe e mostra as 20 primeiras fusões que o BPE aprendeu. As primeiras são as mais frequentes do inglês.
  3. Confirma que o tokenizador JS e o Python produzem os mesmos IDs — roda o Encoder.js via Node e compara.
  4. Extrai amostras do GPT-3 de 175b_samples.jsonl e mostra ao lado de uma geração do gpt2-xl com o mesmo tema.

Exercício 13 — leitura de fonte

  1. Abra repos/gpt-2/src/model.py. Encontre a função norm e confirme, na fórmula, que é o LayerNorm da apostila 05. O que é o epsilon=1e-5?
  2. Na mesma função, os parâmetros aprendidos se chamam g e b. Que letras a apostila 05 usou para eles?
  3. Abra repos/GPT-3-Encoder/vocab.bpe e olhe as 10 primeiras linhas. Que pares de caracteres o BPE achou mais frequentes no inglês? Isso te surpreende?
  4. Em model.py, encontre onde os logits são calculados no fim. Você consegue identificar o weight tying da apostila 02?
  5. Difícil: a função attention_mask usa i >= j - ns + nd em vez de simplesmente i >= j. Por que essa complicação? (Dica: pense no que acontece quando past não é nulo e nd ≠ ns.)
Referências

As fontes primárias dos conceitos deste capítulo. Os números do texto foram medidos no laboratório; as ideias vêm daqui.

Os termos técnicos têm aqui o sentido de aprendizado de máquina. O glossário do curso mostra esse sentido ao lado dos outros que as mesmas palavras têm na TI.

  1. OpenAI (2019). gpt-2: código e modelos do artigo, arquivo src/model.py. Repositório de código. github.com/openai/gpt-2
  2. Radford, A., Wu, J., Child, R., Luan, D., Amodei, D. e Sutskever, I. (2019). Language Models are Unsupervised Multitask Learners. Relatório técnico, OpenAI. cdn.openai.com

Você leu a fonte. A apostila 14 fecha o laboratório com a última peça prática:
como servir esses modelos por HTTP — e o que o TGI e o TEI fazem que o nosso servidor não faz.