// 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.
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:
/** * 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;'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.
// `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, },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
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.ts
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.
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.