Clone do ChatGPT feito em C com 3000 bytes, baseado em GPT-2 (2023)
(nicholas.carlini.com)- 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* datint 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
#defineem 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
jekde 4 em 4 e usa loops internosk2ej2 - 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,
nicholaspode ser dividido emnich,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,
10vem antes de2
- 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 queHeellodevem ser combinados
- Por exemplo, em vez de armazenar diretamente o token
- 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 0xa0em 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
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
Para
Who are you?, ele respondeI am Alice., e, se você pergunta sobre computadores ou funcionalidades, repeteI 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+2ouSum 2+2ficam no nível de simplesmente repetir o que foi ditoLembro 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...
Fico curioso se foi feito com o GPT-2 dessa página
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
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
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
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))))A definição é
GELU(x) := x * Φ(x), em que Φ(x) é a função de distribuição acumulada da distribuição gaussiana