La ventana de contexto es un recurso escaso
27 min de lectura
La lección sobre recuperación acabó con una cuenta que no salía: tres pasajes de la novela ocupan 98 tokens, y en la ventana del mini-GPT caben 64. En un modelo con miles de tokens de ventana caben de sobra, pero el problema sólo cambia de escala. Un asistente que conversa vuelve a mandar en cada llamada su prompt de sistema, sus ejemplos, todo lo que se ha dicho y los pasajes de la pregunta nueva, porque el modelo no guarda nada de una llamada a la siguiente. Cada turno deja dos mensajes más, y antes o después la suma pasa de , una cifra que fijó quien entrenó el modelo.
Entonces algo se queda fuera, y lo único que se puede decidir es quién lo elige. Si no lo elige nadie, el modelo rechaza la llamada o trunca el prompt por su cuenta. Tu mini-GPT, desde su proyecto, lee sólo los últimos 64 tokens, y lo primero que pierde así es lo primero que escribiste: la especificación. Elegir bien pide saber qué cuesta cada token que sí entra, que no es siempre lo mismo, y si al modelo le da igual el sitio de lo que se queda.
Mira cómo se llena. En el explorable, un asistente sobre Marianela va por su séptima pregunta y tiene para ella un prompt de sistema, dos ejemplos, los seis turnos anteriores, los cinco pasajes que trajo el recuperador (del más parecido a la pregunta al menos) y la pregunta, más 400 tokens reservados para la respuesta: 3 410 tokens. Los tamaños los he puesto yo, redondos, del orden que daría un tokenizador de verdad. La ventana empieza en 2 560.
Con «lo trunca el modelo», el prompt de sistema, los dos ejemplos y el primer turno se quedan fuera, y del segundo entra el final. Cambia a «lo descarta el harness»: salen los cinco turnos más viejos, enteros, y se quedan el último y los cinco pasajes. Con «el harness compacta», los cuatro primeros turnos pasan a ser un resumen y sólo sobra el peor pasaje. Vuelve a «lo descarta el harness» y baja la ventana: en 1 024 tokens no queda más que lo que el harness no descarta nunca, y en 768 ni eso cabe, así que la llamada no se hace.
La ventana es un presupuesto
Escribamos para las posiciones que ocupa una pieza tal como la escribe la plantilla de chat: el contenido de cada mensaje, más su token de rol y su <|fin|>. Una llamada lleva el prompt de sistema , los ejemplos , el historial (los mensajes de los turnos anteriores, dos por turno, la pregunta y la respuesta), los pasajes, el caso , el token de rol que abre la respuesta y la respuesta . Todo tiene que caber:
El último término no se conoce hasta que la respuesta está escrita. El harness le guarda sitio de
antemano, la reserva: el max_tokens del contrato de modelo, y una respuesta que quiera más
se corta ahí. Lo demás es lo que el harness elige. De todas las piezas, sólo el historial crece
solo, dos mensajes por turno, y es el que acaba desbordando la suma en una conversación larga.
Un token más cuesta lo que el último
Caber no es gratis. La lección sobre la caché contó que, con la caché, el token de la posición cuesta multiplicaciones: una fila por todas las matrices de la red, y una consulta contra claves y valores en cada capa. Una llamada de posiciones, las del prompt y las de la respuesta, cuesta la suma de todas, como allí,
y el prefill hace las mismas multiplicaciones que si el prompt se leyera token a token, sólo que de una pasada.
Metamos un token más en el prompt, en una posición cualquiera. Cambian dos cosas. Su fila cuesta . Y cada una de las posiciones que quedan detrás lee una clave y un valor más, multiplicaciones cada una. Sumadas,
La se va. Un token más, entre donde entre, cuesta lo que costaría el último de una llamada un token más larga, así que su precio lo pone toda la llamada y no su sitio. El total crece como en su parte de atención: un prompt el doble de largo cuesta más del doble. Y mientras dura la llamada, la caché guarda sus números.
¿A partir de cuántas posiciones manda la atención? Cuando alcanza a . Con el de la lección sobre la caché, de matrices de atención de , un perceptrón por posiciones de dos matrices y la capa de salida,
En el mini-GPT son posiciones, la cifra de aquella lección. En su modelo de 32 capas y , con las cifras habituales en esa forma (, un vocabulario de 32 000 entradas), son . Por debajo de unos 25 000 tokens, un token más cuesta casi lo mismo que el primero. En una ventana de 128 000 (131 072 tokens), el último cuesta 6.2 veces lo que el primero, y la caché de esa llamada, a 2 bytes por número, ocupa 64 GiB, cinco veces los pesos. Las variantes que comparten claves y valores entre cabezas, que el curso deja fuera, existen sobre todo por esta cifra.
Cuando no cabe, alguien elige qué sale
Hay tres maneras de que quepa. La primera es la de un modelo al que nadie ha preparado nada: truncar, quedarse con los últimos menos la reserva y no saber nada de lo anterior. Lo que cae es lo primero que escribe la plantilla, el prompt de sistema y los ejemplos, y el corte pasa por en medio de un mensaje, que entra sin su token de rol. El modelo no recibe ninguna señal de que eso ha pasado: lee una secuencia de tokens, y ésa es la única que hay.
La segunda es descartar: el harness deja fuera piezas enteras, con una regla escrita. Su
contrato, para una función encajar(T_ctx, reserva) -> mensajes que monta la llamada:
- Con la reserva y el token de rol de la respuesta, la llamada no pasa de .
- El prompt de sistema, los ejemplos y la pregunta entran enteros. Si ellos solos no caben, la llamada falla y no se hace: una especificación truncada es otra especificación.
- Cada pieza entra entera o no entra. Un turno sale con sus dos mensajes.
- Salen en un orden fijado de antemano: los turnos, del más viejo al más nuevo, y después los pasajes, del último en el recuperador al primero. Sale lo justo: el prefijo más corto de ese orden que deja caber el resto.
- Lo que entra va en el orden en que estaba.
El orden del punto 4 es una política, no un teorema. Los pasajes se trajeron para esta pregunta y los turnos viejos hablaban de otras, pero una pregunta que dice ella necesita el turno que la nombró. Elegir el subconjunto de piezas que más vale y cabe es el problema de la mochila, y el harness no conoce el valor de cada pieza, así que no hay nada que optimizar: ordena por una prioridad y descarta desde el principio de la lista.
La tercera es compactar: sustituir los turnos viejos por un resumen que escribe el propio modelo y dejar los recientes como estaban. Lo que se conserva ocupa menos en cada llamada posterior, y algo de lo descartado sobrevive. Pero el resumen cuesta una llamada más, que lee los turnos que resume, y es una respuesta del modelo como cualquier otra: puede dejarse lo que la pregunta siguiente iba a necesitar, o escribir algo que nadie dijo. El bloque 5 le da su contrato, qué conserva siempre y qué no puede perder, en un agente que de verdad se queda sin ventana.
Lo que cabe no se lee igual en todas partes
Que una pieza entre no quiere decir que el modelo la use. Liu y sus coautores le dieron a varios modelos preguntas de NaturalQuestions-Open con veinte pasajes, unos 4 000 tokens, de los que sólo uno contenía la respuesta, y fueron moviendo ese pasaje de sitio. GPT-3.5-Turbo acertó el 75.8 % de las preguntas con el pasaje el primero, el 53.8 % con el pasaje el décimo y el 63.2 % con el pasaje el último. Sin ningún pasaje acertaba el 56.1 %, así que, en el centro del prompt, el pasaje que respondía le restaba. La mayoría de los modelos dibujaron la misma curva en U, también los de ventana larga. Sus autores lo llamaron lost in the middle, «perdido en el medio».
Nada en la arquitectura lo pide. La atención lee cualquier posición con las mismas cuentas, y la caché guarda las claves y los valores exactos de todas: es lo que hacen los modelos entrenados, y se mide, no se deduce. Aquí no puedes reproducirlo, porque el modelo guionizado no lee lo que le mandas y en la ventana del mini-GPT no caben veinte pasajes. Para el harness, la consecuencia es que el orden también es suyo: lo mejor va en un extremo, nunca en medio, y cada pasaje que añades empuja los demás hacia el centro.
Encajar el prompt, sobre el modelo guionizado
La celda corre sobre el modelo guionizado de la
lección sobre la especificación, que ahora se carga con una línea, y cuenta los tokens con
el tokenizador de tu mini-GPT. El modelo guionizado no tiene tokenizador ni ventana: la ventana de
256 tokens la he puesto yo, y quien la hace cumplir es encajar. El asistente va por su cuarta
pregunta, con tres turnos detrás y los tres pasajes que el recuperador de TF-IDF (term
frequency–inverse document frequency) de la lección anterior trae para ella. La celda compara lo
que leería un modelo que trunca con lo que manda encajar, y se lo da al modelo guionizado, cuyo
guion dice qué mensajes espera recibir con este presupuesto.
from pyodide.http import open_url
exec(open_url("/courses/llm-agents/guionizado.py").read()) # ModeloGuionizado, lección 18
exec(open_url("/courses/llm-agents/bpe.py").read()) # codificar: aquí sólo cuenta tokens
F = [tuple(par) for par in json.load(open_url("/courses/llm-agents/bpe-merges.json"))]
def tokens(m): # lo que ocupa un mensaje: contenido, token de rol y <|fin|>
return len(codificar(m[1], F)) + 2
SISTEMA = ("sistema", "Respondes preguntas sobre la novela Marianela con los pasajes que "
"acompañan a cada pregunta. Si no la responden, di: no lo sé.")
HISTORIAL = [("usuario", "¿Quién es Pablo?"), # tres turnos, del más viejo
("asistente", "El hijo ciego de don Francisco Penáguilas."),
("usuario", "¿Quién le hace de lazarillo?"), ("asistente", "La Nela."),
("usuario", "¿Con quién vive ella?"),
("asistente", "Con el señor Centeno, capataz de ganado en las minas.")]
PASAJES = [ # los tres que trae TF-IDF, del mejor al peor
"—Dicen que tengo diez y seis años—replicó la Nela, examinando a su vez al doctor.",
"No hay mal que cien años dure.", "Le conozco; tiene una voz ronca y pausada."]
PREGUNTA = "¿Cuántos años tiene la Nela?"
def montar(historial, pasajes):
texto = "".join("Pasaje: %s\n" % p for p in pasajes) + "Pregunta: " + PREGUNTA
return [SISTEMA] + historial + [("usuario", texto)]
def encajar(T_ctx, reserva):
"""Sistema y pregunta enteros; salen turnos, del más viejo, y después pasajes, del peor."""
h, p = list(HISTORIAL), list(PASAJES)
while 1 + sum(map(tokens, montar(h, p))) + reserva > T_ctx: # 1: el <|asistente|> del final
if h: h = h[2:] # un turno: su pregunta y su respuesta, juntas
elif p: p = p[:-1]
else: raise ValueError("no caben ni el sistema y la pregunta: la llamada no se hace")
return montar(h, p)
T_CTX, RESERVA = 256, 24
todo, llamada = montar(HISTORIAL, PASAJES), encajar(T_CTX, RESERVA)
cabe = T_CTX - RESERVA - 1 # lo que deja leer un modelo que trunca
leidos = [max(0, min(tokens(m), cabe - sum(map(tokens, todo[i + 1:])))) for i, m in enumerate(todo)]
print("%d tokens con la reserva, ventana de %d" % (sum(map(tokens, todo)) + 1 + RESERVA, T_CTX))
print("\nrol tokens si trunca el modelo con encajar")
for m, k in zip(todo, leidos):
trunca = "entero" if k == tokens(m) else "fuera" if k == 0 else "sus últimos %d" % k
print("%-9s %6d %-19s %s" % (m[0], tokens(m), trunca, "entra" if m in llamada else "fuera"))
espera = [SISTEMA] + HISTORIAL[4:] + montar([], PASAJES)[-1:] # lo que he previsto yo
modelo = ModeloGuionizado([(espera, "Dieciséis años, según dice ella misma.")])
print("\nel modelo guionizado responde:", modelo(llamada))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador y se reutiliza en todas las lecciones.
Todo junto son 313 tokens para una ventana de 256. Un modelo que trunca lee los 231 últimos del
prompt, y del prompt de sistema le llegan siete: : no lo sé. y el <|fin|>. Lee
entera la conversación y los pasajes, y de su especificación, sólo la orden de no saber. encajar
descarta los dos turnos más viejos, que no hacen falta para esta pregunta, y deja el prompt de
sistema entero, el tercer turno y los tres pasajes. Es lo que el guion esperaba, y el modelo
guionizado responde.
Ahora sube RESERVA a 32. Ocho tokens más para la respuesta obligan a descartar también el tercer
turno, y el modelo guionizado falla en el mensaje 1: su guion esperaba ese turno. Quien falla no es
el modelo. Es tu presupuesto, comprobado contra lo que habías previsto que recibiera.
Comprueba tu intuición
Cuatro preguntas: dónde empieza a mandar la atención en un modelo más ancho, qué pierde un modelo
que trunca, qué garantiza encajar y qué hacer con la curva en U.
Un modelo de capas tiene , un perceptrón por posiciones de dos matrices con y un vocabulario de entradas. ¿En qué posición la atención de un token cuesta lo mismo que todas sus matrices?
Se acepta un margen de ±1.
Un harness le manda a un modelo un prompt más largo que su ventana, y el modelo, sin avisar, se queda con los últimos tokens que caben. ¿Qué pierde primero?
¿Qué garantiza encajar, según su contrato?
Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.
Con veinte pasajes en el prompt y uno solo con la respuesta, GPT-3.5-Turbo acierta el 75.8 % de las preguntas si ese pasaje va el primero, el 53.8 % si va el décimo y el 56.1 % sin ningún pasaje. ¿Qué te dice eso de tu harness?
Todo lo que el harness decide en esta lección tiene un número que nadie ha medido todavía para el tuyo: cuántos pasajes traer, en qué orden ponerlos, si el historial sale antes que ellos, cuánto reservar. La curva en U se midió con veinte pasajes y miles de preguntas, y contradecía lo que la arquitectura dejaba esperar. Con tu modelo y tus preguntas puede salir otra, y lo único que lo dice es contar aciertos.
La lección siguiente, sobre evaluar, convierte «acierta más» en un número con su intervalo: cuántas preguntas hacen falta para que la diferencia entre dos harness sea creíble, cómo se puntúa una respuesta que no tiene una sola forma correcta y qué falla cuando el que puntúa es otro modelo.
Para profundizar3 fuentes · 3 papers
De dónde sale lo de esta lección, y dónde seguir si quieres más. Nada de aquí hace falta para continuar el curso.
- Lost in the Middle: How Language Models Use Long Contexts
La curva en U de la lección, con seis modelos, 10, 20 y 30 pasajes y la tabla posición a posición. Su apartado sobre poner la pregunta antes y después de los pasajes mide un remedio para el harness.
- RULER: What's the Real Context Size of Your Long-Context Language Models?
La ventana que un modelo anuncia no es la que usa bien: de 17 modelos que dicen admitir 32 000 tokens o más, sólo la mitad rinde de forma aceptable a esa longitud en sus trece tareas.
- MemGPT: Towards LLMs as Operating Systems
Un harness que trata la ventana como la memoria de un sistema operativo: avisa al modelo cuando se llena, saca los mensajes viejos a un almacén que puede consultar y deja en su lugar un resumen que se rehace en cada vaciado.