Blog

Treinar um LLM numa iGPU: o nanochat (Karpathy) numa simples GPU integrada

Sem placa gráfica, sem CUDA: uma iGPU Intel UHD 770 treina o nanochat, o mini-ChatGPT de Karpathy, do zero em menos de uma hora com PyTorch XPU. Números medidos, iGPU + CPU, script pronto a correr.

Treinar um LLM numa iGPU: o nanochat (Karpathy) numa simples GPU integrada

O nanochat é o projeto de Andrej Karpathy que mostra, num único repositório legível, todas as etapas de um “ChatGPT”: tokenizer, pré-treino, fine-tuning (SFT) e interface de chat. Foi pensado para 8 GPU NVIDIA H100, mas também tem uma configuração reduzida para CPU.

Corremo-lo inteiramente numa iGPU: a Intel UHD 770, a GPU integrada no processador (um Core i5 de secretária), sem qualquer placa gráfica dedicada e sem CUDA. Tudo com PyTorch XPU, o suporte nativo das GPU Intel no PyTorch, e depois com a iGPU e a CPU a calcular em conjunto.

Resultados em resumo

  • Uma simples iGPU basta para treinar um LLM do zero: instalação, dados, tokenizer, pré-treino (33 min), SFT (cerca de 6 min) e primeiras respostas em menos de uma hora, na GPU integrada da VM, sem placa gráfica.
  • 1,33 s por passo de pré-treino, ou seja cerca de 6150 tokens/s, para um modelo pequeno de 4 camadas. No mesmo trabalho, a GPU é cerca de 1,3 vezes mais rápida do que os 6 vCPU.
  • iGPU e CPU em conjunto: ao entregar à CPU 4 linhas em cada 16 de cada lote, o pré-treino passa de 1,33 para 1,13 s por passo (menos 15 % de tempo), com uma qualidade de modelo quase idêntica (validação 1,55 contra 1,52). Dar mais trabalho à CPU torna tudo mais lento.
  • O modelo obtido faz de assistente e fala inglês, mas continua incoerente: é o tamanho do modelo (37 milhões de parâmetros, sobretudo tabelas de embeddings, 39 min de treino), não a plataforma.
  • Foram precisas 4 adaptações para a GPU Intel e 4 correções do nanochat para uma configuração pequena. Está tudo num script de um só comando, no fim do artigo.

Porquê uma iGPU?

Uma iGPU (GPU integrada) é a parte gráfica que se encontra dentro do processador, no mesmo chip que os núcleos da CPU. Normalmente fica reservada ao ecrã; aqui, serve para calcular.

  • Partilha a RAM do servidor em vez de ter a sua própria memória de vídeo. Sem limite de VRAM de 8 ou 12 GB, e sem cópias dispendiosas entre CPU e GPU: é isso que torna eficiente o treino iGPU + CPU mais abaixo. O nosso modelo só usou 1,5 GB.
  • O PyTorch suporta-a nativamente (build XPU), incluindo o torch.compile: o mesmo código que com CUDA, a menos de algumas linhas.
  • Custa uma fração de uma GPU dedicada: o nosso plano iGPU Pro custa 44,99 € s/ IVA por mês, quando uma GPU dedicada na cloud se aluga muitas vezes por 300 a 500 € por mês.
  • É muito menos potente (menos de 1 TFLOPS): não substitui uma GPU dedicada para treinar um modelo grande. Basta para aprender, experimentar e correr modelos pequenos.

Condições do teste

ElementoValor
Máquina virtualplano iGPU Pro: 6 vCPU, 24 GB de RAM, Debian 13, kernel 7.3
CPU / GPUIntel Core i5-13500, GPU integrada Intel UHD 770 (partilhada com outras VM)
Softwarenanochat (commit 92d63d4), PyTorch 2.9.1 build XPU, torch.compile com Triton XPU
Modelod4: 4 camadas, largura 256, contexto de 512 tokens, vocabulário de 32 768
Pré-treino1500 passos de 8192 tokens (12,3 milhões de tokens)
SFT220 passos de 8192 tokens, mistura de conversas, MMLU e GSM8K

É a configuração “CPU” do nanochat, reduzida para caber em menos de uma hora. O nanochat completo (speedrun.sh, modelo de 20 camadas) precisa de cerca de 4 h em 8 H100: são três ordens de grandeza a mais de cálculo, fora do alcance de uma GPU integrada.

1. Os números

Cálculo bruto

Produto de matrizes 2048×2048TFLOPS
fp320,71
bf160,73

A GPU é partilhada: quando outra VM a está a usar, medimos valores até 0,27 TFLOPS. Esta GPU não tem unidades matriciais dedicadas, por isso o bf16 não é mais rápido do que o fp32, mas ocupa metade da memória.

Treino e chat

EtapaDuraçãoDetalhe
Instalação (pacotes, nanochat, PyTorch XPU)~1 min
Dados (8 ficheiros) + tokenizer50 sna CPU
1.ª iteração do pré-treino83 scompilação dos kernels GPU, uma única vez
Pré-treino, 1500 passos33 min1,33 s/passo, ~6150 tokens/s, 1,5 GB de memória GPU
SFT, 220 passos~6 min~1,5 s/passo
1.ª resposta do chat3 mincompilação
Respostas seguintes13 a 15 srespostas longas, até ao limite de tokens

GPU contra CPU, mesmo trabalho

SFT, passos de 16 384 tokenss/passotokens/s
GPU Intel UHD 7702,646200
6 vCPU (i5-13500)3,0 a 3,54630

A curva de aprendizagem

InícioFim
Pré-treino, bits por byte na validação3,201,52
SFT, bits por byte na validação1,351,10

Quanto mais baixo, melhor o modelo prevê o texto. As duas curvas descem de forma regular, sem acidentes.

2. iGPU e CPU em conjunto

A GPU calcula enquanto os 6 vCPU ficam à espera. Por isso pusemos os dois a trabalhar ao mesmo tempo (correção nanochat_hybrid_patch.py):

  1. Cada lote de 16 linhas é dividido em dois: uma parte vai para a GPU, a outra para uma cópia do modelo na memória da CPU. As duas calculam em paralelo.
  2. Os gradientes das duas partes são somados, na proporção do número de linhas. É matematicamente o mesmo cálculo que um lote inteiro num só processador.
  3. O otimizador atualiza o modelo na GPU e depois os pesos são copiados para a cópia da CPU. Numa GPU integrada, GPU e CPU partilham a mesma memória: esta cópia custa cerca de 0,1 s por passo.

Tudo corre em fp32: a CPU deste servidor não tem instruções bf16 rápidas, e nesta GPU o fp32 é tão rápido como o bf16.

Quanto trabalho dar à CPU?

Linhas GPU + CPU (threads CPU)s/passoFace à GPU sozinha em fp32 (1,40 s)
16 + 0 (só GPU)1,40referência
12 + 4 (3 ou 4 threads)1,16−17 %
10 + 6 (5 threads)1,32−6 %
10 + 6 (4 threads)1,41sem ganho
8 + 8 (4 threads)1,43mais lento

Contraintuitivo: dar mais linhas à CPU torna tudo mais lento. As nossas medições mostram que a CPU acaba sempre antes da GPU, mas que a parte da GPU fica mais lenta quando a CPU está a calcular: os núcleos ocupados a calcular deixam de estar disponíveis para controlar a GPU (lançar os seus kernels, gerir a diferenciação automática). A melhor definição deixa portanto margem: 4 linhas em 16 e 3 threads de cálculo em 6 vCPU.

O run completo, só GPU contra GPU + CPU

Pré-treino, 1500 passosSó GPU (bf16)GPU + CPU (fp32)
Tempo por passo1,33 s1,13 s
Tokens por segundo~6150~7200
Duração total33 min28 min
Validação final (bits por byte)1,521,55
SFT e depois chatidênticoidêntico

A validação final é um pouco pior no modo híbrido (1,55 contra 1,52). Ainda não isolámos a causa: a passagem para fp32 ou um efeito da divisão entre GPU e CPU.

Dois limites a ter em conta: o ganho depende do número de vCPU (com 2 vCPU não há nada a partilhar), e só aplicámos este método ao pré-treino, a etapa mais longa. O SFT corre só na GPU.

3. O que o modelo responde

What is the capital of France? The city of France has a fundamental concept in Japan, which is a city of France’s Italian history and country. […] a fundamental concept in Paris and the way to showcase the world’s most fundamental and modern aspects of the history…

Hi! Who are you? I’m glad you’ve got a few minutes of time. We’re passionate about the concept of “A”…

O modelo aprendeu a forma de uma resposta de assistente e associações de palavras (France, Paris), mas não a raciocinar: com 4 camadas e 39 minutos de cálculo, é o resultado esperado. O modelo base, antes do SFT, limitava-se a continuar o texto sem responder.

4. O que foi preciso mudar

Para a GPU Intel (PyTorch XPU)

  1. Reconhecer a GPU: o nanochat só conhece cuda, mps e cpu. Acrescentámos xpu (deteção, sincronização, memória).
  2. Sem cálculo em dupla precisão (fp64) nesta GPU: uma linha do cálculo dos embeddings rotativos passava por fp64 e falhava (UR error: 45). Reescrevemo-la em fp32.
  3. Cabeçalhos Level Zero: o torch.compile compila os seus kernels GPU com Triton XPU, que precisa do pacote libze-dev.
  4. Limite de 512 work-items por grupo: a ordenação dos 32 768 tokens do vocabulário (topk) ultrapassa este limite. Fazemo-la na CPU, o custo é desprezável.

Para o nanochat numa configuração pequena

Estes quatro problemas também aparecem em CPU: vêm da configuração reduzida (contexto de 512 tokens), não da GPU.

  1. O carregador do SFT bloqueia: só aceita as conversas que cabem inteiras em 513 tokens. As mais longas ficam na sua fila de espera até a encherem, e depois só produz preenchimento: o modelo deixa de aprender, deriva e acaba em nan. Truncamos as conversas a 513 tokens.
  2. Uma loss nan quando um lote não contém nenhum token de resposta (divisão 0/0). Dividimos por pelo menos 1.
  3. Um learning rate demasiado alto para um modelo tão pequeno: usamos 20 % do valor por omissão no SFT e paramos aos 220 passos (a partir daí, o modelo colapsava de repente, sempre no mesmo passo).
  4. O chat carregava um modelo antigo: o chat_cli escolhe o checkpoint com o maior número de passo, não o mais recente. Passamos-lhe -s 219.

5. Correr o nanochat na sua iGPU, passo a passo

Tudo se faz por SSH na VM (Debian 13, kernel recente). Conte com uma hora, das quais 40 minutos de cálculo.

Todo o código (script, Docker, correções) está no GitHub: LinuxGPU-VPS/examples/nanochat-xpu.

Opção rápida: um só comando

curl -fsSLO https://raw.githubusercontent.com/LinuxGPU-VPS/examples/main/nanochat-xpu/run.sh
sudo bash run.sh                      # só iGPU
sudo HYBRID_CPU_ROWS=4 bash run.sh    # iGPU + CPU

O script faz exatamente os passos abaixo, cronometra cada um em ~/nanochat-logs/timeline.txt e para no primeiro erro.

Opção Docker

O mesmo pipeline num contentor, sem instalar nada na máquina além do Docker:

git clone https://github.com/LinuxGPU-VPS/examples
cd examples/nanochat-xpu
docker compose up --build                       # só iGPU
HYBRID_CPU_ROWS=4 docker compose up --build     # iGPU + CPU
docker compose run --rm nanochat /opt/linuxgpu/train.sh chat   # depois, conversar com o modelo

A imagem (Ubuntu 24.04) inclui o controlador de GPU da Intel (Level Zero), o PyTorch XPU, o nanochat e as correções. O contentor acede à iGPU através de /dev/dri. Os dados e os modelos ficam em ./data, e a duração de cada passo em ./logs/timeline.txt.

1. Verificar que a iGPU está visível

ls /dev/dri    # card0 e renderD128: renderD128 é a iGPU

Nas nossas VM, o comando gpu-check também mostra “Intel(R) UHD Graphics 770” e testa OpenCL, Vulkan e a codificação de vídeo.

2. Instalar os pacotes do sistema

sudo apt-get install -y git curl build-essential libze-dev

O libze-dev contém os cabeçalhos Level Zero, a interface de baixo nível das GPU Intel. Sem eles, o torch.compile falha no momento de compilar os seus kernels para a iGPU (erro level_zero/ze_api.h: No such file or directory).

3. Instalar o nanochat e o PyTorch para GPU Intel (XPU)

git clone https://github.com/karpathy/nanochat && cd nanochat
git checkout 92d63d4                  # a versão testada
curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=$HOME/.local/bin:$PATH
uv venv && uv sync --extra cpu        # dependências do nanochat (com PyTorch CPU)
VIRTUAL_ENV=$PWD/.venv uv pip install --reinstall torch==2.9.1 \
  --index-url https://download.pytorch.org/whl/xpu   # substitui o PyTorch CPU pela build XPU
.venv/bin/python -c "import torch; print(torch.xpu.is_available(), torch.xpu.get_device_name(0))"
# True Intel(R) UHD Graphics 770

xpu é o nome que o PyTorch dá às GPU Intel, como cuda para a NVIDIA. A build XPU traz tudo o que é necessário (runtime SYCL, Triton XPU para o torch.compile): não há driver proprietário a instalar. Não volte a correr uv sync depois, porque reporia a versão CPU.

4. Aplicar as correções para a iGPU

curl -fsSLO https://raw.githubusercontent.com/LinuxGPU-VPS/examples/main/nanochat-xpu/nanochat_xpu_patch.py
python3 nanochat_xpu_patch.py .

A correção altera algumas linhas do nanochat. As três mudanças indispensáveis para a iGPU:

# nanochat/common.py: escolher a iGPU quando está presente
elif torch.xpu.is_available():
    device_type = "xpu"

# nanochat/gpt.py: a iGPU não tem fp64, calculamos as frequências em fp32
inv_freq = torch.exp(channel_range * (-math.log(base) / head_dim))

# nanochat/engine.py: a ordenação dos 32 768 tokens ultrapassa o limite da iGPU (512 work-items)
vals, idx = torch.topk(logits.float().cpu(), k, dim=-1)

Acrescenta também as correções do nanochat descritas acima (carregador do SFT, loss nan, gradientes não finitos).

5. Configurar o ambiente

export NANOCHAT_BASE_DIR=$HOME/.cache/nanochat   # dados, tokenizer e modelos
export NANOCHAT_DTYPE=bfloat16                   # senão o nanochat calcula em fp32 fora de CUDA
export OverrideDefaultFP64Settings=1             # emulação de fp64 pelo driver Intel,
export IGC_EnableDPEmulation=1                   # caso alguma operação precise dela
P=.venv/bin/python

6. Descarregar os dados e treinar o tokenizer (CPU, ~1 min)

$P -m nanochat.dataset -n 8
$P -m scripts.tok_train --max-chars=2000000000

7. Pré-treinar o modelo na iGPU (~33 min)

$P -m scripts.base_train --device-type=xpu --depth=4 --head-dim=64 --window-pattern=L \
  --max-seq-len=512 --device-batch-size=16 --total-batch-size=8192 \
  --eval-every=500 --eval-tokens=131072 --core-metric-every=-1 --sample-every=500 \
  --num-iterations=1500 --run=dummy

--device-type=xpu envia o cálculo para a iGPU. O primeiro passo demora cerca de 80 s: o PyTorch compila os seus kernels para a iGPU, uma única vez. Durante o treino, o nvtop (instalado nas nossas VM) mostra a carga da iGPU.

Para pôr a iGPU e a CPU a trabalhar em conjunto (−15 % de tempo), aplique também a segunda correção e lance o mesmo comando com três variáveis:

curl -fsSLO https://raw.githubusercontent.com/LinuxGPU-VPS/examples/main/nanochat-xpu/nanochat_hybrid_patch.py
python3 nanochat_hybrid_patch.py .
NANOCHAT_DTYPE=float32 NANOCHAT_CPU_ROWS=4 NANOCHAT_CPU_THREADS=3 \
  $P -m scripts.base_train --device-type=xpu ...   # mesmas opções que acima

8. Fine-tuning (SFT) e depois chat (~6 min)

NANOCHAT_CLIP=1.0 $P -m scripts.chat_sft --device-type=xpu --eval-every=50 \
  --eval-tokens=65536 --chatcore-every=-1 --num-iterations=220 --init-lr-frac=0.2 --run=dummy
$P -m scripts.chat_cli --device-type=xpu -s 219 -p "What is the capital of France?"
$P -m scripts.chat_cli --device-type=xpu -s 219    # modo de conversa

-s 219 escolhe o modelo saído do SFT (o último passo). Sem esta opção, o chat_cli usa o modelo com o maior número de passo, que pode vir de um ensaio anterior.

O que reter

  • Uma iGPU treina mesmo um modelo de linguagem pequeno, sem placa gráfica dedicada: PyTorch nativo, torch.compile, sem CUDA, e a CPU pode ajudá-la (menos 15 % de tempo).
  • É o terreno certo para aprender como funciona um LLM de ponta a ponta, alterar o código e ver o efeito em poucos minutos, não para treinar um modelo útil em produção.
  • Para servir um modelo já treinado, veja antes os nossos guias sobre modelos Hugging Face em iGPU e inferência com OpenVINO.

Experimente

Todas as nossas VM têm uma iGPU Intel UHD 770 utilizável pelo PyTorch XPU, OpenCL e Vulkan logo após a instalação, a partir de 14,99 € s/ IVA por mês. Este teste correu no plano iGPU Pro (6 vCPU, 24 GB de RAM, 240 GB de SSD, a maior parte da iGPU), a 44,99 € s/ IVA por mês: a escolha certa para treinar, graças aos vCPU que ajudam a iGPU. Ver os planos iGPU.