rukh · lab

// M5 · lección 07

Los modelos alineados en la demo

Lo que cambia en el registro de etapas de la demo, diecinueve líneas nuevas con su test: la etapa de GRPO, el DPO que ahora apunta al brazo dentro de política, y la razón de que servir un modelo alineado no cambie absolutamente nada del resto del navegador.

  • tiempo de trabajo40 min
  • nivel base
  • actualizado el23 de septiembre de 2026

Qué vas a construir

Casi nada, y eso es lo que enseña. El hito entrena tres modelos nuevos, publica dos de ellos en el Hub, y lo que hace falta en rukh-web para que alguien pueda jugar contra ellos desde el navegador son diecinueve líneas: una entrada en el mapa de tamaños, una entrada en la lista de etapas, y una línea en el test que fija esa lista.

El commit de la demo para este hito es a34c8b9, y toca exactamente dos ficheros. Nada más de la demo cambia, porque un modelo alineado es el mismo decoder de M2 con otros pesos: mismo vocabulario, mismo contexto, misma forma de entrada y salida. Con los LLM pasa lo mismo: la versión «instruct» de un modelo se sirve con el mismo tokenizador y el mismo motor de inferencia que su base.

El registro de etapas

src/lib/registry.ts guarda todo lo que la demo sabe de cada modelo. M0 lo enseñó entero; aquí solo se mira lo que añade este hito, empezando por el mapa de tamaños, que se lee antes de descargar nada:

src/lib/registry.ts
/**
* Download sizes in MB, **measured** on the published files rather than estimated. They drive
* the consent copy, which is the last thing a reader sees before the browser fetches tens of
* megabytes, so an estimate that drifts from the file is a promise the page does not keep. Read
* them back with `python -c "import os; print(os.path.getsize(...))"` whenever a stage is
* re-exported: `small` and the encoder both changed size when their weights did.
*
* Every number here was re-read from the staged `onnx` folders on 2026-09-20 and every
* one of them was wrong -- by 6 to 10 MB, always low, because they had been carried over from the
* plan's estimates and never corrected. The consent step had been under-promising every download
* since P2.
*/
export const STAGE_SIZE_MB = {
'tiny-int8': 8,
'small-fp16': 79,
'small-int8': 43,
'medium-fp16': 231,
'medium-int8': 122,
'medium-dpo-fp16': 231,
'medium-grpo-fp16': 231,
'medium-elo-fp16': 231,
'medium-elo-int8': 122,
// The swappable export is 438 KB bigger than the ordinary one -- the cost of a graph that takes
// its LoRA factors as inputs is 0.19 % of the file, which is the whole argument for doing it.
'medium-lora-fp16': 232,
'medium-lora-int8': 122,
} as const;

rukh-web/src/lib/registry.tslíneas 42-68 · a34c8b9

'medium-grpo-fp16': 231 es la línea nueva. El docstring de encima explica que los tamaños están medidos sobre los ficheros publicados, porque son lo que la demo promete antes de una descarga de cientos de megabytes. Cuando se releyeron, todos estaban entre 6 y 10 MB por debajo: venían de las estimaciones del plan.

src/lib/registry.ts
// `medium` after DPO on preference pairs the model proposed itself. +57 Elo over the base in a
// head-to-head of 800 games (M5), and it beats the off-policy arm by +37 when the two play each
// other -- which is the only direct comparison between the two candidates, and the reason this
// is the one served.
{
id: 'medium-dpo-fp16',
label: 'Rukh medium + DPO (fp16)',
kind: 'onnx',
repo: 'chorcat/rukh-medium-dpo',
file: 'onnx/model-fp16.onnx',
sizeMb: STAGE_SIZE_MB['medium-dpo-fp16'],
block: DEFAULT_BLOCK,
},
// `medium` after GRPO against a verifiable reward: +43 Elo over the base, and the aligned model
// that damages legality least -- 1.66 times its base's illegal-proposal rate against the 2.0-2.6
// of the two DPO arms.
{
id: 'medium-grpo-fp16',
label: 'Rukh medium + GRPO (fp16)',
kind: 'onnx',
repo: 'chorcat/rukh-medium-grpo',
file: 'onnx/model-fp16.onnx',
sizeMb: STAGE_SIZE_MB['medium-grpo-fp16'],
block: DEFAULT_BLOCK,
},

rukh-web/src/lib/registry.tslíneas 205-229 · a34c8b9

Las dos entradas de la lista de etapas. La segunda es nueva; de la primera cambia el comentario. medium-dpo-fp16 apuntaba a un DPO entrenado sobre todos los pares de Lichess y ahora apunta al brazo entrenado con los pares que propuso el propio modelo. El comentario cita por qué: +57 Elo sobre la base en 800 partidas, y +37 sobre el otro brazo en enfrentamiento directo. Con la medición al lado, nadie cambiará el repo dentro de seis meses porque «el otro parecía mejor».

Las cifras del comentario de GRPO se escribieron antes de cerrar las mediciones: la publicada es +44 (IC 23 a 66) y no +43, y las tasas de ilegales de los brazos de DPO son 1,7-2,0 y 2,0-2,9 veces su base. Un comentario con cifras envejece igual que una estimación copiada.

block: DEFAULT_BLOCK explica por qué no hace falta tocar nada más: ONNX Runtime Web no expone los metadatos del fichero, así que la demo lee el contexto del registro, y un modelo alineado tiene el mismo contexto que su base.

El test que fija la lista

tests/registry.test.ts
describe('stage registry', () => {
it('keeps the mock first and every other stage an ONNX one', () => {
expect(STAGES.map((stage) => stage.id)).toEqual([
'mock',
'tiny-int8',
'small-fp16',
'small-int8',
'medium-fp16',
'medium-int8',
'medium-dpo-fp16',
'medium-grpo-fp16',
'medium-elo-fp16',
'medium-elo-int8',
'medium-lora-fp16',
'medium-lora-int8',
]);
expect(STAGES[0].kind).toBe('mock');
expect(STAGES.slice(1).every((stage) => stage.kind === 'onnx')).toBe(true);
});

rukh-web/tests/registry.test.tslíneas 31-49 · a34c8b9

Una línea nueva, 'medium-grpo-fp16', dentro de una comparación de igualdad. toEqual sobre el array entero hace que añadir, quitar o mover una etapa rompa el test, a propósito: el orden de la lista es el del selector, y una etapa que aparece en medio sin que nadie lo decida es un cambio de interfaz disfrazado de refactor. Las dos líneas de abajo exigen que la primera etapa sea el mock y todas las demás, ONNX.

Lo que no cambió, y por qué es la noticia

Esta lista corta es el resultado de los cinco módulos anteriores:

Pieza de la demo Por qué no se toca
El tokenizador (src/lib/chess-lm/) el vocabulario de 2 030 tokens se fijó en M1 y ningún alineamiento lo cambia: DPO y GRPO mueven probabilidad entre los mismos tokens
El worker de ONNX la forma de entrada y salida del grafo es la del decoder de M2; un modelo alineado exporta idéntico
El enmascarado de jugadas ilegales sigue siendo necesario —y más que antes, porque alinear multiplica la tasa de propuestas ilegales— pero el código es el mismo
El tablero y la máquina de partida no saben qué modelo hay detrás, y ese fue el diseño de M0
El selector de Elo solo lo contestan las etapas de M4, que son las únicas entrenadas con cabeceras por debajo de 1800

La fila del enmascarado es la que conviene subrayar. Los modelos alineados proponen entre 1,7 y 2,9 veces más jugadas ilegales que su base, y en la demo no se nota, porque el sorteo enmascarado las descarta antes de que lleguen al tablero. Justo porque el jugador no lo ve, hay que medirlo en otro sitio. Un asistente con un validador de salida detrás esconde así sus errores de formato.

// demoJugar contra el modelo alineado con GRPO231 MB la primera vez; después queda en la caché del navegador. Compáralo con ?stage=medium-fp16, que es su base.
Abrir la demo?stage=medium-grpo-fp16

Qué has aprendido

La frontera entre el repositorio de ML y el de la demo es una lista, y si está bien hecha un modelo nuevo cuesta tres entradas: el tamaño, la etapa y su nombre en el test. Los números que una interfaz enseña antes de una descarga son promesas y se miden. Y un comentario junto a una entrada de configuración, citando la medición que la decidió, vale más que el mismo dato en un documento que nadie va a abrir.

Lo siguiente es lo que salió: los números del hito, con sus intervalos y sus tres criterios.