Un transformer no sabe qué palabra va primero
La atención tiene un secreto incómodo: es ciega al orden. Si barajas los tokens de una frase, el mecanismo de atención devuelve exactamente los mismos productos escalares. Para el modelo, “el perro muerde al hombre” y “el hombre muerde al perro” son, de partida, la misma bolsa de vectores.
Alguien tiene que inyectar la posición. Durante años ese alguien fue una suma: coger el embedding de cada token y añadirle un vector que codificaba “soy la posición 3”. Funcionaba, pero envejecía mal. La codificación posicional rotatoria —RoPE, por sus siglas en inglés— cambió el verbo. En vez de sumar posición, la rota. Y ese cambio es la razón de que hoy la usen Llama, Qwen, Mistral y buena parte de los modelos abiertos que despliegas en producción.
El problema de sumar la posición
El transformer original sumaba señales sinusoidales al embedding de entrada. La idea era elegante, pero arrastra dos defectos prácticos.
El primero es que mezcla dos cosas distintas en el mismo vector: qué significa el token y dónde está. La red tiene que aprender a separarlas de nuevo en cada capa.
El segundo es que la posición es absoluta. El vector de la posición 512 no guarda ninguna relación limpia con el de la 511. Y lo que de verdad importa en el lenguaje casi nunca es la posición absoluta, sino la distancia relativa: cuántos tokens hay entre el verbo y su sujeto, no si ese verbo está en la casilla 40 o en la 4.000. Si entrenas con contextos de 2.000 tokens y en inferencia le pasas 8.000, el modelo se encuentra con posiciones absolutas que nunca vio y se desorienta.
La idea de RoPE: rotar, no sumar
RoPE ataca el problema desde otro ángulo. No toca los embeddings de entrada; actúa dentro de la atención, sobre los vectores de consulta (query) y clave (key), justo antes de calcular su producto escalar.
La receta es geométrica. Parte el vector en pares de dimensiones y trata cada par como un punto en un plano. A ese punto lo gira un ángulo proporcional a la posición del token: la posición 1 gira poco, la 100 gira cien veces más, y cada par de dimensiones gira a su propia frecuencia. Un token en la posición 5 tiene sus vectores girados cinco pasos; el mismo token en la posición 50, cincuenta.
La magia aparece en el producto escalar. Cuando la atención compara una consulta en la posición m con una clave en la posición n, el resultado depende solo de la diferencia m - n, no de los valores absolutos. Las rotaciones absolutas se cancelan y queda la distancia relativa. Consigues codificación relativa sin construir una matriz de posiciones relativas: sale gratis de la geometría.
Cómo se ve en código
La implementación cabe en unas pocas líneas. Precalculas las frecuencias, construyes senos y cosenos para cada posición y rotas cada par de dimensiones.
import torch
def rope(x: torch.Tensor, base: float = 10000.0) -> torch.Tensor:
# x: (batch, seq_len, n_heads, head_dim). head_dim debe ser par.
seq_len, head_dim = x.shape[1], x.shape[-1]
# Una frecuencia por cada par de dimensiones: las primeras giran
# deprisa (detalle local), las últimas despacio (contexto largo).
inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim))
pos = torch.arange(seq_len).float()
angles = torch.einsum("s,d->sd", pos, inv_freq) # (seq_len, head_dim/2)
cos = angles.cos().repeat_interleave(2, dim=-1) # (seq_len, head_dim)
sin = angles.sin().repeat_interleave(2, dim=-1)
# Rotación 2D de cada par (x1, x2) -> (x1·cos - x2·sin, x2·cos + x1·sin)
x1, x2 = x[..., 0::2], x[..., 1::2]
x_rot = torch.stack((-x2, x1), dim=-1).reshape_as(x)
return x * cos[None, :, None, :] + x_rot * sin[None, :, None, :]
Lo importante no es el álgebra, sino dónde vive: se aplica a query y key en cada capa de atención, nunca a los value. Y no añade ningún parámetro entrenable. Es pura transformación determinista, lo que la hace barata y predecible.
Absoluta sinusoidal frente a RoPE
| Aspecto | Posicional absoluta (sinusoidal) | RoPE |
|---|---|---|
| Cómo inyecta la posición | Suma un vector al embedding de entrada | Rota query y key dentro de la atención |
| Qué codifica de verdad | Posición absoluta | Distancia relativa entre tokens |
| Parámetros extra | Ninguno (o una tabla aprendida) | Ninguno |
| Dónde actúa | Una vez, a la entrada | En cada capa de atención |
| Extrapolar a más contexto | Pobre; degrada fuera de lo entrenado | Buena, y ampliable con escalado de frecuencias |
La última fila es la que paga las facturas. Como RoPE codifica distancias, tolera mejor secuencias más largas que las del entrenamiento. Y cuando no basta, hay una palanca: reescalar las frecuencias.
Por qué te importa en producción
El interés práctico de RoPE es que la ventana de contexto deja de ser un muro y pasa a ser un dial. Técnicas como el escalado NTK o la interpolación de posiciones no reentrenan el modelo: solo ajustan la base de las frecuencias —ese 10000.0 del código— para “estirar” el rango de posiciones que el modelo reconoce. Así es como un modelo entrenado a 8.000 tokens se sirve razonablemente bien a 32.000 con un ajuste fino corto, o a veces sin ninguno.
Esto tiene consecuencias concretas cuando eliges y despliegas un modelo abierto:
- Si vas a alargar el contexto por encima del de entrenamiento, comprueba qué método de escalado RoPE usa el modelo y a qué
baseestá calibrado. Un valor mal puesto degrada la recuperación en los extremos de la ventana. - Al comparar dos checkpoints del mismo modelo, mira si difieren solo en la configuración de RoPE. Muchas variantes “long-context” son eso: el mismo peso con las frecuencias reescaladas.
- Si mides una caída de calidad justo en tokens muy lejanos del principio, sospecha del escalado posicional antes que de los datos.
RoPE no es un detalle académico enterrado en el paper de atención. Es el parámetro que decide, en la práctica, hasta dónde puede leer tu modelo sin perder el hilo. La próxima vez que un despliegue te ofrezca “context length: 128k”, ya sabes qué mecanismo está haciendo ese trabajo por dentro, y dónde mirar cuando se rompe.