Blog

Entrenar un LLM en una iGPU: nanochat (Karpathy) en una simple GPU integrada

Sin tarjeta gráfica y sin CUDA: una iGPU Intel UHD 770 entrena nanochat, el mini-ChatGPT de Karpathy, de principio a fin en menos de una hora con PyTorch XPU. Cifras medidas, iGPU + CPU, script listo para lanzar.

Entrenar un LLM en una iGPU: nanochat (Karpathy) en una simple GPU integrada

nanochat es el proyecto de Andrej Karpathy que muestra, en un solo repositorio legible, todas las etapas de un «ChatGPT»: tokenizer, preentrenamiento, fine-tuning (SFT) e interfaz de chat. Está pensado para 8 GPU NVIDIA H100, pero también ofrece una configuración reducida para CPU.

Lo hemos ejecutado por completo en una iGPU: la Intel UHD 770, la GPU integrada en el procesador (un Core i5 de sobremesa), sin ninguna tarjeta gráfica dedicada y sin CUDA. Todo con PyTorch XPU, el soporte nativo de las GPU Intel en PyTorch, y después con la iGPU y la CPU calculando juntas.

Resultados en resumen

  • Una simple iGPU basta para entrenar un LLM de principio a fin: instalación, datos, tokenizer, preentrenamiento (33 min), SFT (unos 6 min) y primeras respuestas en menos de una hora, en la GPU integrada de la VM, sin tarjeta gráfica.
  • 1,33 s por paso de preentrenamiento, es decir, unos 6150 tokens/s, para un modelo pequeño de 4 capas. Con el mismo trabajo, la GPU va unas 1,3 veces más rápido que las 6 vCPU.
  • iGPU y CPU juntas: si la CPU se encarga de 4 de las 16 filas de cada lote, el preentrenamiento pasa de 1,33 a 1,13 s por paso (un 15 % menos de tiempo), con una calidad de modelo casi idéntica (validación 1,55 frente a 1,52). Darle más trabajo a la CPU, en cambio, lo ralentiza.
  • El modelo obtenido hace de asistente y habla inglés, pero sigue siendo incoherente: es por el tamaño del modelo (37 millones de parámetros, sobre todo tablas de embeddings, 39 min de entrenamiento), no por la plataforma.
  • Hicieron falta 4 adaptaciones para la GPU Intel y 4 correcciones de nanochat para una configuración pequeña. Todo está en un script de un solo comando, al final del artículo.

¿Por qué una iGPU?

Una iGPU (GPU integrada) es la parte gráfica que está dentro del procesador, en el mismo chip que los núcleos de la CPU. Normalmente se reserva para la pantalla; aquí sirve para calcular.

  • Comparte la RAM del servidor en lugar de tener su propia memoria de vídeo. Sin límite de VRAM de 8 o 12 GB, y sin copias costosas entre CPU y GPU: es lo que hace eficiente el entrenamiento iGPU + CPU más abajo. Nuestro modelo solo usó 1,5 GB.
  • PyTorch la soporta de forma nativa (build XPU), torch.compile incluido: el mismo código que con CUDA, salvo unas pocas líneas.
  • Cuesta una fracción de una GPU dedicada: nuestro plan iGPU Pro cuesta 44,99 € al mes sin IVA, cuando una GPU dedicada en la nube se alquila a menudo de 300 a 500 € al mes.
  • Es mucho menos potente (menos de 1 TFLOPS): no sustituye a una GPU dedicada para entrenar un modelo grande. Basta para aprender, experimentar y ejecutar modelos pequeños.

Condiciones de la prueba

ElementoValor
Máquina virtualplan iGPU Pro: 6 vCPU, 24 GB de RAM, Debian 13, kernel 7.3
CPU / GPUIntel Core i5-13500, GPU integrada Intel UHD 770 (compartida con otras VM)
Softwarenanochat (commit 92d63d4), PyTorch 2.9.1 build XPU, torch.compile con Triton XPU
Modelod4: 4 capas, anchura 256, contexto de 512 tokens, vocabulario de 32 768
Preentrenamiento1500 pasos de 8192 tokens (12,3 millones de tokens)
SFT220 pasos de 8192 tokens, mezcla de conversaciones, MMLU y GSM8K

Es la configuración «CPU» de nanochat, reducida para que quepa en menos de una hora. El nanochat real (speedrun.sh, modelo de 20 capas) necesita unas 4 h en 8 H100: son tres órdenes de magnitud más de cálculo, fuera del alcance de una GPU integrada.

1. Las cifras

Cálculo bruto

Producto de matrices 2048×2048TFLOPS
fp320,71
bf160,73

La GPU es compartida: cuando otra VM la está usando, hemos medido hasta 0,27 TFLOPS. Esta GPU no tiene unidades matriciales dedicadas, así que el bf16 no va más rápido que el fp32, pero ocupa la mitad de memoria.

Entrenamiento y chat

EtapaDuraciónDetalle
Instalación (paquetes, nanochat, PyTorch XPU)~1 min
Datos (8 archivos) + tokenizer50 sen la CPU
1.ª iteración del preentrenamiento83 scompilación de los kernels GPU, una sola vez
Preentrenamiento, 1500 pasos33 min1,33 s/paso, ~6150 tokens/s, 1,5 GB de memoria GPU
SFT, 220 pasos~6 min~1,5 s/paso
1.ª respuesta del chat3 mincompilación
Respuestas siguientes13 a 15 srespuestas largas, hasta el límite de tokens

GPU frente a CPU, mismo trabajo

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

La curva de aprendizaje

InicioFinal
Preentrenamiento, bits por byte en validación3,201,52
SFT, bits por byte en validación1,351,10

Cuanto más bajo, mejor predice el modelo el texto. Las dos curvas bajan de forma regular, sin sobresaltos.

2. iGPU y CPU juntas

La GPU calcula mientras las 6 vCPU esperan. Así que pusimos a trabajar a las dos a la vez (corrección nanochat_hybrid_patch.py):

  1. Cada lote de 16 filas se parte en dos: una parte va a la GPU y la otra a una copia del modelo en la memoria de la CPU. Las dos calculan en paralelo.
  2. Los gradientes de las dos partes se suman, en proporción al número de filas. Matemáticamente es el mismo cálculo que un lote entero en un solo procesador.
  3. El optimizador actualiza el modelo en la GPU y después los pesos se copian a la copia de la CPU. En una GPU integrada, GPU y CPU comparten la misma memoria: esta copia cuesta unos 0,1 s por paso.

Todo funciona en fp32: la CPU de este servidor no tiene instrucciones bf16 rápidas y, en esta GPU, el fp32 va igual de rápido que el bf16.

¿Cuánto trabajo darle a la CPU?

Filas GPU + CPU (hilos CPU)s/pasoFrente a la GPU sola en fp32 (1,40 s)
16 + 0 (GPU sola)1,40referencia
12 + 4 (3 o 4 hilos)1,16−17 %
10 + 6 (5 hilos)1,32−6 %
10 + 6 (4 hilos)1,41sin ganancia
8 + 8 (4 hilos)1,43más lento

Contraintuitivo: darle más filas a la CPU lo ralentiza todo. Nuestras mediciones muestran que la CPU siempre termina antes que la GPU, pero que la parte de la GPU se vuelve más lenta cuando la CPU calcula: los núcleos que calculan ya no están libres para pilotar la GPU (lanzar sus kernels, gestionar la diferenciación automática). Por eso el mejor ajuste deja margen: 4 filas de 16 y 3 hilos de cálculo en 6 vCPU.

La ejecución completa, GPU sola frente a GPU + CPU

Preentrenamiento, 1500 pasosGPU sola (bf16)GPU + CPU (fp32)
Tiempo por paso1,33 s1,13 s
Tokens por segundo~6150~7200
Duración total33 min28 min
Validación final (bits por byte)1,521,55
SFT y después chatidénticoidéntico

La validación final es algo peor en el modo híbrido (1,55 frente a 1,52). Aún no hemos aislado la causa: el paso a fp32 o un efecto del reparto entre GPU y CPU.

Dos límites que conviene conocer: la ganancia depende del número de vCPU (con 2 vCPU no hay nada que repartir), y solo hemos aplicado este método al preentrenamiento, la etapa más larga. El SFT funciona solo en la GPU.

3. Lo que responde el modelo

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”…

El modelo ha aprendido la forma de una respuesta de asistente y asociaciones de palabras (France, Paris), pero no a razonar: con 4 capas y 39 minutos de cálculo, es el resultado esperado. El modelo base, antes del SFT, se limitaba a continuar el texto sin responder.

4. Lo que hubo que cambiar

Para la GPU Intel (PyTorch XPU)

  1. Reconocer la GPU: nanochat solo conoce cuda, mps y cpu. Añadimos xpu (detección, sincronización, memoria).
  2. Sin cálculo en doble precisión (fp64) en esta GPU: una línea del cálculo de los embeddings rotatorios pasaba por fp64 y fallaba (UR error: 45). La reescribimos en fp32.
  3. Cabeceras de Level Zero: torch.compile compila sus kernels GPU con Triton XPU, que necesita el paquete libze-dev.
  4. Límite de 512 work-items por grupo: la ordenación de los 32 768 tokens del vocabulario (topk) supera ese límite. La hacemos en la CPU, el coste es despreciable.

Para nanochat en configuración pequeña

Estos cuatro problemas también aparecen en CPU: vienen de la configuración reducida (contexto de 512 tokens), no de la GPU.

  1. El cargador del SFT se bloquea: solo acepta las conversaciones que caben enteras en 513 tokens. Las más largas se quedan en su cola de espera hasta llenarla, y entonces solo produce relleno: el modelo ya no aprende nada, deriva y acaba en nan. Truncamos las conversaciones a 513 tokens.
  2. Una loss nan cuando un lote no contiene ningún token de respuesta (división 0/0). Dividimos como mínimo por 1.
  3. Un learning rate demasiado alto para un modelo tan pequeño: usamos el 20 % del valor por defecto para el SFT y paramos en 220 pasos (más allá, el modelo se hundía de golpe, siempre en el mismo paso).
  4. El chat cargaba un modelo antiguo: chat_cli toma el checkpoint con el número de paso más alto, no el más reciente. Le pasamos -s 219.

5. Ejecutar nanochat en tu iGPU, paso a paso

Todo se hace por SSH en la VM (Debian 13, kernel reciente). Calcula una hora, de la que 40 minutos son de cálculo.

Todo el código (script, Docker, correcciones) está en GitHub: LinuxGPU-VPS/examples/nanochat-xpu.

Opción rápida: un solo comando

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

El script hace exactamente los pasos siguientes, cronometra cada uno en ~/nanochat-logs/timeline.txt y se detiene en el primer error.

Opción Docker

El mismo pipeline en un contenedor, sin instalar nada en la máquina salvo Docker:

git clone https://github.com/LinuxGPU-VPS/examples
cd examples/nanochat-xpu
docker compose up --build                       # solo iGPU
HYBRID_CPU_ROWS=4 docker compose up --build     # iGPU + CPU
docker compose run --rm nanochat /opt/linuxgpu/train.sh chat   # después, chatear con el modelo

La imagen (Ubuntu 24.04) incluye el controlador de GPU de Intel (Level Zero), PyTorch XPU, nanochat y las correcciones. El contenedor accede a la iGPU a través de /dev/dri. Los datos y los modelos van a ./data, y la duración de cada paso a ./logs/timeline.txt.

1. Comprobar que la iGPU es visible

ls /dev/dri    # card0 y renderD128: renderD128 es la iGPU

En nuestras VM, el comando gpu-check muestra también «Intel(R) UHD Graphics 770» y prueba OpenCL, Vulkan y la codificación de vídeo.

2. Instalar los paquetes del sistema

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

libze-dev contiene las cabeceras de Level Zero, la interfaz de bajo nivel de las GPU Intel. Sin ellas, torch.compile falla al compilar sus kernels para la iGPU (error level_zero/ze_api.h: No such file or directory).

3. Instalar nanochat y PyTorch para GPU Intel (XPU)

git clone https://github.com/karpathy/nanochat && cd nanochat
git checkout 92d63d4                  # la versión probada
curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=$HOME/.local/bin:$PATH
uv venv && uv sync --extra cpu        # dependencias de nanochat (con PyTorch CPU)
VIRTUAL_ENV=$PWD/.venv uv pip install --reinstall torch==2.9.1 \
  --index-url https://download.pytorch.org/whl/xpu   # sustituye PyTorch CPU por la 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 es el nombre que PyTorch da a las GPU Intel, como cuda para NVIDIA. La build XPU trae todo lo necesario (runtime SYCL, Triton XPU para torch.compile): no hay que instalar ningún driver propietario. No vuelvas a ejecutar uv sync después, porque volvería a poner la versión CPU.

4. Aplicar las correcciones para la iGPU

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

La corrección modifica algunas líneas de nanochat. Los tres cambios imprescindibles para la iGPU:

# nanochat/common.py: elegir la iGPU cuando está presente
elif torch.xpu.is_available():
    device_type = "xpu"

# nanochat/gpt.py: la iGPU no tiene fp64, calculamos las frecuencias en fp32
inv_freq = torch.exp(channel_range * (-math.log(base) / head_dim))

# nanochat/engine.py: la ordenación de los 32 768 tokens supera el límite de la iGPU (512 work-items)
vals, idx = torch.topk(logits.float().cpu(), k, dim=-1)

También añade las correcciones de nanochat descritas más arriba (cargador del SFT, loss nan, gradientes no finitos).

5. Configurar el entorno

export NANOCHAT_BASE_DIR=$HOME/.cache/nanochat   # datos, tokenizer y modelos
export NANOCHAT_DTYPE=bfloat16                   # si no, nanochat calcula en fp32 fuera de CUDA
export OverrideDefaultFP64Settings=1             # emulación fp64 por el driver Intel,
export IGC_EnableDPEmulation=1                   # por si alguna operación la necesita
P=.venv/bin/python

6. Descargar los datos y entrenar el tokenizer (CPU, ~1 min)

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

7. Preentrenar el modelo en la 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 envía el cálculo a la iGPU. El primer paso tarda unos 80 s: PyTorch compila sus kernels para la iGPU, una sola vez. Durante el entrenamiento, nvtop (instalado en nuestras VM) muestra la carga de la iGPU.

Para hacer trabajar la iGPU y la CPU juntas (un 15 % menos de tiempo), aplica además la segunda corrección y lanza el mismo comando con tres variables:

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 ...   # mismas opciones que arriba

8. Fine-tuning (SFT) y después 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 conversación

-s 219 elige el modelo resultante del SFT (el último paso). Sin esta opción, chat_cli toma el modelo con el número de paso más alto, que puede venir de una prueba anterior.

Qué hay que recordar

  • Una iGPU entrena de verdad un modelo de lenguaje pequeño, sin tarjeta gráfica dedicada: PyTorch nativo, torch.compile, sin CUDA, y la CPU puede ayudarla (un 15 % menos de tiempo).
  • Es un buen entorno para aprender cómo funciona un LLM de principio a fin, modificar el código y ver el efecto en pocos minutos, no para entrenar un modelo útil en producción.
  • Para servir un modelo ya entrenado, consulta mejor nuestras guías sobre los modelos de Hugging Face en iGPU y la inferencia con OpenVINO.

Probar

Todas nuestras VM tienen una iGPU Intel UHD 770 utilizable con PyTorch XPU, OpenCL y Vulkan desde la instalación, desde 14,99 € al mes sin IVA. Esta prueba se hizo en el plan iGPU Pro (6 vCPU, 24 GB de RAM, 240 GB de SSD, la mayor parte de la iGPU), a 44,99 € al mes sin IVA: la mejor opción para entrenar, gracias a las vCPU que ayudan a la iGPU. Ver los planes iGPU.