LLMs em C/CUDA simples e puro, sem necessidade de 245 MB de PyTorch ou 107 MB de cPython. O foco atual está no pré-treinamento, em particular na reprodução das minisséries GPT-2 e GPT-3, juntamente com uma implementação de referência paralela do PyTorch em train_gpt2.py. Você reconhecerá este arquivo como um nanoGPT ligeiramente ajustado, um projeto anterior meu. Atualmente, llm.c é um pouco mais rápido que PyTorch Nightly (cerca de 7%). Além do código principal de última geração em train_gpt2.cu, temos uma implementação de CPU fp32 de referência simples em aproximadamente 1.000 linhas de código limpo em um arquivo train_gpt2.c. Gostaria que este repositório mantivesse apenas código C e CUDA. Portagens para outros idiomas ou repositórios são muito bem-vindas, mas devem ser feitas em repositórios separados, e tenho prazer em linká-los abaixo na seção "garfos notáveis". A coordenação do desenvolvedor acontece nas Discussões e no Discord, seja no canal #llmc
no canal Zero to Hero, ou no #llmdotc
no GPU MODE Discord.
A melhor introdução ao repositório llm.c hoje é a reprodução do modelo GPT-2 (124M). A discussão nº 481 aborda isso em detalhes. Podemos reproduzir outros modelos das séries GPT-2 e GPT-3 tanto no llm.c quanto na implementação paralela do PyTorch. Dê uma olhada no README dos scripts.
dica de depuração: ao executar o comando make
para construir o binário, modifique-o substituindo -O3
por -g
para que você possa percorrer o código em seu IDE favorito (por exemplo, vscode).
Se você não treinará em vários nós, não estiver interessado em precisão mista e estiver interessado em aprender CUDA, os arquivos fp32 (legado) podem ser do seu interesse. Esses são arquivos que foram "marcados" no início da história do llm.c e congelados no tempo. Eles são mais simples, mais portáteis e possivelmente mais fáceis de entender. Execute o código 1 GPU, fp32 assim:
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2fp32cu
./train_gpt2fp32cu
O script download_starter_pack.sh é uma maneira rápida e fácil de começar e baixa vários arquivos .bin que ajudam você a decolar. Eles contêm: 1) o modelo GPT-2 124M salvo em fp32, em bfloat16, 2) um "estado de depuração" usado em testes de unidade (um pequeno lote de dados e ativações e gradientes de destino), 3) o tokenizer GPT-2 e 3) o conjunto de dados tokenizado de tinyshakespeare. Alternativamente, em vez de executar o script .sh, você pode recriar esses artefatos manualmente da seguinte maneira:
pip install -r requirements.txt
python dev/data/tinyshakespeare.py
python train_gpt2.py
A seção "Sou tão pobre em GPU que nem tenho uma GPU". Você ainda pode gostar de ver o trem llm.c! Mas você não irá longe demais. Assim como a versão fp32 acima, a versão CPU é um ponto de verificação ainda anterior na história do llm.c, quando era apenas uma simples implementação de referência em C. Por exemplo, em vez de treinar do zero, você pode ajustar um GPT- 2 pequenos (124M) para produzir texto semelhante ao de Shakespeare, por exemplo:
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2
OMP_NUM_THREADS=8 ./train_gpt2
Se preferir evitar a execução do script do pacote inicial, conforme mencionado na seção anterior, você pode reproduzir exatamente os mesmos arquivos e artefatos .bin executando python dev/data/tinyshakespeare.py
e depois python train_gpt2.py
.
As linhas acima (1) baixam um conjunto de dados tinyshakespeare já tokenizado e baixam os pesos GPT-2 (124M), (3) iniciam deles em C e treinam para 40 etapas em tineshakespeare com AdamW (usando tamanho de lote 4, comprimento de contexto apenas 64 ), avaliar a perda de validação e obter uma amostra de algum texto. Honestamente, a menos que você tenha uma CPU robusta (e possa aumentar o número de threads OMP no comando de inicialização), você não irá tão longe nos LLMs de treinamento de CPU, mas pode ser uma boa demonstração/referência. A saída é assim no meu MacBook Pro (Apple Silicon M3 Max):
[GPT-2]
max_seq_len: 1024
vocab_size: 50257
num_layers: 12
num_heads: 12
channels: 768
num_parameters: 124439808
train dataset num_batches: 1192
val dataset num_batches: 128
num_activations: 73323776
val loss 5.252026
step 0: train loss 5.356189 (took 1452.121000 ms)
step 1: train loss 4.301069 (took 1288.673000 ms)
step 2: train loss 4.623322 (took 1369.394000 ms)
step 3: train loss 4.600470 (took 1290.761000 ms)
... (trunctated) ...
step 39: train loss 3.970751 (took 1323.779000 ms)
val loss 4.107781
generating:
---
Come Running Away,
Greater conquer
With the Imperial blood
the heaviest host of the gods
into this wondrous world beyond.
I will not back thee, for how sweet after birth
Netflix against repounder,
will not
flourish against the earlocks of
Allay
---
Os arquivos de dados dentro de /dev/data/(dataset).py
são responsáveis por baixar, tokenizar e salvar os tokens em arquivos .bin, facilmente legíveis em C. Por exemplo, quando você executa:
python dev/data/tinyshakespeare.py
Baixamos e tokenizamos o conjunto de dados tinyshakespeare. A saída disso é semelhante a esta:
writing 32,768 tokens to ./dev/data/tinyshakespeare/tiny_shakespeare_val.bin
writing 305,260 tokens to ./dev/data/tinyshakespeare/tiny_shakespeare_train.bin
Os arquivos .bin contêm um cabeçalho curto (1024 bytes) e, em seguida, um fluxo de tokens em uint16, indicando os IDs dos tokens com o tokenizer GPT-2. Mais conjuntos de dados estão disponíveis em /dev/data
.
Também estou anexando um teste de unidade simples para garantir que nosso código C esteja de acordo com o código PyTorch. Na CPU como exemplo, compile e execute com:
make test_gpt2
./test_gpt2
Isso agora carrega o arquivo gpt2_124M_debug_state.bin
que é escrito por train_gpt2.py, executa uma passagem direta, compara os logits e as perdas com a implementação de referência do PyTorch, depois faz 10 iterações de treinamento com Adam e garante que as perdas correspondam ao PyTorch. Para testar a versão da GPU, executamos:
# fp32 test (cudnn not supported)
make test_gpt2cu PRECISION=FP32 && ./test_gpt2cu
# mixed precision cudnn test
make test_gpt2cu USE_CUDNN=1 && ./test_gpt2cu
Isso testa o caminho fp32 e o caminho de precisão mista. O teste deve passar e a impressão overall okay: 1
.
Anexei um pequeno tutorial aqui, em doc/layernorm/layernorm.md. É um guia simples e passo a passo para implementar uma única camada do modelo GPT-2, a camada layernorm. Este é um bom ponto de partida para entender como as camadas são implementadas em C.
flash de atenção . A partir de 1º de maio de 2024, usaremos o Flash Attention da cuDNN. Como o cuDNN aumenta o tempo de compilação de alguns segundos para ~ minutos e esse caminho de código é muito novo no momento, ele está desabilitado por padrão. Você pode habilitá-lo compilando assim:
make train_gpt2cu USE_CUDNN=1
Isso tentará compilar com cudnn e executá-lo. Você precisa ter o cuDNN instalado em seu sistema. As instruções de instalação do cuDNN com apt-get pegarão o conjunto padrão de pacotes cuDNN. Para uma configuração mínima, o pacote cuDNN dev é suficiente, por exemplo, no Ubuntu 22.04 para CUDA 12.x:
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install libcudnn9-dev-cuda-12
Além disso, você precisa do frontend cuDNN, mas são apenas arquivos de cabeçalho. Basta clonar o repositório em seu disco. O Makefile atualmente procura por ele em seu diretório inicial ou no diretório atual. Se você o colocou em outro lugar, adicione CUDNN_FRONTEND_PATH=/path/to/your/cudnn-frontend/include
à linha de comando make
.
Certifique-se de instalar MPI e NCCL, por exemplo, no Linux:
sudo apt install openmpi-bin openmpi-doc libopenmpi-dev
Para NCCL siga as instruções do site oficial (por exemplo, instalador de rede)
e então:
make train_gpt2cu
mpirun -np < number of GPUs > ./train_gpt2cu
ou simplesmente execute um de nossos scripts em ./scripts/
.
Certifique-se de ter instalado NCCL
seguindo as instruções da seção multi-GPU.
Atualmente oferecemos suporte a três maneiras que permitem executar treinamento de vários nós:
./scripts/multi_node/run_gpt2_124M_mpi.sh
para obter detalhes../scripts/multi_node/run_gpt2_124M_fs.sbatch
para obter detalhes../scripts/multi_node/run_gpt2_124M_tcp.sbatch
para obter detalhes.Observação:
slurm-wlm
abandonou o suporte PMIx), você terá que usar a abordagem FS (2) ou TCP (3) . Para testar se o seu slurm suporta a execução do PMIx: srun --mpi=list
e veja se você obtém pmix
na saída.mpirun
- MPI (1).Nenhum desses 3 métodos é superior, apenas oferecemos opções para que você possa rodar em seu ambiente específico.
Apenas como exemplo de processo para varrer as taxas de aprendizado em uma máquina com 4 GPUs no TinyStories. Execute um script de shell sweep.sh
(após você, é claro, chmod u+x sweep.sh
):
#! /bin/bash
learning_rates=(3e-5 1e-4 3e-4 1e-3)
for i in {0..3} ; do
export CUDA_VISIBLE_DEVICES= $i
screen -dmS " tr $i " bash -c " ./train_gpt2cu -i data/TinyStories -v 250 -s 250 -g 144 -l ${learning_rates[$i]} -o stories $i .log "
done
# you can bring these down with
# screen -ls | grep -E "tr[0-3]" | cut -d. -f1 | xargs -I {} screen -X -S {} quit
Este exemplo abre 4 sessões de tela e executa os quatro comandos com LRs diferentes. Isso grava os arquivos de log stories$i.log
com todas as perdas, que você pode plotar como desejar em Python. Um exemplo rápido de como analisar e plotar esses arquivos de log está em dev/vislog.ipynb.
Mais algumas palavras sobre o que desejo que este repositório seja:
Primeiro, quero que llm.c
seja um lugar de educação. Por exemplo, nossa pasta dev/cuda
é um local para uma biblioteca de kernels para todas as camadas que são escritas manualmente à mão e muito bem documentadas, desde kernels muito simples até kernels mais complexos/mais rápidos. Se você tiver um novo kernel com várias compensações diferentes, sinta-se à vontade para contribuir aqui.
Dito isso, também quero que llm.c
seja muito rápido, até mesmo útil na prática para treinar redes. Por exemplo, para começar, deveríamos ser capazes de reproduzir a grande execução de treinamento do GPT-2 (1.6B). Isso requer que incorporemos quaisquer kernels mais rápidos que existam, incluindo o uso de bibliotecas como cuBLAS, cuBLASLt, CUTLASS, cuDNN, etc. Também acho que isso serve a um propósito educacional para estabelecer um limite superior especializado e uma unidade de medida, por exemplo, você poderia dizer que seus kernels escritos manualmente têm 80% da velocidade do cuBLAS, etc. Então você pode optar por fazer uma execução super rápida ou pode optar por "arrastar e soltar" quaisquer kernels manuais que deseja usar e executar com aqueles.
No entanto, como restrição, quero manter a linha principal llm.c
na pasta raiz simples e legível. Se houver um PR que, por exemplo, melhora o desempenho em 2%, mas "custa" 500 linhas de código C complexo e talvez uma dependência exótica de terceiros, posso rejeitar o PR porque a complexidade não vale a pena. Como um exemplo concreto - tornar cuBLAS para matmuls o padrão no loop de treinamento raiz é óbvio: torna o código da linha principal muito mais rápido, é uma única linha de código interpretável e é uma dependência muito comum. Além disso, podemos ter implementações manuais que podem competir com cuBLAS em dev/cuda
.
Por fim, serei muito mais sensível à complexidade na pasta raiz do projeto, que contém os arquivos principais/padrão do projeto. Em comparação, a pasta dev/
é um pouco mais como um espaço de trabalho para desenvolvermos uma biblioteca de kernels ou classes e compartilhar código útil, relacionado ou educacional, e parte desse código pode ser aceitável (localmente) complexo.
Suporte AMD
C#
CUDA C++
C++/CUDA
WebGPU C++
C++
Ir
Java
Metal
Mojo
OpenCL
Ferrugem
Rápido
Ziguezague
Havana Gaudi2
Nim
Formas de organizar o desenvolvimento:
#llmc
no meu canal Zero to Hero Discord. MIT