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
| Elemento | Valor |
|---|---|
| Máquina virtual | plano iGPU Pro: 6 vCPU, 24 GB de RAM, Debian 13, kernel 7.3 |
| CPU / GPU | Intel Core i5-13500, GPU integrada Intel UHD 770 (partilhada com outras VM) |
| Software | nanochat (commit 92d63d4), PyTorch 2.9.1 build XPU, torch.compile com Triton XPU |
| Modelo | d4: 4 camadas, largura 256, contexto de 512 tokens, vocabulário de 32 768 |
| Pré-treino | 1500 passos de 8192 tokens (12,3 milhões de tokens) |
| SFT | 220 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×2048 | TFLOPS |
|---|---|
| fp32 | 0,71 |
| bf16 | 0,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
| Etapa | Duração | Detalhe |
|---|---|---|
| Instalação (pacotes, nanochat, PyTorch XPU) | ~1 min | |
| Dados (8 ficheiros) + tokenizer | 50 s | na CPU |
| 1.ª iteração do pré-treino | 83 s | compilação dos kernels GPU, uma única vez |
| Pré-treino, 1500 passos | 33 min | 1,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 chat | 3 min | compilação |
| Respostas seguintes | 13 a 15 s | respostas longas, até ao limite de tokens |
GPU contra CPU, mesmo trabalho
| SFT, passos de 16 384 tokens | s/passo | tokens/s |
|---|---|---|
| GPU Intel UHD 770 | 2,64 | 6200 |
| 6 vCPU (i5-13500) | 3,0 a 3,5 | 4630 |
A curva de aprendizagem
| Início | Fim | |
|---|---|---|
| Pré-treino, bits por byte na validação | 3,20 | 1,52 |
| SFT, bits por byte na validação | 1,35 | 1,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):
- 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.
- 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.
- 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/passo | Face à GPU sozinha em fp32 (1,40 s) |
|---|---|---|
| 16 + 0 (só GPU) | 1,40 | referência |
| 12 + 4 (3 ou 4 threads) | 1,16 | −17 % |
| 10 + 6 (5 threads) | 1,32 | −6 % |
| 10 + 6 (4 threads) | 1,41 | sem ganho |
| 8 + 8 (4 threads) | 1,43 | mais 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 passos | Só GPU (bf16) | GPU + CPU (fp32) |
|---|---|---|
| Tempo por passo | 1,33 s | 1,13 s |
| Tokens por segundo | ~6150 | ~7200 |
| Duração total | 33 min | 28 min |
| Validação final (bits por byte) | 1,52 | 1,55 |
| SFT e depois chat | idêntico | idê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)
- Reconhecer a GPU: o nanochat só conhece
cuda,mpsecpu. Acrescentámosxpu(deteção, sincronização, memória). - 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. - Cabeçalhos Level Zero: o
torch.compilecompila os seus kernels GPU com Triton XPU, que precisa do pacotelibze-dev. - 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.
- 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. - Uma loss
nanquando um lote não contém nenhum token de resposta (divisão 0/0). Dividimos por pelo menos 1. - 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).
- O chat carregava um modelo antigo: o
chat_cliescolhe 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.