El experimento que no sale
Tienes un asistente con una conversación larga y el KV cache creciendo sin parar. La solución parece evidente: ventana deslizante. Te quedas con los últimos N tokens y descartas los antiguos. Memoria constante, latencia constante, problema resuelto.
Lo implementas y el modelo se desmorona. No degrada suavemente: colapsa. La perplejidad se dispara varios órdenes de magnitud en cuanto el borde de la ventana pasa por encima de los primeros tokens de la secuencia. El texto pasa de coherente a ensalada de palabras en cuestión de un par de pasos de decode.
Lo raro es que esos primeros tokens ya no importaban. Eran el saludo inicial o un fragmento de instrucción que quedó 20.000 tokens atrás. Y sin embargo, quitarlos rompe el modelo entero.
Los primeros tokens no son texto: son un desagüe
Si visualizas los mapas de atención de un transformer decoder ya entrenado, aparece un patrón que no encaja con ninguna intuición semántica. Una fracción enorme de la atención, en casi todas las capas y casi todas las cabezas, apunta a los primeros tokens de la secuencia. Da igual lo que digan. Suelen ser un token de inicio o una palabra funcional sin contenido.
Esos tokens reciben atención no por lo que significan, sino por lo que hacen: absorben masa de atención sobrante. De ahí el nombre: attention sink, sumidero de atención.
La causa está en el softmax. La distribución de atención de cada cabeza tiene que sumar exactamente 1. El modelo no tiene una salida de “aquí no hay nada relevante”: está obligado a repartir toda esa probabilidad entre los tokens disponibles. Cuando una cabeza no encuentra nada útil que mirar en el contexto, necesita un sitio donde verter el excedente sin contaminar el resultado.
Durante el entrenamiento, los primeros tokens son los candidatos naturales para ese papel. Son los únicos visibles para todas las posiciones de la secuencia por la máscara causal. El modelo aprende a usarlos como vertedero y calibra sus pesos asumiendo que siempre estarán ahí.
Cuando la ventana deslizante los descarta, la masa de atención sobrante tiene que ir a otra parte. Y va a tokens con contenido real, que quedan sobreponderados de forma absurda. El modelo no ha perdido información: ha perdido el sitio donde no mirar.
La receta: unos pocos sinks más la ventana
La corrección es casi ofensiva de lo barata que es. Mantén de forma permanente los primeros tokens en el cache y desliza la ventana solo sobre el resto. Con cuatro tokens fijos basta para recuperar la perplejidad de referencia; es el número que reporta el trabajo original de StreamingLLM y el que suele funcionar en la práctica.
# Ventana deslizante con sumideros de atención.
# El cache es una lista de pares (k, v) por posición.
N_SINKS = 4 # tokens iniciales que no se descartan nunca
WINDOW = 2048 # tokens recientes que sí rotan
def recortar_cache(cache):
if len(cache) <= N_SINKS + WINDOW:
return cache
# Los sinks van al principio; en medio se tira todo lo que sobra.
return cache[:N_SINKS] + cache[-WINDOW:]
def decode(cache, token):
k, v = proyectar_kv(token)
cache.append((k, v))
cache = recortar_cache(cache)
# Detalle crítico: la posición que se usa en la codificación posicional
# es el índice dentro del cache recortado, no el índice absoluto
# del token en la conversación.
logits = atender(query(token), cache, posiciones=range(len(cache)))
return muestrear(logits), cache
El comentario del final no es un adorno. Con codificación posicional rotatoria (RoPE), la posición se aplica en el momento de calcular la atención, así que hay que reindexar respecto al cache recortado. Si conservas los índices absolutos, el modelo ve un salto enorme entre el token 3 y el token 40.000. Vuelve a degradarse, esta vez por otra razón. Este fallo es silencioso: no lanza ninguna excepción, solo empeora la salida.
Cuándo compensa cada estrategia
| Estrategia | Memoria del cache | Calidad en secuencias largas | Cuándo usarla |
|---|---|---|---|
| Atención densa completa | Crece linealmente sin techo | Máxima, hasta agotar la GPU | Contextos que caben de sobra en memoria |
| Ventana deslizante simple | Constante | Colapsa al pasar los primeros tokens | Prácticamente nunca |
| Ventana con sumideros | Constante | Estable en secuencias de millones de tokens | Streaming continuo, sesiones sin fin |
| Recomputar el contexto | Constante, coste de cómputo alto | Máxima dentro del recorte | Cuando puedes pagar el prefill repetido |
La cuarta fila merece un matiz. Recortar el histórico y volver a hacer prefill del contexto recortado también funciona, y de hecho es lo que hacen muchas aplicaciones de chat. Pero pagas un prefill completo cada vez que recortas, y ese coste crece con el tamaño del contexto que decidas conservar.
Lo que esto no te da
Aquí está la parte que se malinterpreta con frecuencia. Los sumideros de atención permiten seguir generando sin que la perplejidad se dispare. No amplían la memoria del modelo.
El contenido que sale de la ventana se ha ido. Si en el token 5.000 el usuario dijo su nombre y la ventana cubre 2.048 tokens, en el token 30.000 ese nombre no existe para el modelo. Los cuatro sinks que conservas no guardan información útil: son un desagüe, no un resumen.
Dicho de otro modo: esto resuelve un problema de estabilidad numérica, no uno de recuperación de información. Si necesitas que el modelo recuerde algo de hace 50.000 tokens, sigues necesitando RAG, un resumen incremental o una memoria externa explícita. Los sumideros solo garantizan que lo que sí está en la ventana se procese bien.
Cómo aterrizarlo
Si sirves modelos con vLLM, SGLang o TensorRT-LLM, es probable que ya lo tengas: las implementaciones de ventana deslizante de los motores de inferencia maduros llevan sinks incorporados. Búscalo antes de escribir nada. Varias familias de modelos recientes van más allá y entrenan un sumidero explícito —un valor aprendido que se suma al denominador del softmax— en lugar de dejar que el comportamiento emerja sobre tokens reales.
Si mantienes tu propio bucle de decode, la lista es corta:
- Fija los primeros tokens del cache y no los toques nunca. Cuatro es un punto de partida razonable.
- Reindexa las posiciones respecto al cache recortado, no respecto a la secuencia original.
- Mide perplejidad sobre un documento largo antes y después. Si el arreglo funciona, la curva se aplana; si no, verás el pico exactamente donde la ventana pasa por encima de los sinks.
- No vendas esto como memoria infinita. Es generación estable, sin límite práctico de longitud, que es otra cosa.
La lección de fondo va más allá de este truco concreto. El modelo no aprendió solo a representar tu texto: aprendió a apoyarse en propiedades estructurales de su propio contexto. Optimizar la inferencia sin conocer esas dependencias es una forma segura de romper algo que funcionaba, sin un solo error en los logs.
Fuentes
- Efficient Streaming Language Models with Attention Sinks (Xiao et al., 2023), el trabajo que aisló el fenómeno y propuso la corrección.