2 pontos por GN⁺ 2024-12-13 | 1 comentários | Compartilhar no WhatsApp
  • Com cerca de 3000 bytes de código C, o autor monta um inferidor de GPT-2 que processa em um único fluxo desde o carregamento dos pesos até a tokenização, execução do Transformer e conversão da saída
  • Mesmo mantendo o código pequeno, ele gera respostas do GPT-2 Small em poucos segundos em máquinas modernas com cache KV, multiplicação de matrizes rápida e paralelização OMP opcional
  • A qualidade da saída é descrita como “objetivamente bem ruim”, e ainda existem limitações práticas como tratamento de UTF-8 e exigência de memória ao executar modelos grandes
  • A implementação é dividida em operações de matriz, camadas de rede neural, Transformer, Byte Pair Encoding, I/O e carregamento de pesos e do BPE, mostrando a estrutura completa de um pequeno inferidor
  • O GPT-2 é um modelo open source de 2019 muito mais fraco que o GPT-4, mas os componentes centrais para rodar um modelo de linguagem moderno ainda podem ser expressos em pouco código C

Executor de GPT-2 feito em C com 3000 bytes

  • Este programa é uma implementação de GPT-2 sem dependências, que lê as matrizes de pesos e o arquivo de BPE dos arquivos originais do TensorFlow
  • A entrada é tokenizada com um codificador simples de Byte Pair Encoding (BPE), e a saída é convertida de volta em texto com um decodificador BPE
  • A estrutura interna segue de um pacote básico de álgebra linear para operações de matriz, arquitetura Transformer e código de inferência
  • O código está disponível no GitHub
  • O GPT-2 Small gera uma resposta em poucos segundos em máquinas modernas
    • Implementa cache KV
    • Usa multiplicação de matrizes eficiente
    • Pode ativar paralelização OMP opcionalmente

Requisitos de execução e limitações

  • É possível criar um programa conversacional parecido com o ChatGPT com esta implementação, mas a qualidade da saída não é boa
  • Há algumas peculiaridades no tratamento de caracteres UTF-8
  • Ao executar o modelo de tamanho XL com contexto longo, podem ser necessários cerca de 100 GB de RAM
  • Com entrada ASCII e GPT-2 Small, ele pode rodar praticamente em qualquer lugar

Como GPT-2 e o Transformer funcionam

  • ChatGPT é um aplicativo que conversa como uma pessoa usando um modelo de linguagem, e o GPT-4 é apresentado como o modelo mais recente que o alimenta
  • Este programa em C reproduz um comportamento parecido com o do ChatGPT usando o GPT-2, um modelo de 2019
  • GPT-2 é um modelo de machine learning da família Transformer
  • O Transformer recebe como entrada uma sequência de palavras de tamanho fixo e prevê a próxima palavra
  • Repetindo o mesmo procedimento, é possível gerar sequências de comprimento arbitrário

Operações de matriz e compactação com macros

  • Como redes neurais são compostas por operações de matriz, a implementação começa com uma struct Matrix mínima
    • float* dat
    • int rows, cols
  • As operações necessárias se dividem em dois grandes tipos
    • Operações matriz-escalar
    • Operações matriz-matriz
  • Com macros em C, o código reduz estruturas de loop repetidas e gera várias funções trocando apenas operadores específicos
  • O #define em C é próximo de uma substituição textual simples, então até expressões com ponto e vírgula, além de operadores comuns, podem ser passadas como argumento de macro para reduzir o tamanho do código

Multiplicação de matrizes rápida

  • A multiplicação de matrizes básica parte de uma implementação simples O(n³) com três loops aninhados
  • Considerando cache e padrões de acesso à memória, os loops são reorganizados para reler e reescrever a mesma memória repetidamente
  • A implementação rápida incrementa j e k de 4 em 4 e usa loops internos k2 e j2
  • Na etapa de inferência, também é adicionada uma forma de multiplicar apenas parte da matriz A por B para reaproveitar resultados já calculados

Implementação das camadas da rede neural

  • Para montar o Transformer, algumas camadas de rede neural são implementadas manualmente
  • A função de ativação GELU é implementada com macro
  • Para causal attention, existe uma função que processa a parte triangular inferior da matriz
    • Isso restringe a matriz de attention para olhar apenas o passado, sem ver tokens futuros
  • LayerNorm normaliza a média e a variância de cada camada
  • A função Linear soma o bias por tiling depois da multiplicação de matrizes

Corpo principal do Transformer

  • A implementação do Transformer repete o seguinte fluxo em cada camada
    • Passa por LayerNorm e Linear para calcular query, key, value de uma vez
    • Divide qkv por head
    • Calcula o produto de query e key e aplica o processamento de causal attention
    • Multiplica o resultado do softmax pela matriz value
    • Reúne os resultados e aplica residual connection
    • Passa por GELU e Linear e aplica novamente residual connection
  • No final, após a LayerNorm final, a saída da posição do último token é multiplicada pelos pesos de embedding para calcular os candidatos ao próximo token

Como funciona o cache KV

  • Na inferência com Transformer, depois de gerar um token, não é necessário recalcular a função inteira para produzir o próximo
  • Se a maior parte do resultado até o token N for reaproveitada, gerar o token N+1 exige apenas trabalho adicional parcial
  • A implementação faz todas as alocações sequencialmente dentro do mesmo bloco de memória
  • Cada multiplicação de matrizes sempre usa a mesma memória, de modo que na iteração seguinte a memória anterior é preservada sem ser reinicializada com zero
  • Na nova iteração, apenas a linha N+1 é calculada

Implementação de Byte Pair Encoding

  • Como modelos de linguagem exigem entrada de tamanho fixo, é difícil lidar com infinitas palavras diretamente no nível de palavras
  • Um modelo em nível de caracteres teria de aprender o significado de cada palavra do zero e ainda reduziria o tamanho efetivo do contexto em algo próximo ao comprimento médio das palavras
  • Modelos como o GPT-2 usam BPE para criar tokens como pedaços de palavras
    • Palavras comuns podem virar um único token
    • Palavras raras são divididas em pedaços menores
    • Por exemplo, nicholas pode ser dividido em nich, o, las
  • O algoritmo BPE típico combina repetidamente pares de tokens adjacentes
  • Esta implementação em C usa um método recursivo que pode levar tempo exponencial em vez de um algoritmo linear, para reduzir o tamanho do código
    • Procura itens do vocabulário que correspondam ao prefixo da palavra atual
    • Tokeniza recursivamente o restante da string
    • Escolhe a melhor tokenização com base no comprimento e no índice do vocabulário

Carregamento dos pesos

  • Os pesos da rede neural precisam ser lidos do disco, e o arquivo é uma serialização binária plana de floats de 32 bits
  • Os tamanhos de modelo do GPT-2 usam a mesma arquitetura, e os pesos também são armazenados na mesma ordem, então basta ler matrizes com o formato correto em sequência
  • A ordem de armazenamento das camadas é diferente do esperado
    • Depois da camada 0 e 1 vem a 10
    • Isso acontece porque os nomes são ordenados em ordem lexicográfica
    • Na ordenação de strings, 10 vem antes de 2
  • A implementação usa código de permutação para converter essa ordem para a ordem real das camadas

Carregamento do vocabulário BPE

  • Para executar o BPE, primeiro é preciso ler do disco o arquivo de vocabulário
  • O arquivo original foi feito para ser lido em Python e não em um formato fácil de fazer parse em um pequeno código C
  • O arquivo não é uma lista de palavras, mas sim uma lista de merges do BPE
    • Por exemplo, em vez de armazenar diretamente o token Hello, ele registra que H e ello devem ser combinados
  • O arquivo usa uma codificação parecida com UTF-8, mas não exatamente igual
    • Caracteres ASCII imprimíveis são armazenados diretamente
    • Caracteres não imprimíveis no intervalo 0–31 são codificados como 188 + valor do caractere
    • Por exemplo, espaço é codificado como o token Ġ
  • Como Ġ no disco é 0xc4 0xa0 em UTF-8, é preciso um tratamento separado para convertê-lo de volta em espaço

O que esse código pequeno mostra

  • É possível comprimir décadas de avanços em machine learning em alguns milhares de bytes de código
  • Tirando os pesos reais do modelo, quase nada essencial para rodar uma rede neural moderna fica de fora
  • Esta implementação foi feita principalmente por diversão, mas mostra como uma rede neural pode de fato ser executada com componentes simples

1 comentários

 
GN⁺ 2024-12-13
Opiniões no Hacker News
  • Não rodei o código pessoalmente, mas o fato de ele ser pequeno é impressionante
    Pensando que os primeiros programas ELIZA eram maiores, é como se, nos últimos 4 anos, tivéssemos conseguido espremer esse tipo de coisa byte por byte
    Se alguém souber onde está escondida a mágica, gostaria que explicasse. Fico curioso se é a função GELU ou o modelo baixado pelo script Bash

    • A maior parte da mágica está no arquivo de modelo de 475 MB baixado pelo script Bash
    • Rodei e não achei muito impressionante
      Para Who are you?, ele responde I am Alice., e, se você pergunta sobre computadores ou funcionalidades, repete I am a computer model trained by OpenAI. How can I help you?
      Quando se pede uma explicação de adição, ele dá uma explicação de multiplicação, e 2+2 ou Sum 2+2 ficam no nível de simplesmente repetir o que foi dito
  • Lembro de ter mexido no GPT-2 quando ele saiu pela primeira vez
    Exportei logs de conversa com um amigo, fiz fine-tuning do GPT-2 e o fiz imitar uma conversa entre nós dois; era muito engraçado e, às vezes, assustadoramente preciso
    Fico curioso sobre o que causou o salto enorme do GPT-2 para o GPT-3. Não sei se foi um modelo maior, mais dados ou os dois
    Sei que RLHF fez uma grande diferença, mas o modelo base do GPT-3 também já era bem útil apenas como autocomplete de texto, se você desse exemplos suficientes

  • Não sei bem, mas há alguns contos de fadas que eu adoro escritos pelo GPT-2
    https://deepdreams.stavros.io/episodes/the-princess-the-fair...

    • É realmente bom, de fato divertido, e também é uma boa história para ouvir e pegar no sono
      Fico curioso se foi feito com o GPT-2 dessa página
    • É impressionante, estranho e ainda assim cerca de 90% coerente, o que cria uma atmosfera peculiar própria
  • Acho interessante o trecho “é, em grande parte, feito por diversão, mas é um bom exemplo de como redes neurais são, na prática, simples”
    Shhh, não vamos contar para ninguém. Inteligência artificial é magia negra usada para ganhar dinheiro

  • O GPT-2 foi ajustado para instruções para poder ser usado em chat de verdade?
    Se não, sinto que chamar isso de clone do ChatGPT é forçar bastante a barra

    • O texto já diz isto: se você não se importa com a qualidade da saída, dá para fazer algo parecido com o ChatGPT, e, objetivamente, a saída é bem horrível, mas roda
      Na prática, é inutilizável e quase não tem relação além de pegar emprestado o nome. Ainda assim, é um programa que compila e executa
      Vendo as reações elogiando o desempenho de um projeto cujo próprio autor admite que não funciona direito, parece que o ponto central é mesmo chamar atenção com palavras da moda
  • A frase “estão vendo, linguagens com macros de verdade? Lisp nem sempre é melhor que C!” é aceitável desta vez. É uma piada para cima
    Se você não viu o link do código, ele está enterrado no texto: https://github.com/carlini/c-chat-gpt-2

  • Já vi chatbots clássicos de inteligência artificial melhores
    https://www.cs.cmu.edu/afs/cs/project/ai-repository/ai/areas...
    O Splotch compila bem em sistemas modernos tipo Unix com apenas alguns pequenos ajustes

  • Fico curioso se alguém rodou localmente para ver que tipo de saída este GPT-2 produz

    • Parece que ele quase sempre devolve a mesma saída repetidamente
      Ainda assim é bem interessante, e dá vontade de olhar por dentro e tentar ajustar. Faz um tempo que eu queria mexer com GPT-2 localmente
    • Pelo que li, usando a mesma temperatura e seed, o modelo GPT-2 carregado normalmente e o modelo carregado por este programa deveriam produzir exatamente a mesma saída
      Não consegui verificar diretamente a temperatura e o seed no código; eu estava mais tentando entender por que ele foi ofuscado
      Mesmo desofuscado, acho que o código não ficaria enorme; algo em torno de 10 mil caracteres já seria suficientemente impressionante só de ver na tela
  • Hoje em dia, usando gptscript, dá para implementar seu próprio ChatGPT rapidinho
    https://github.com/gptscript-ai/gptscript

  • GELU é realmente mágica:
    UNARY(GELU, b / 2 * (1 + tanh(.7978845 * (b + .044715 * b * b * b))))

    • Isso é apenas uma aproximação prática da definição matemática real da GELU
      A definição é GELU(x) := x * Φ(x), em que Φ(x) é a função de distribuição acumulada da distribuição gaussiana
    • Tem um formato que lembra a raiz quadrada inversa rápida