17°
ModelosGPUClimaTutorial

Corrí AIFS 2.0, el modelo de clima de ECMWF, en mi tarjeta de escritorio y quedé a 0.45 °C del pronóstico oficial

ECMWF publicó los pesos de su modelo de pronóstico con IA. Pesa menos de un giga y corre en una tarjeta de escritorio: 48 horas de pronóstico en 108 segundos y 5.77 GB de memoria, o 33 minutos si no tienes GPU. Lo instalé paso a paso, lo comparé contra el modelo físico operativo del mismo centro y anoté los dos tropiezos que casi me hacen publicar un disparate.

Efrain Garay 16 de agosto de 2026

ECMWF publicó los pesos de AIFS, su modelo de pronóstico basado en datos. No una demo ni una API con cuota: el punto de control completo, con licencia CC-BY-4.0, en Hugging Face.

Lo que me hizo dejar todo y probarlo es que un pronóstico es de las poquísimas cosas que puedo publicar sabiendo que no soy yo quien decide si estuvo bien. Un benchmark de modelos lo diseño, lo corro y lo califico yo. Una predicción del clima la califica el jueves.

Así que lo instalé, lo corrí y guardé el pronóstico con fecha y hora antes de saber qué iba a pasar.

El resumen en 23 segundos: lo que pesa, lo que tarda y cuánto se acerca al pronóstico oficial. Sin sonido por defecto: actívalo en los controles.Verlo en el visualizador de reels →

Qué es AIFS y por qué importa que quepa

AIFS es un transformador de grafos con ventana deslizante, entrenado sobre ERA5 y los análisis operativos de ECMWF. La versión aifs-single-2.0 es determinista y produce un estado global cada seis horas.

El dato que cambia todo: el punto de control pesa 994 MB. El modelo físico con el que compite, el IFS, corre en uno de los superordenadores más grandes de Europa. Este cabe en una tarjeta de videojuegos.

La barrera real no es el tamaño. Es que AIFS fue entrenado llamando a flash_attn_func, del paquete flash-attn, que solo compila en tarjetas Ampere o posteriores. Mi 4070 Ti SUPER sí entra en ese rango; el muro es para todo lo anterior, y para quien no tenga CUDA.

Paso 1: el entorno

Python entre 3.11 y 3.13, y un entorno virtual propio. Nada de instalar esto encima de un entorno que ya funciona.

mkdir -p ~/aifs && cd ~/aifs
python3.12 -m venv venv
git clone https://github.com/huggingface/AIFS-single-2.0-on-all-GPUs
cd AIFS-single-2.0-on-all-GPUs
~/aifs/venv/bin/pip install -r requirements.txt

Ese repositorio no es de ECMWF y lo dice en la primera línea. Es un envoltorio que intercepta la importación de flash_attn y la redirige a scaled_dot_product_attention, la atención fusionada que PyTorch trae desde la versión 2.0 y que funciona en CUDA, en Metal y en CPU.

No hace falta instalar flash-attn. Ese es todo el truco.

Paso 2: el tropiezo que casi me deja corriendo en CPU sin darme cuenta

Terminada la instalación, esto:

torch 2.13.0+cu130 | cuda disponible: False
UserWarning: CUDA initialization: The NVIDIA driver on your system is too old
(found version 12080)

El requirements.txt pide torch>=2.1, y pip resuelve eso a la última versión, que viene compilada contra CUDA 13. El driver de mi máquina es el 570.144, o sea CUDA 12.8. PyTorch no falla: desactiva CUDA y sigue. Si no miro esa línea, todo el pronóstico corre en procesador y yo creyendo que uso la tarjeta.

Se arregla pidiendo explícitamente la variante que corresponde al driver:

~/aifs/venv/bin/pip install --index-url https://download.pytorch.org/whl/cu128 "torch==2.7.*"
torch 2.7.1+cu128 | cuda: True
gpu: NVIDIA GeForce RTX 4070 Ti SUPER

Vale la pena verificar siempre esa línea antes de medir nada. Un torch.cuda.is_available() que devuelve False en silencio es la forma más barata de publicar números equivocados.

Paso 3: las condiciones iniciales

El modelo no adivina desde cero: necesita el estado real de la atmósfera. Concretamente dos análisis separados por seis horas, el del momento inicial y el de seis horas antes, para que pueda inferir hacia dónde se mueve todo.

Salen del portal de datos abiertos de ECMWF, gratis y sin cuenta:

from aifs import load_ics
fields, date = load_ics(cache_dir="ic_cache")

La primera vez tardó 255 segundos y dejó 660 MB en caché. Las siguientes cargan en segundos desde el .npz local.

Paso 4: el pronóstico

from aifs import run_forecast
states = run_forecast(fields, date, lead_time=48, num_chunks=16)

num_chunks parte el grafo para que la memoria no explote; subirlo baja el consumo a cambio de algo de velocidad. Cada elemento de states es un estado global, uno cada seis horas.

Paso 5: sacar el dato de una ciudad

Acá hay una sutileza que conviene no pasar por alto. AIFS no entrega una grilla rectangular de latitud y longitud: entrega una grilla gaussiana reducida, un vector de puntos con sus coordenadas. Para una ciudad se busca el punto más cercano, y eso es lo que hay: no es una interpolación.

import numpy as np

def punto_mas_cercano(lats, lons, lat, lon):
    lon = lon % 360
    glon, glat = np.asarray(lons) % 360, np.asarray(lats)
    dlon = np.minimum(np.abs(glon - lon), 360 - np.abs(glon - lon))
    return int(np.argmin(np.hypot(glat - lat, dlon * np.cos(np.radians(lat)))))

idx = punto_mas_cercano(states[0]["latitudes"], states[0]["longitudes"], -33.4489, -70.6693)
for st in states:
    print(st["date"], round(float(np.asarray(st["fields"]["2t"])[idx]) - 273.15, 2), "°C")

Para Santiago, el punto más cercano cayó en −33.583, −70.720: unos quince kilómetros del centro. La temperatura viene en kelvin, de ahí el 273.15.

Medición 1: qué cuesta correrlo

Análisis inicial del 16 de agosto de 2026 a las 00:00 UTC, pronóstico a 48 horas, RTX 4070 Ti SUPER de 16 GB.

Memoria de video, pico5.77 GB
Pronóstico de 48 h108.5 s
Por paso de 6 h13.6 s
Descarga de condiciones iniciales254.6 s (una vez)
Punto de control994 MB
Dónde se va el tiempo de un pronóstico a 48 horassegundos · menos es mejor
  1. Descargar las condiciones iniciales254.6 sMás del doble que el pronóstico completo. El cuello de botella es la red, no la tarjeta.
  2. Pronosticar 48 horas108.5 sOcho pasos de seis horas, con 5.77 GB de memoria de video en el pico.
  3. Un paso de 6 horas13.6 sLa unidad real de trabajo del modelo. Todo lo demás es multiplicarla.

RTX 4070 Ti SUPER, con la atención de PyTorch en lugar de flash-attn. La descarga se paga una sola vez.

Cabe con holgura en una tarjeta de 8 GB. La descarga inicial pesa más que el modelo.

Medición 2: ¿y si no tienes tarjeta?

Repetí exactamente la misma corrida forzando procesador, con CUDA_VISIBLE_DEVICES="", en un Ryzen de 16 hilos.

GPUCPU
Pronóstico de 48 h108.5 s1984.3 s (33 min)
Por paso de 6 h13.6 s248.0 s

18.3 veces más lento. Pero termina, y sin tocar una línea de código.

Lo interesante es comparar los dos resultados punto por punto:

hora UTCGPUCPUdiferencia
16 ago 06:005.775.770.000
16 ago 12:005.155.150.000
16 ago 18:0015.1715.170.000
17 ago 00:0010.4310.430.000
17 ago 06:007.597.590.000
17 ago 12:006.566.55−0.010
17 ago 18:0012.8812.84−0.040
18 ago 00:0010.1010.09−0.010

Los cinco primeros pasos dan la misma temperatura hasta el centésimo de grado. En el viento, que no aparece en esta tabla, la divergencia asoma antes: en el tercer paso ya hay una centésima de metro por segundo de diferencia. La diferencia aparece en el sexto y crece hacia el final. Eso es exactamente lo que se espera de un modelo autoregresivo: cada paso toma como entrada la salida del anterior, así que una discrepancia mínima en el orden de las reducciones de punto flotante se arrastra y se amplifica.

Para este horizonte la diferencia es irrelevante, cuatro centésimas de grado. A diez días, ese mismo mecanismo es el que separa dos pronósticos que empezaron iguales.

Medición 3: contra el pronóstico oficial

Esta es la que importa. Comparo mi corrida casera contra el IFS operativo del propio ECMWF, el modelo físico que corre en su superordenador, consultado por la API de Open-Meteo. Mismo punto, misma hora, temperatura a dos metros.

hora UTCAIFS en mi tarjetaIFS operativodiferencia
16 ago 06:005.776.00−0.23
16 ago 12:005.155.10+0.05
16 ago 18:0015.1715.20−0.03
17 ago 00:0010.439.80+0.63
17 ago 06:007.598.40−0.81
17 ago 12:006.567.90−1.34
17 ago 18:0012.8812.70+0.18
18 ago 00:0010.1010.40−0.30

Error medio absoluto: 0.45 °C.

Menos de medio grado de diferencia contra el modelo operativo de referencia mundial, a dos días, desde una tarjeta que también sirve para jugar. Y ese número carga con la penalización del parche de atención: el envoltorio advierte que la ruta por SDPA no es bit a bit idéntica a los núcleos con los que se entrenó el modelo. Con la implementación original, la diferencia debería ser menor.

Las diferencias se reparten a ambos lados del cero, cinco negativas y tres positivas, así que no hay un sesgo cálido ni frío que valga la pena nombrar con ocho datos. La mayor, 1.34 °C, cae al mediodía del segundo día.

La vida de un número: de 0.91 a 0.45

Ningún número nace bueno. Este pasó por tres estados antes de quedarse quieto, y el recorrido enseña más que el resultado.

Estado 1 · 0.91 °C — el número ingenuo. Corrí el modelo, pedí el pronóstico oficial a una API, resté y publiqué. Parecía razonable: menos de un grado contra el mejor centro meteorológico del mundo. Lo que no hice fue preguntarme qué me estaba devolviendo exactamente esa API.

Estado 2 · 0.61 °C — aparece el suelo. Al revisar la llamada descubrí que Open-Meteo, por defecto, no entrega el punto de grilla crudo: elige una celda terrestre de elevación parecida y encima ajusta por altura con un modelo digital de elevación de 90 metros. Pedí las dos versiones del mismo instante:

elevación que usatemperatura a las 06:00 UTC
por defecto538 m5.4 °C
cell_selection=nearest&elevation=nan450 m6.0 °C

0.60 °C de diferencia, idéntica en las ocho horas. Estaba restando el punto de grilla crudo de AIFS menos un IFS corregido por altura. Dos cosas distintas con el mismo nombre, y una ciudad en un valle rodeado de cordillera es justo donde más se nota.

Estado 3 · 0.45 °C — aparece el reloj. Corregido el punto, los números seguían sin cuadrar: la diferencia entre mi primera consulta y la nueva no era el 0.60 constante que acababa de medir, sino un abanico de +0.3 a −1.9 °C. Eso solo tiene una explicación. El IFS se actualiza cuatro veces al día, y mi consulta original había traído una corrida distinta de la que estaba comparando después.

Es decir que durante todo el estado 1 estuve enfrentando el pronóstico de AIFS, lanzado desde el análisis de las 00 UTC, contra un IFS que podía venir de un análisis posterior, con seis o doce horas más de información sobre la atmósfera. No es que mi modelo saliera mal parado: es que el oficial estaba jugando con ventaja.

qué comparabaMAE
otra corrida del IFS, corregida por altura0.91 °C
corrida actual, corregida por altura0.61 °C
corrida actual, mismo punto de grilla0.45 °C

Estado 4 · lo que sigue abierto. Falta cerrar el reloj del todo: para que la comparación sea estricta hay que fijar la corrida del IFS al mismo análisis inicial que usa AIFS. Mientras el marcador consulte el pronóstico oficial vigente, esa parte sigue siendo aproximada, y lo digo acá en vez de esperar a que alguien lo note.

Ninguno de los tres estados fue un error del modelo. Los tres fueron míos, en cómo pedí el dato con el que lo estaba juzgando. Correr el modelo es la parte fácil; lo que cuesta es asegurarse de que los dos números que restas midan lo mismo, en el mismo lugar y en el mismo momento.

Lo que no hace

Conviene separar dos cosas que es fácil confundir. AIFS Single v2 sí es un modelo operativo de ECMWF: su propia ficha dice que lo corren cuatro veces al día para generar un pronóstico global a quince días. Lo que queda fuera de esa categoría es esta ejecución, con un envoltorio que sustituye el mecanismo de atención por otro y que avisa en la primera línea de que no se use para nada donde haya seguridad de por medio.

Dicho de otro modo: el modelo es el mismo que ECMWF opera profesionalmente; la implementación con la que lo corro en casa, no.

Es determinista, así que da un único futuro y no una distribución de posibles; para eso está aifs-ens-2.0, que es de conjunto. Y hereda el sesgo de suavizado típico de estos modelos: tiende a subestimar los extremos, que es justo lo que más interesa cuando el clima importa de verdad.

Para mirar la evolución general de la atmósfera a unos días, con datos abiertos y sin depender de nadie, funciona notablemente bien.

El código

Todo lo de arriba, empaquetado y con README: github.com/EfrainGaray/marcador-clima. Son dos scripts, pronostico.py para correr el modelo sobre cualquier coordenada y marcador.py para llevar el registro. El marcador en vivo, actualizándose solo, está en /clima.

El juez llega el martes

El pronóstico quedó guardado en JSON con la hora exacta del análisis inicial, antes de que ocurriera nada. El 18 de agosto se puede contrastar contra lo que efectivamente pasó, y esa comparación va a actualizar este post diga lo que diga.

Y no queda en este pronóstico: cada día se guarda uno nuevo, con el oficial al lado, y se rellena la realidad cuando llega. Ese registro acumulado está abierto en /clima. Las primeras horas verificadas ya dan una ventaja clara al modelo oficial, que es exactamente lo que uno esperaría de un sistema que corre en un superordenador; lo interesante será ver cuánta, y si se sostiene, cuando haya meses de datos en vez de horas.

Es la parte que me interesa de todo esto. Puedo elegir el benchmark, puedo elegir el hardware y puedo elegir cómo presentar los números, pero no puedo elegir el clima.

Comentarios

Todavía no hay comentarios. El primero es tuyo.

Se revisa antes de publicarse. El correo no se guarda ni aparece en ninguna parte.