// cheatsheets · 7 módulos · 79 preguntas
Lo que te preguntarían, en corto.
Al final de cada módulo, ocho preguntas de entrevista con la respuesta que darías en un minuto. Esta página está pensada para imprimirse.
// M0
Taller: repo, entorno, datos y tablero
¿Qué es un modelo de lenguaje de ajedrez?
Una red que predice la siguiente jugada dada la secuencia de jugadas anteriores. Se entrena con la misma pérdida que un LLM de texto (siguiente token), pero cada token es una jugada, así que las reglas y la estrategia emergen de predecir bien.
¿Por qué el ajedrez es un buen dominio para aprender IA generativa?
Es cerrado y verificable: un motor decide si una jugada es legal y cuánto vale, así que las recompensas son gratis y objetivas. Los datos son abiertos y CC0 (Lichess), los modelos son pequeños (decenas de millones de parámetros) y la demo se entiende en cinco segundos.
¿Qué diferencia hay entre SAN, UCI, FEN y PGN?
SAN escribe una jugada como en los libros (Nf3) y depende del tablero; UCI escribe origen y destino (g1f3) y no depende de nada; FEN describe una posición completa en una línea; PGN es el fichero de una partida entera con cabeceras y jugadas en SAN.
¿Qué papel tiene Stockfish en este curso?
Es el juez, no el jugador. Con UCI_LimitStrength y UCI_Elo (hacen falta los dos) se convierte en un rival calibrado para estimar Elo; a poca profundidad da recompensas baratas para DPO y GRPO; y sus evaluaciones públicas etiquetan posiciones para el encoder.
¿Por qué las configuraciones usan extra="forbid"?
Para que una clave mal escrita en un YAML falle al cargar, antes de tocar la red o la GPU. Con el comportamiento por defecto de pydantic se ignoraría, el valor por defecto se usaría en silencio y el resultado mediría otra cosa.
¿Qué filtros aplica el recorte de datos y por qué?
Ambos Elo ≥ 1800 (calidad), base de tiempo ≥ 180 s (excluye bullet, donde se juega a ciegas contra el reloj), terminación Normal o Time forfeit (excluye abandonos y trampas) y sin variantes. El mínimo de 20 plies no se aplica aquí sino en M1, tras convertir a UCI, y el manifiesto lo guarda como min_plies_deferred.
¿Para qué sirven el --dry-run y el manifiesto de la descarga?
El --dry-run imprime el mismo plan (la SQL con los filtros) que la descarga ejecutaría, porque los dos salen del mismo código. El manifiesto, escrito al final, guarda los filtros aplicados de verdad, las filas por mes y un sha256 por fichero: es la prueba de qué entró en los datos.
¿Por qué la legalidad la decide el navegador y no el modelo?
Porque el modelo solo propone una distribución sobre tokens; chess.js conoce las reglas y enmascara las jugadas ilegales antes de muestrear, es decir, de sortear la jugada según sus probabilidades (decodificación restringida, la misma técnica que fuerza JSON válido en un LLM). Medir cuántas propone legales sin máscara es, además, una métrica de cuánto ha aprendido las reglas.
¿Para qué sirve MLflow si ya tengo los logs?
Para que cada entrenamiento quede registrado con su configuración completa, el SHA de git del código, sus métricas por paso y sus artefactos, comparable con los demás. Las model cards se generan desde MLflow para que ningún número de la tabla se escriba a mano.
¿Cómo se mide el progreso en cada módulo?
Siempre con la misma tabla: tasa de jugadas legales sin máscara, exactitud top-1 y top-3 en validación, puzles resueltos por dificultad y Elo estimado contra Stockfish limitado con intervalo de confianza. Los criterios de aceptación se escriben antes de medir.
// M1
Del PGN al tensor: datos y tokenización
¿Qué es un token y quién decide qué cuenta como token?
La unidad mínima que el modelo lee y escribe, convertida a un id entero por el tokenizador. Lo decide quien diseña el modelo, no los datos: es la primera hipótesis sobre el dominio ("una jugada es la unidad") y cambia la longitud de las secuencias, lo que el modelo tiene que aprender y el coste de atención, que crece con el cuadrado de la longitud.
¿Qué diferencia hay entre un vocabulario fijo, BPE y tokenización por carácter?
El fijo se enumera sin mirar datos (en Rukh, las 1 968 jugadas UCI posibles más control y Elo: 2 030 tokens). Carácter usa un alfabeto mínimo (35 símbolos) y da secuencias cuatro o cinco veces más largas. BPE aprende fusiones de pares frecuentes desde los caracteres hasta un tamaño objetivo (4 096) y descompone lo raro en trozos, sin token desconocido.
¿Cómo se entrena un BPE en dos frases?
Se parte de los caracteres, se cuenta el par adyacente más frecuente en el corpus, se fusiona en un token nuevo y se repite hasta el tamaño de vocabulario pedido. Tokenizar es aplicar las fusiones en el mismo orden en que se aprendieron; sobre texto UCI, las primeras fusiones reconstruyen casillas y jugadas, y las últimas, trozos de apertura.
¿Para qué sirven los tokens especiales?
Son la interfaz de control: <bos> y <eos> marcan principio y fin, <pad> rellena y lo ignora la pérdida, <mask> es el hueco del encoder de M3, <unk> es una red de seguridad, los de resultado codifican quién ganó y los de Elo (<w1800>, <b1900>) condicionan la partida al nivel de cada jugador.
¿Por qué el Elo va al principio de la secuencia y el resultado al final?
El modelo es causal: solo ve lo anterior. Con el Elo delante, cada jugada se predice condicionada al nivel, y el modelo aprende que después de `<w1900>` vienen unas jugadas y después de `<w2400>` otras. Condicionar elige a quién imitar, no sube el nivel, y un token que no aparece en los datos no aprende nada: los tramos por debajo de 1800 están en el vocabulario pero el recorte no los contiene. El resultado, en cambio, va al final: se predice desde las jugadas y no contamina la generación.
¿Qué es ignore_index y qué pasa si lo olvidas?
Un parámetro de la entropía cruzada (CrossEntropyLoss(ignore_index=0)) que hace que las posiciones cuyo objetivo es <pad> no contribuyan a la pérdida ni al gradiente. Sin él, el modelo aprende que después de cualquier cosa viene <pad> y lo predice con confianza.
¿Qué es el empaquetado en flujo y qué alternativa descartamos?
Concatenar todas las partidas codificadas en un único array uint16 y servir ventanas de 200 tokens que empiezan en un <bos>, de modo que casi no hay padding y cada ejemplo lleva los tokens de control. Descartamos las ventanas en posiciones arbitrarias (estilo nanoGPT) porque perderían el Elo del principio, y las filas de una partida con padding porque desperdician más de la mitad del cálculo.
¿Por qué partimos los datos por mes y los puzles por PuzzleId?
Para evitar fugas: un jugador activo juega cientos de partidas con el mismo repertorio, y un split al azar pone sus partidas en los dos lados e infla la exactitud. Entrenar con enero y validar con febrero reproduce la situación real (partidas futuras). Los puzles se parten por su id con semilla 42 para que el conjunto de test sea reproducible y no cambie entre ejecuciones. Y el tamaño de lo apartado se decide por el error típico de la medida que sostiene, no por un porcentaje.
¿Qué es un memmap y por qué lo usa el dataloader?
Un fichero mapeado a memoria: np.load(mmap_mode='r') no lee el array, deja que el sistema operativo traiga las páginas cuando se tocan. Los 480 MB de tokens de un mes arrancan al instante, varios workers comparten las mismas páginas físicas y nadie copia el fichero en RAM.
¿De dónde salen los pares de preferencia para DPO sin ejecutar Stockfish?
Del dataset público de evaluaciones de Lichess, que tiene varias líneas principales por posición ordenadas de mejor a peor. La primera jugada de la mejor línea es la elegida y la primera jugada de una línea peor en 100 centipeones o más es la rechazada: un par por posición, equilibrado por fase y verificado legal con python-chess.
// M2
El decoder: un GPT que juega
Explica la atención en tres frases.
Cada posición proyecta su vector en una consulta (Q), una clave (K) y un valor (V). El producto escalar entre la consulta de una posición y las claves de todas las demás, dividido por la raíz de la dimensión de la cabeza y pasado por softmax, da un reparto de pesos que suma 1. La salida es la media de los valores ponderada por esos pesos: softmax(QKᵀ/√d)·V. La escala 1/√d existe porque el producto escalar de d dimensiones tiene varianza d y sin ella el softmax saturaría y el gradiente se anularía.
¿Qué es la máscara causal y qué pasa si falta?
Poner a menos infinito, antes del softmax, el peso de toda clave posterior a la consulta, de modo que la matriz de atención queda triangular inferior. Sin ella la posición t puede leer el token t+1, que es justo el que tiene que predecir: la tarea pasa de predecir a copiar, la pérdida se desploma y el top-1 se dispara, pero al generar (donde no hay futuro) el modelo juega al azar. Se detecta porque la legalidad sin máscara se hunde, y se verifica con un test: cambiar un token futuro no debe mover los logits pasados ni un decimal.
¿Por qué pre-norm y no post-norm?
Pre-norm pone el LayerNorm dentro de la rama, antes de la subcapa (x = x + attn(ln(x))), así que la conexión residual queda libre: la derivada de cada bloque es la identidad más la de la subcapa y el gradiente llega íntegro desde la pérdida hasta el embedding. Post-norm normaliza después de la suma, mete la normalización en el camino residual y hace que una pila profunda solo entrene con warmup largo e inicialización muy cuidada. Es un cambio de dos caracteres que convierte doce capas entrenables con suerte en doce capas entrenables.
¿Qué hace el warmup y qué pasa sin él?
Sube la tasa de aprendizaje linealmente desde cero durante los primeros pasos (1 000 en rukh-small) antes de empezar el coseno. En los primeros pasos, Adam estima el segundo momento con un puñado de gradientes y su paso normalizado puede mover un parámetro tanto como la tasa entera; sobre una red recién inicializada eso satura la atención y el modelo cae en predecir siempre las jugadas más frecuentes. No suele explotar con un NaN: se queda en una pérdida mediocre que ya no baja, que es peor porque no se nota.
Temperatura y top-k: ¿en qué se diferencian?
La temperatura divide los logits antes del softmax y reescala toda la distribución: por debajo de 1 la concentra (con 0 es el argmax), por encima la aplana. El top-k no reescala, recorta: deja solo los k tokens de mayor probabilidad y pone el resto a cero. Son complementarios, no alternativas. Con 2 030 jugadas, la cola de mil opciones improbables suma un porcentaje apreciable de disparates; el top-k la elimina y la temperatura decide cuánto arriesga el modelo dentro de lo que queda. Rukh usa temperatura 0,6 y k = 20, y lo paga: en small, jugar a 0,6 en vez de casi a cero cuesta unos 180 puntos de Elo.
¿Qué mide la legalidad sin máscara, por qué hay dos cifras y por qué no vale medirla con máscara?
Mide el porcentaje de jugadas legales que propone el modelo cuando se le deja emitir el token que quiera, sobre 1 000 posiciones de validación. Se publica dos veces: legality_argmax toma el token más probable (sin temperatura ni top-k) y legality_sampled saca uno como lo saca la demo (temperatura 0,6 y top-k 20), que es siempre la menor de las dos porque muestrear mete cola. El listón de ≥ 99 % para rukh-small es el argmax, porque habla de lo que saben los pesos y no de un ajuste del muestreador; la muestreada es lo que experimenta un jugador y va al lado con su definición. Con la máscara de legalidad puesta el 100 % está garantizado por construcción, así que estarías midiendo a python-chess y no al modelo.
¿Por qué bf16 y no fp16?
Los dos ocupan 16 bits, pero reparten distinto. fp16 tiene 5 bits de exponente y 10 de mantisa: mucha precisión y poco rango, con el mínimo normal en torno a 6e-5, justo donde viven los gradientes de una red profunda, así que se van a cero y hace falta un GradScaler con escalado dinámico de la pérdida. bf16 tiene 8 bits de exponente (el mismo rango que fp32) y 7 de mantisa: menos precisión por número y nada que se desborde por abajo, así que entrena sin escalar nada. En una GPU moderna es nativo, así que la simplicidad sale gratis.
¿Qué es un checkpoint y qué tiene que llevar dentro?
Una fotografía del entrenamiento en disco. No basta con los pesos: lleva el paso alcanzado, el estado del optimizador (los momentos de Adam), el estado de los generadores aleatorios, la configuración del modelo y de la tirada y la procedencia (hash del vocabulario, hash del manifiesto de datos y SHA de git). Con eso se puede reanudar exactamente donde se cortó, evaluar el modelo meses después y decir con qué datos exactos se entrenó. Rukh escribe uno cada 1 000 pasos más best.pt por pérdida de validación.
¿Qué es la perplejidad?
La exponencial de la entropía cruzada media. Se lee como entre cuántas opciones equiprobables duda el modelo: con un vocabulario de 2 030 jugadas, un modelo sin entrenar tiene perplejidad 2 030 y pérdida ln(2030) ≈ 7,6; una pérdida de 3,0 es perplejidad 20, es decir, acierta como si eligiera al azar entre veinte jugadas. Como en una posición típica hay unas treinta legales, esa cifra ya implica bastante aprendizaje. No es una métrica aparte: es la misma pérdida en otra escala.
¿Qué significa un intervalo de confianza del Elo y por qué importa?
El Elo estimado es el valor que mejor explica los resultados contra rivales de fuerza conocida según la fórmula logística P = 1/(1+10^((rival−modelo)/400)); el intervalo del 95 % dice cuánto podría moverse ese valor si se repitieran las partidas, y se calcula por bootstrap remuestreando los resultados y reajustando mil veces. La suite juega 20 partidas contra cada uno de ocho escalones, 160 en total: a un 50 % de rendimiento la desviación típica sería de unos 27 puntos (±54), y el bootstrap sobre las partidas reales de small da ±68 (1359, IC 1293-1429), porque contra la mayoría de escalones el modelo está lejos del 50 % y cada partida informa menos. Es decir: una mejora de 10, 30 o 50 puntos de Elo medida con una escalera de 160 partidas no es una mejora, es ruido, y presumir de ella es el error clásico de los benchmarks.
¿Cuándo se escala la red? Tokens únicos por parámetro.
Antes de subir de talla, divide los tokens únicos del corpus (los de meta.json, no los vistos por el bucle: repetir material no lo convierte en material nuevo) entre los parámetros. small v1 iba a 240 M / 39 M = 6,2, con 4,3 pasadas sobre el mismo corpus, y su hueco train/val crecía (+0,060) mientras el top-1 se aplanaba: la firma de material agotado, y triplicar la red (medium v1, mismos datos) solo compró 84 Elo con los intervalos solapados. Devolver el mes de validación al corpus (471 M, 12,1 por parámetro) bajó la pérdida del mismo small de 1,5197 a 1,4703 y batió a medium. medium-v4, con 1 681 M de tokens únicos (14,6 por parámetro, 1,46 pasadas), da 1504 de Elo y 99,80 % de legalidad. Hueco creciendo y métrica plana: faltan datos; hueco estable y métrica subiendo: hay margen en pasos o en capacidad.
// M3
El encoder: entender la posición
¿En qué se diferencia un encoder bidireccional de un decoder causal?
En una línea: la máscara. El decoder pasa `is_causal=True` a la atención y cada posición solo ve hacia atrás, que es lo que hace posible generar de izquierda a derecha sin hacer trampa; el encoder pasa `causal=False` y cada posición ve toda la secuencia, incluidas las jugadas posteriores. Todo lo demás —embeddings, multi-cabeza, bloques pre-norm, LayerNorm final— es el mismo código: en Rukh, `PositionEncoder` y `MoveDecoder` construyen sus bloques desde el mismo `rukh.models.layers`. Lo que se gana es un contexto completo para representar; lo que se pierde es la capacidad de generar, porque la tarea de predecir el siguiente token con el siguiente token a la vista es trivial.
¿Qué es el MLM y por qué 80/10/10?
Masked language modeling: se esconde el 15 % de los tokens y el modelo los reconstruye desde los dos lados. De los tokens elegidos, el 80 % se sustituye por `<mask>`, el 10 % por otro token al azar y el 10 % se deja como está. Si todos fueran `<mask>`, el modelo solo vería ese símbolo durante el preentrenamiento y nunca durante el afinado, así que aprendería una representación que únicamente funciona cuando falta algo. El 10 % aleatorio le obliga a desconfiar de lo que lee (un token que *está* puede ser falso) y el 10 % intacto le obliga a representar todas las posiciones, no solo las marcadas. En Rukh se aplica a jugadas y los tokens de control (`<bos>`, Elo, resultado, `<eos>`) nunca se tapan: son la condición, no la señal.
¿Qué es el pooling y cuándo usar CLS o media?
Reducir los T vectores que devuelve el encoder a uno solo que represente la secuencia. `cls` toma la posición 0, un token que no aporta contenido y cuya única función es acumular el resumen que la atención le traiga; `mean` promedia los vectores de los tokens reales. La media es más robusta y es el defecto de Rukh, pero tiene una condición: hay que ignorar el relleno, porque promediar los vectores de los `<pad>` diluye la representación en una cantidad que depende de cuánto se rellenó ese lote, es decir, el vector de una misma posición cambiaría según con quién le toque viajar. `PositionEncoder.pool` lee la máscara de `idx` cuando no se le pasa una.
¿Qué diferencia hay entre un probe lineal y un fine-tuning completo, y qué mide cada uno?
El probe congela el encoder y entrena solo una capa lineal encima del vector agrupado: mide si la información ya estaba en la representación, porque una capa lineal no puede aprenderla por su cuenta. El fine-tuning completo mueve todos los pesos: mide lo mejor que puede hacer esa arquitectura con esas etiquetas, y suele ganar, pero necesita más etiquetas, se sobreajusta antes y ya no dice nada sobre el preentrenamiento. Entre los dos está `last-n`, que descongela las últimas capas y la norma final. Y el hito midió lo contrario de lo esperable: `last-n` sacó el mejor error de valor (0,1180) por delante de `full` (0,1354) y del probe (0,1429), porque descongelarlo todo con 438 093 etiquetas aleja las capas de abajo de la representación que dejó el preentrenamiento; `full` solo gana en la exactitud del resultado. Es una tirada por modo, así que la ordenación es sugerente y harían falta varias semillas para establecerla. En Rukh son los tres modos de `rukh train heads --mode probe|last-n|full`, y un test comprueba que en `probe` los pesos del encoder salen idénticos bit a bit.
¿Qué mide el F1, cuándo no sirve la exactitud y dónde se elige el umbral?
F1 es la media armónica de precisión (de lo que marqué, cuánto era de verdad) y exhaustividad (de lo que había, cuánto marqué). La exactitud deja de servir en cuanto una clase es rara, y en M3 lo es: los errores son el **3,72 %** de las filas, así que un detector que conteste siempre «no hay error» saca **96,3 %** sin mirar el tablero y la cabeza real saca 96,78 %. Peor todavía, el F1 en un umbral arbitrario tampoco mide la representación: las probabilidades de esta cabeza van de 0,0020 a 0,3102, así que en el umbral de fábrica de 0,5 no se dispara nunca y su F1 es **exactamente 0,0000**. El mismo modelo tiene **0,740 de ROC AUC**. El procedimiento honesto es partir las filas etiquetadas en dos por `game_id`, elegir el umbral en la mitad `tune` (aquí 0,0663) y publicar el F1 medido en la mitad `score` (aquí 0,180), que no ha visto ningún umbral. Elegir el umbral en las filas que luego se puntúan convierte la estimación en un récord personal.
¿Qué es una fuga de datos y por qué aquí el split va por partida?
Cualquier camino por el que información de validación llega al entrenamiento. Nunca falla con un error: los números salen mejores y el problema se descubre en producción. En M3 el peligro concreto es partir por posición: dos posiciones consecutivas de la misma partida se diferencian en una jugada, así que la del ply 30 en entrenamiento y la del 31 en validación es casi la misma posición y un modelo que memoriza puntúa como uno que entiende. Rukh parte por `game_id` con una función pura de CRC-32 sobre el id y la semilla, y un test comprueba que ningún `game_id` está en los dos lados.
¿Por qué se correlaciona el valor con el `cp` de Stockfish y por qué dos correlaciones?
Porque `cp` es la única referencia objetiva de «cuánto vale esta posición» que hay a escala, y porque una regresión necesita una medida de calidad que no dependa de un umbral. Se correlaciona contra `tanh(cp / 400)`, la escala acotada con la que se entrena la cabeza, y nunca contra el `cp` en bruto: un mate vale ±9 999 centipeones y media docena de filas así decidirían el Pearson del conjunto entero. Se publican las dos correlaciones: Pearson mide si los valores se alinean en una recta, Spearman solo si el orden coincide. Discrepan exactamente cuando el modelo tiene bien el orden y mal la escala, que es lo que le hace un `tanh` acotado, así que dar solo una oculta la mitad del diagnóstico. `GOAL.md` pide ≥ 0,80 sobre Spearman y **el checkpoint publicado (`last-n`, jugadas) se queda en 0,520, la entrada por casillas en 0,422 y la pérdida con término de orden sube a 0,6665: criterio no cumplido**, publicado sin cumplir y con el diagnóstico al lado.
¿Qué es un baseline y por qué el de M3 es deliberadamente tonto?
El rival contra el que se mide el modelo para saber si su número significa algo. La de M3 cuenta material (peón 1, caballo y alfil 3, torre 5, dama 9), le suma movilidad y llama error a toda jugada que pierda un punto neto tras la captura más rentable del rival a un ply. Es tonto a propósito: un baseline que ya entendiera los sacrificios no sería un suelo, sería un competidor, y el margen dejaría de decir «el modelo aprendió algo más que contar piezas». Lo esencial es medirla exactamente igual que al modelo: mismas filas, mismas etiquetas, misma definición de acierto. Medido: la heurística saca 8,9 % de F1 y el encoder publicado 18,0 %, **+9,2 puntos**, por encima de los cinco que pide `GOAL.md`. Y la comparación no es simétrica en dos sentidos opuestos que el informe dice en voz alta: la heurística ve la posición anterior y la jugada, que el encoder no ve; y la heurística es una regla sin umbral que ajustar, mientras que el modelo se mide en su mejor punto de operación.
¿Qué aporta la curva por número de etiquetas?
Dice cuántas etiquetas hacen falta de verdad. Se entrena la misma receta con el 10, 25, 50 y 100 % de las etiquetas de entrenamiento y se dibuja la métrica contra el número de filas. Si la curva se aplana pronto, etiquetar más es tirar dinero y el cuello de botella está en el modelo o en la tarea; si sigue subiendo al 100 %, etiquetar más es la inversión más rentable que queda. Los subconjuntos son **anidados** (el 10 % está dentro del 25 %, y este dentro del 50 %) porque con muestras independientes un bache a mitad de la curva podría ser mala suerte del sorteo y estaríamos midiendo ruido de muestreo en vez del valor de los datos. Medido en M3: plana desde el 25 % (0,1217 / 0,1192 / 0,1210 / 0,1215 de error de valor al 10, 25, 50 y 100 %), así que más allá de unas cien mil etiquetas más evaluaciones de Stockfish no compran nada. Y la dispersión entre esos cuatro puntos es la misma que entre dos tiradas idénticas, que es la segunda lección: antes de explicar una diferencia pequeña, mide cuánto se mueve tu montaje cuando no cambias nada.
¿En qué se diferencia este modelo del de M2?
En la máscara, en la tarea y en para qué sirve. El de M2 es causal, se entrena a predecir la siguiente jugada y genera: es lo que juega en la demo. El de M3 es bidireccional, se entrena tapando jugadas en medio de la partida y no genera nada: produce un vector por posición del que salen el valor, la detección de errores y el resultado. También es más pequeño (8 capas, d=384, 15 052 800 parámetros frente a los 12 capas, d=512 y 38 971 392 del `small` de M2; el `medium` que afina M4 tiene 115 M) y usa `ignore_index=-100` en vez del `0` del decoder, porque en la entrada por casillas el `0` es un token que sí se puede pedir que prediga. En la demo conviven: el decoder elige jugada, el encoder pinta la barra, y cada uno en su propio worker.
¿Qué es un punto de operación y qué métricas no dependen de él?
El umbral a partir del cual una probabilidad se convierte en una decisión («p ≥ 0,0663 es error»). Precisión, exhaustividad y F1 dependen de él: moverlo cambia las tres sin tocar un peso, y multiplicar todas las probabilidades por tres convierte un F1 de 0,0000 en 0,5 en otro número sin que el modelo sepa nada nuevo. ROC AUC (probabilidad de que un error al azar puntúe más que una jugada tranquila al azar; 0,5 es una moneda) y precisión media (área bajo precisión-exhaustividad; su referencia es la tasa base de la mitad score, 0,039) solo miran el orden y no se mueven con ningún umbral. En M3 el checkpoint publicado tiene 0,740 y 0,112; el de casillas, 0,696 y 0,091, y cada uno tiene su propio umbral ajustado (0,0663 y 0,04459), que no se pueden intercambiar.
// M4
Fine-tuning e instrucción
¿Qué diferencia hay entre preentrenar y afinar?
Menos de la que sugiere el vocabulario. Es el mismo bucle, la misma pérdida y el mismo optimizador; lo único que cambia es de dónde salen los lotes y, normalmente, la tasa de aprendizaje. `medium-elo` se entrena con entropía cruzada sobre el siguiente token igual que `medium-v4`, con otros datos y con `lr` 1e-4 en vez de 4,5e-4. Las dos consecuencias prácticas son las que importan: el afinado hereda todo lo que el preentrenamiento aprendió y puede deshacer cosas que nadie le pidió deshacer (olvido catastrófico), y la pérdida de validación deja de ser el objetivo porque mide la distribución de la que te estás yendo. En Rukh el afinado por Elo subió la pérdida de validación de 1,3733 a 1,3987 y bajó el top-1 de 54,73 % a 54,66 %: eso es el precio, no el fallo.
¿Por qué no funcionaba el condicionamiento por Elo y cómo se descubrió?
Porque la mitad baja del eje nunca se había entrenado. `configs/data/lichess-2025-01-02.yaml` fija `min_elo: 1800` aplicado a los dos jugadores, y ese es el filtro con el que se descargó todo el corpus del proyecto: los 5,9 M de partidas base, los 13,1 M de la Elite Database, los 19 M de `tokens-v4`. Así que los doce tokens por debajo de 1800 —`<w0600>` a `<w1700>`— nunca recibieron un gradiente y su embedding seguía en la inicialización. Pedir «juega como 1500» era enseñarle ruido. No se descubrió leyendo el modelo sino contando: el segundo token de cada partida empaquetada *es* el Elo de las blancas, así que un histograma de tres líneas sobre `tokens.npy` lo enseña. La regla que deja: antes de concluir que un mecanismo no funciona, comprueba cuántos ejemplos ha visto.
¿Qué es LoRA y por qué su rango es a la vez su ventaja y su límite?
En vez de mover una matriz de pesos `W`, se aprende una corrección `B·A` de rango `r` y se suma. Sobre `medium`, una proyección de consulta es 768 × 768 = 589 824 números y su adaptador con `r = 8` son 12 288: el 2,08 %. Sobre consulta y valor en las dieciséis capas, 393 216 parámetros de 115 120 128 —el 0,34 %, 1,6 MB en `float32`— y los pesos base no se mueven, así que todos los adaptadores comparten una sola copia. El límite es el mismo hecho: todo lo que el adaptador aprenda tiene que pasar por ocho dimensiones por matriz. Sirve para inclinar un modelo hacia un estilo, un dominio o un formato; no sirve para enseñarle algo que no sabe.
¿Por qué `B` empieza en cero y qué pasaría si no?
Porque así el modelo adaptado **es** el modelo base en el paso 0: `B·A = 0`, la salida es idéntica y no ha cambiado nada hasta que llega el primer gradiente. Cualquier otra inicialización movería los pesos antes de aprender nada y toda comparación contra la base empezaría desde un modelo que ya es distinto —con lo que «lo que hizo el adaptador» incluiría el sorteo de la inicialización. `A` sí se sortea (Kaiming uniforme, como en el paper): si las dos fueran cero el gradiente del producto sería cero y el adaptador no arrancaría nunca.
¿Cómo compruebas que la LoRA que escribiste es LoRA de verdad?
Poniéndola al lado de la implementación de referencia sobre los mismos pesos. Un `alpha` en el sitio equivocado, `A` y `B` intercambiadas o una inicialización distinta dan un modelo que entrena, converge y produce números creíbles, y ninguna da LoRA. En Rukh se montan dos modelos sobre los mismos pesos base —uno con `apply_lora`, otro con `peft.get_peft_model`—, se copia nuestra `A` en la suya para que la comparación sea de las matemáticas y no de dos sorteos, y los dos dan pasos de SGD sobre el mismo lote comparando las pérdidas **paso a paso** con tolerancia 1e-5. Detalle: la comparación se hace sobre `attn.proj` y no sobre `q`/`v`, porque `peft` no puede expresar lo mismo que nosotros sobre una `qkv` fusionada (con `target_modules=["qkv"]` da una sola pareja `A`/`B` de 2304 filas; nosotros damos una por rango).
«Monótono» y «separado»: ¿por qué son dos afirmaciones y no una?
Porque una es barata y la otra es la que vale. Monótono quiere decir que las estimaciones puntuales suben con la condición, y con tres números y ruido eso sale por azar una vez de cada seis. Separado quiere decir que los intervalos de confianza consecutivos no se solapan, y eso ya no pasa por casualidad. El proyecto se quemó con esto: una prueba de 32 partidas dio 0,703 contra 0,609 a favor de `<w2600>` y pareció una victoria del condicionamiento; con 160 partidas se cayó (1058 frente a 1095, intervalos casi superpuestos) y hubo que retirar la conclusión. Por eso `rukh eval sweep` imprime los dos veredictos por separado y el criterio de `GOAL.md` se lee sobre el segundo. Medido en M4: **ninguna de las dos**. Las estimaciones no son monótonas (1425, 1549, 1498, 1538, 1606, 1644), y de los cinco pares contiguos solo uno está separado, y por los pelos: 1200→1500 (1479 frente a 1481). Ningún par de los que nombra el criterio lo está. Y aparece una tercera pregunta que solo existe cuando la segunda sale que no: ¿faltan partidas o no hay diferencia? Entre `<w1500>` y `<w2000>` harían falta 24 420 partidas por condición, así que no faltan partidas.
Si el modelo condicionado a 1200 juega peor, ¿cómo sabes que juega peor y no que se ha roto?
Midiendo la legalidad sin máscara por condición. Hay dos maneras muy distintas de empeorar: elegir jugadas legales peores —que es lo que hace un humano débil— o proponer jugadas que no existen —que es lo que hace un modelo al que se le ha deshecho la comprensión del tablero. La segunda sería un dial roto con aspecto de funcionar, porque la máscara de legalidad de la demo esconde las jugadas ilegales y el jugador solo vería un rival más flojo. Son dos columnas distintas del barrido precisamente para que no se confundan. Y la respuesta medida es la tercera posibilidad, que era la que no estaba en la lista: **ni una cosa ni la otra**. A `<w1200>` el modelo afinado escribe **cero** jugadas ilegales de mil —el base escribía cuatro— y su Elo es el mismo que el del base. No se rompió y tampoco juega peor de forma medible: lo que cambió es el repertorio, 1,26 bits menos de entropía en la primera jugada.
¿Qué demuestra hacer QLoRA de un modelo de 0,6 B en una GPU de 32 GB?
De la cuantización, nada: el modelo en bf16 son 1,2 GB y no hay memoria que ahorrar, que es la única ventaja de QLoRA. Lo honesto es intentarlo, medir el pico de memoria y decir cuál de las dos cosas pasó, en vez de fingir que se ha demostrado una técnica cuya razón de ser no aplica a esa escala. Lo que sí demuestra el experimento es otra cosa: si merecía la pena escribir un modelo propio. Y para que esa respuesta valga, las dos mitades tienen que diferenciarse en **una** sola cosa: mismas partidas, mismas métricas, mismo harness, y la representación como única variable.
¿Por qué `best.pt` es una trampa en un afinado?
Porque significa «menor pérdida de validación», y en un afinado que cambia de corpus a propósito la pérdida de validación sube desde el principio. En `medium-elo` `best.pt` se quedó congelado en el **paso 200** —un modelo casi sin tocar— mientras el resultado real era el paso 3 800. Quien evaluara ese fichero después estaría midiendo los pesos equivocados y los números saldrían perfectamente plausibles, que es la clase peligrosa de error: no hay excepción que lo delate. El bucle ahora escribe un aviso cuando hay `init_from` y `best.pt` no es el último paso, nombrando el checkpoint que sí es el resultado.
¿Por qué un adaptador se publica en su propio repositorio y no como un modelo?
Porque no es un modelo. Un repositorio de modelo de este proyecto lleva pesos, tres exportaciones ONNX, un vocabulario y una card cuyos números salen de `rukh eval`; un adaptador son dos matrices por proyección adaptada —1,6 MB frente a los 460 MB de los pesos en `float32`— y por sí solo no hace absolutamente nada: sin el checkpoint base exacto es un montón de números sin modelo que corregir. Por eso la card empieza por el modelo base y el comando que lo carga, y las métricas que publica son las que dicen qué **cambió** el adaptador (el reparto de primeras jugadas que movió) y qué **costó** (legalidad, top-1 y puzles medidos en la base y en la base con el adaptador, con la misma suite). Lo que no se ha medido, como el Elo del modelo adaptado, la card lo dice como «no medido». Publicar un adaptador con el Elo del modelo base sería publicar el número de otro.
¿Cómo se cambia de estilo en el navegador sin descargar otro modelo?
Sacando el adaptador de los pesos y metiéndolo en el grafo. Fundir una LoRA y exportar el ONNX cuesta 231 MB por estilo en `medium`, o sea 462 MB por dos estilos para mover 1,6 MB de corrección: tira por tierra lo único que compra el método. La alternativa es exportar el decoder con `A` y `B` como **entradas** del grafo, apiladas por capas en dos tensores `(capas, rangos, r, d)` y `(capas, rangos, d, r)`, y que el navegador suba 1,6 MB por cada cambio de estilo. Tres propiedades lo hacen honesto y las tres se prueban: con ceros el grafo **es** el modelo base exactamente —por eso el fichero adaptable sustituye al ordinario en vez de sumarse a él, y «sin estilo» no es otro modelo—; con otro adaptador la respuesta cambia sin tocar el fichero; y la paridad contra PyTorch con el mismo adaptador cargado se mide sobre posiciones reales. El factor `alpha/r` vive en el **fichero**, no en el grafo, de modo que un adaptador de cualquier rango es correcto sin que nadie tenga que cuadrar dos números a mano. Lo que no admite es un adaptador cuyos rangos no midan todos lo mismo: los factores viajan como un tensor cada uno, así que entra `q`, `k` y `v` sobre el `qkv` fusionado y no entra el MLP, cuyo `fc` es cuatro veces más ancho.
¿Qué cambia un afinado y qué no?
Cambia el **comportamiento** y no cambia la **competencia**, y M4 lo mide cinco veces por caminos distintos. Del lado del comportamiento: un adaptador de 1,6 MB —el 0,34 % del modelo— sube `1. e4` del 59,64 % al **99,85 %** sin costar una décima de legalidad, de top-1 ni de puzles; y la cabecera de Elo ordena el repertorio en las **seis** condiciones (entropía de la primera jugada de 1,596 a 1,992 bits) sin jugar una sola partida. Del lado de la competencia: ni el afinado condicionado (1558) ni el de maestros (1583) separan su intervalo del modelo del que salieron (1504), los puzles son planos en todas las condiciones (37,0 – 38,2 %) y el modelo **sin** afinar recorre 161 Elo por el mismo eje usando cabeceras que nunca entrenó. El mecanismo explica las dos mitades: predecir el siguiente token sobre partidas humanas enseña **qué se juega** a cada nivel y nunca premia **calcular mejor**. Mover la competencia necesita otra herramienta —recompensas, DPO, GRPO— y ese es M5.
// M5
Alineamiento con recompensas verificables
¿Qué cambia el alineamiento que no cambiaba el afinado?
De dónde sale la señal. Preentrenar y afinar usan la misma pérdida —entropía cruzada sobre el siguiente token— y toda su información viene de partidas que alguien jugó: enseñan **qué se juega**, nunca premian **calcular mejor**. El alineamiento cambia el objetivo: la señal pasa a ser «de estas dos jugadas, esta es mejor» (un par de preferencia) o «esta jugada vale 0,84» (una recompensa). Ninguna de las dos frases aparece en ninguna partida, y las dos se pueden construir. Por eso M4 terminó midiendo que afinar cambia el comportamiento y no la competencia, y por eso este módulo existe. Es el mismo salto que convierte un LLM base en un asistente: el RLHF.
¿Cuánto más barato es un enfrentamiento directo, y de dónde sale ese número?
Unas cuatro veces, y sale de dos fórmulas de potencia. Distinguir dos proporciones que difieren en Δp pide `n > z²·2p(1−p)/(Δp)²` partidas **en cada lado** (el 2 es la varianza de la resta); detectar una ventaja en un enfrentamiento pide `n > z²·p(1−p)/(p−0,5)²` **en total**. Para +50 Elo son 375 partidas por lado en la escalera, 750 en total, frente a 185 de frente, y eso sin contar el ruido propio de la escalera. El script del lab imprime `8.1x` porque pone un 2 donde la desviación típica de la resta pide √2, y al elevarlo al cuadrado cuenta dos veces la varianza. Y se usa al revés, que es como se usa de verdad: con 200 partidas la diferencia más pequeña visible es 48 Elo, con 400 es 34, con 50 es 95. `labs/m5/games_needed_match.py` imprime la tabla antes de que se juegue nada.
¿Qué es un control de instrumento y qué encontró el de M5?
Medir algo cuya respuesta ya sabes. Un modelo contra una copia exacta de sí mismo tiene que dar 0,5000 clavado; salió **0,975 con 999 jugadas ilegales**. Eran dos errores en `infer/game.py`: la partida empezaba desde el tablero con el libro de aperturas puesto pero esas jugadas nunca se le reproducían al jugador, y al rival —si guardaba estado— no se le contaba ninguna jugada, porque `Opponent` no tiene `observe` y nadie preguntaba si lo tenía. Tras el arreglo, 0,5000 exacto. Costó un minuto de máquina y evitó publicar un +40 que habría sido un artefacto.
¿Qué promete Bradley-Terry y qué no?
Promete convertir preferencias en puntuaciones: `P(a gana a b) = σ(r(a) − r(b))`, así que la pérdida es `−log σ(r_chosen − r_rejected)`. Lo que **no** promete es una escala. En la fórmula solo aparecen diferencias, así que sumar cien a todas las puntuaciones no cambia nada: el «0,8» de un entrenamiento y el «1,4» de otro son dos reglas sin origen y compararlas no significa nada. Hay un test que lo fija, porque un modelo que se rompiera con un desplazamiento constante no lo notaría nadie aguas abajo.
¿Por qué DPO no necesita reward model?
Porque su referencia es **implícita**: una copia congelada de los pesos de partida, que no se entrena ni se publica. DPO parte de escribir el óptimo del refuerzo con preferencias en función de la política y de esa referencia, y entrenar la política directamente sobre los pares. Se ahorran un entrenamiento y una red de valor. `β` mide cuánto tira esa referencia: con un `β` grande basta alejarse poco para que la pérdida quede satisfecha. Cumple el papel de la penalización KL del RLHF clásico y de GRPO. Rukh entrena con `β` 0,1 y no ha medido otros valores.
¿Qué mira uno en un entrenamiento DPO: el margen o el acierto?
El acierto. El margen puede crecer con la ordenación todavía mal, porque dice «`chosen` gana a `rejected`» y no dice «`chosen` es buena». De ahí también `nll_weight`: un poco de pérdida de siguiente token sobre la jugada preferida ancla la política, porque DPO puro puede subir el margen mientras el modelo se olvida de jugar.
¿Qué diferencia hay entre un par on-policy y uno off-policy, y por qué importa?
De dónde salen las dos jugadas. Off-policy: de otro sitio —las evaluaciones multi-PV de Lichess—, y dicen qué jugada es mejor tanto si el modelo iba a considerarla como si no. On-policy: del propio modelo, muestreando candidatas en la posición y puntuándolas con el motor. Importa porque **un modelo solo pierde partidas con las jugadas que juega**. En Rukh los dos conjuntos salieron muy distintos: los on-policy tienen un 2,9 % de pares de mate frente al 29 % de los off-policy, así que no son dos fuentes de lo mismo, son dos distribuciones de dificultad.
¿Qué hace GRPO que PPO hace más caro?
El baseline. Para saber si una recompensa fue buena hace falta algo contra lo que compararla; PPO entrena una red de valor entera para estimarla. GRPO hace que la política proponga un **grupo** de respuestas a la misma entrada y usa la media del grupo, que ya estaba pagada. La ventaja de cada candidata es `(r − media) / (desviación + ε)`. Sin red de valor, sin segundo entrenamiento.
¿Qué hace que una recompensa sea «verificable» y qué no le da eso?
Que la calcula un programa y se puede comprobar, en vez de estimarla un modelo entrenado con opiniones. El ajedrez es de los pocos sitios donde se puede escribir entera: legalidad, mate, repetición y la evaluación del motor. Lo que gana es que no se equivoca como se equivoca un modelo aprendido. Lo que **no** gana es inmunidad al reward hacking: sigue siendo un diseño humano, y por tanto exactamente especificable y exactamente hackeable.
¿Por qué la legalidad es una puerta y no un término de la suma?
Porque un término se puede comprar. `+2 si es legal` suena a preferencia fuerte y es un **precio**: cualquier otro término que valga más de dos lo paga. Con una calidad sin tope, ese precio lo alcanza cualquier posición de más de dos peones. Y hay una trampa peor que Rukh cometió: la puerta estaba en `0,0`, que cae **dentro** del intervalo legal `[−0,25, 1,25]`, así que una jugada imposible puntuaba por encima de una legal que se mete en repetición. Una puerta que lo que separa puede saltarse no es una puerta: el valor tiene que derivarse del mínimo legal, no escribirse como constante.
¿Dónde vive el reward hacking, si no es en qué jugada gana?
En la forma del grupo. La galería de M5 midió seis recompensas —la sana y cinco rotas— sobre el mismo grupo de candidatas y las seis **coronan la misma jugada** en las tres posiciones: todas son monótonas en la evaluación del motor, así que el máximo no se mueve. El daño está en lo que consume el optimizador, que no es la recompensa sino `(r − media)/desviación`. Sin tope en la calidad, una sola candidata catastrófica se lleva el 46,5 % de toda la masa de ventaja del grupo y aplasta a las demás en el mismo número pequeño: el grupo deja de ser una comparación.
En GRPO, ¿qué hay que mirar además de que suba la recompensa?
Que suba la recompensa buena, la medida contra la mejor jugada del motor (`engine best`) y no contra el mejor del grupo, y a la vez la proporción de grupos planos y la KL. Una recompensa verificable **siempre** puede subir: está escrita, y lo que está escrito se puede optimizar. La del grupo sube también cuando el modelo colapsa, porque ocho veces la misma jugada es su propio mejor; los grupos planos son el detector de ese colapso. Si la del motor sube, los planos no se disparan y la KL se queda pequeña, es una mejora; si los planos o la KL se disparan, es un síntoma. Los ilegales solo dicen algo con `restrict_to_legal: false`: con `true`, el de las ejecuciones publicadas, salen siempre 0. Y para elegir entre dos ejecuciones, ni esto: se enfrentan.
// M6
Evaluar, exportar, publicar
¿Qué mide cada columna de la tabla, y cuál es la trampa de cada una?
Legalidad sin máscara: si el modelo sabe qué jugadas existen (suelo; por debajo del 99 % nada de lo demás se interpreta). Top-1 y top-3: **imitación** de los humanos de 1800 en adelante, no calidad. Puzles: táctica, con la línea entera y no la primera jugada. Elo: cuánto gana a un Stockfish limitado, y es la única que mide jugar. Las cuatro salen del mismo modelo y se mueven por separado: `medium-v4-dpo-onpolicy` tiene mejor Elo que `medium-v4` y peor top-1, y no es una contradicción.
¿Por qué el Elo es un par y no una propiedad del modelo?
Porque el mismo checkpoint a temperatura 0,05 y `top_k` 1 y a temperatura 0,6 y `top_k` 20 son dos jugadores distintos, a unos 180 Elo de distancia (1359 frente a 1181). M2 las puso en dos filas, `small` y `small-greedy`; la tabla de hoy solo lleva las `-greedy`, y la card dice siempre con qué muestreo se midió lo que afirma.
¿De dónde sale el intervalo del Elo y qué no cubre?
De un bootstrap por percentiles: 500 remuestreos de las 160 partidas, un ajuste logístico por remuestreo, percentiles 2,5 y 97,5. Cubre el ruido del muestreo de partidas y nada más. No cubre que los peldaños estén donde dicen (`UCI_Elo` está calibrado para ritmo de torneo, no para 0,1 s; los `Skill Level` son anclas nominales), ni que el rival sea el mismo en dos ejecuciones, ni que ese Elo se parezca al de Lichess: no se parece y no hay conversión honesta.
¿Qué tres mediciones de reproducibilidad tiene el proyecto?
La escalera: la misma configuración con la misma semilla dio 1498 y 1558, porque el rival juega por tiempo y aleatoriza. El reward model de M5: la misma configuración, semilla y reparto dieron 72,91 % y 74,21 %, por los kernels no deterministas de la GPU. Y M6: el mismo modelo dos veces por tiempo con la máquina parada dio 1538 y 1538, y dos veces por nodos 1541 y 1524; los nodos no compran reproducibilidad porque `UCI_LimitStrength` aleatoriza igual, y el rival se queda por tiempo. La regla es la misma en los tres: antes de explicar una diferencia pequeña, mide cuánto se mueve tu montaje cuando no cambias nada.
¿Medir al rival por nodos arregla la reproducibilidad de la escalera?
No, y era la hipótesis razonable: a 0,1 s lo que Stockfish explora depende de lo que esté haciendo la máquina (en M3 se midieron +13 Elo con la CPU cargada), así que fijar los nodos debería quitar ese ruido. M6 la midió y no: por nodos las dos tiradas se separan 17 Elo y por tiempo 0, porque `UCI_LimitStrength` aleatoriza al rival a propósito con los dos límites. El rival se queda por tiempo con una condición de runbook: nada más en la máquina mientras corre. `elo_nodes` queda para máquinas compartidas y para el lab en que el propio jugador carga la CPU.
¿Qué dice y qué no dice una paridad del 95 %?
Dice que la exportación está bien hecha (un 80 % en fp16 sería un bug del grafo). No dice cuánto peor juega: un int8 que elige otra jugada una vez de cada veinte es otro jugador, y su Elo solo se sabe jugando. `medium-v4`: fp16 99,9 %, int8 95,1 % de paridad, y el coste en Elo lo mide el lab 3 con el grafo ONNX dentro del mismo `DecoderPlayer` que usa el checkpoint.
¿Por qué la cuantización dinámica pierde más paridad de lo que sugiere el tamaño del error?
Porque el error de redondeo no se reparte de forma uniforme: se nota donde los logits de las dos mejores jugadas están cerca, que es exactamente donde la decisión es difícil. Un error de 2,8 en los logits (el máximo medido en el int8 de `medium-v4`) no cambia nada en una posición con una jugada clara y cambia la elección en una posición igualada.
¿Qué tiene que llevar una model card para que alguien pueda contradecirla?
El fichero exacto que se midió (SHA-256 del fichero y, desde M6, de los pesos), el muestreo y la suite de cada número, la fecha, lo que no se midió (`n/a` es un valor), la procedencia y la licencia de los datos y la de los pesos. Las de Rukh se generan desde `run.json` y `results.json`, no se escriben a mano, y `rukh publish cards` las regenera todas subiendo solo el `README.md`.
¿Por qué un modelo se identifica por sus pesos y no por su fichero?
Porque el fichero que `rukh pull` trae del Hub no es byte a byte el que se midió: el publicador quita el optimizador y la cabeza atada, y `torch.save` no es estable. La guarda que compara SHA de ficheros rechazaba los mismos pesos; la que compara el SHA de los tensores de `model_state` (sin las cabezas atadas) acepta el mismo modelo en otro fichero y sigue rechazando otros pesos.
¿Qué hace `rukh eval nightly` y por qué la clave de caché se partió en dos?
Recorre el catálogo de `rukh pull`, trae lo que falte, fusiona cada LoRA sobre su base, mide lo que no está en caché y reescribe `results.json` y `benchmarks.md`: doce etapas (once filas de decoder, con los dos baselines, y el encoder), la misma suite, el mismo día; las dos filas de decoder que no están en el catálogo se miden a mano ese día. La clave de caché se partió por familia: la de partidas lleva el límite del rival y los peldaños, la de puzles no; antes, cambiar el tiempo por jugada invalidaba 6 000 puzles que no lo usan.
¿Qué hace que un baseline sea comparable?
Que se mida con el mismo harness: los mismos peldaños, la misma máscara de rescate, el mismo criterio de puzles, el mismo día. El nanoGPT de ajedrez de Karvonen se reimplementó y se puso en el mismo `play_rungs` que el resto, preguntado en su formato exacto y con una regla más estricta que la suya (una pregunta y se cuenta, sin cinco reintentos). Copiar el número de su paper habría comparado dos instrumentos, no dos modelos.
¿Qué no se puede decir de Rukh aunque los números inviten?
Que «juega a 1500 Elo» (gana lo que ganaría un 1500 contra Stockfish limitado a 0,1 s, no contra personas); que un alineado es «56 puntos mejor» restando dos escaleras (la resta de dos ±55 es ±80; se dice el +57 (36-79) del enfrentamiento directo); que «entiende ajedrez» (no hay instrumento que lo mida); que es mejor que un baseline medido con otro harness; y que un lector «reproduce el número»: reproduce el intervalo.
¿Por qué la arena de la demo imprime cuántas partidas harían falta?
Para que nadie lea diez partidas como un veredicto. Con veinte partidas el intervalo de la diferencia cubre varios cientos de Elo; una diferencia de 50 pide unas 185 partidas y una de 11, unas 3 800 (la aritmética de M5, `gamesNeeded`). Lo que veinte partidas sí enseñan es **cómo** juegan distinto dos etapas, que se ve en el tablero antes que en el número.