Curso / HF LLM Course / Capítulo 1
HF LLM Course · 1 de 12

Hugging Face · LLM Course · Capítulo 1

Transformers: por qué todo lo que ya usás corre sobre esto

Tenés práctica con código pero nunca viste la teoría de punta a punta. Este capítulo te da el mapa completo: qué es un transformer, por qué casi todo LLM moderno tira solo la mitad de esa arquitectura, y cómo el texto sale, literalmente, un token a la vez.

Antes de leerNinguno: este es el punto de partida del recorrido. Si preferís la base matemática antes que el modelo, arrancá por fast.ai lección 3 (ver la ruta «fundamentos primero» de la portada).

🎯 Al terminar vas a poder

  • Explicar con tus palabras qué distingue un LLM de un modelo de NLP clásico, y por qué casi todo lo que usás hoy es decoder-only.
  • Seguir a mano el cálculo de atención — Query, Key, Value, puntajes, softmax, suma ponderada — sobre un ejemplo de tres tokens.
  • Calcular cuánta memoria ocupa el KV cache de un modelo concreto por token y por conversación, y explicar por qué GQA lo reduce.
  • Elegir temperature, top_p, top_k y repetition_penalty con criterio, y diagnosticar una salida repetitiva o incoherente a partir de ellos.
  • Explicar por qué un fine-tune no borra el sesgo del modelo base y qué capas de mitigación existen encima.

0. Por qué este capítulo pesa más de lo que parece

El curso original de Hugging Face nació en 2022 como un curso de NLP (procesamiento de lenguaje natural) y fue mutando a curso de LLMs a medida que el campo lo hizo. Eso importa para vos: vas a encontrar frases pensadas para un mundo con BERT y GPT-2 como protagonistas, en un momento donde el protagonista real es un puñado de modelos decoder-only enormes (Llama, Gemma, Qwen, DeepSeek, GPT, Claude). La mecánica de fondo no cambió, pero el énfasis sí, y este capítulo está reordenado para reflejar eso.

🟢 Actualización 2026

Casi ningún proyecto nuevo hoy arranca fine-tuneando un encoder tipo BERT desde cero. La ruta por defecto es: agarrar un LLM decoder-only pre-entrenado, y resolver la tarea con prompting, RAG, o un fine-tune liviano (LoRA/QLoRA). Vas a ver esa arquitectura en todos lados porque es, en la práctica, la única que te vas a cruzar el 90% del tiempo — por eso este capítulo le da más peso que el original.

Como venís con experiencia práctica (ya usaste modelos, APIs, quizás algún fine-tune) pero sin la teoría de base, el objetivo acá no es que memorices nombres de arquitecturas. Es que entiendas por qué funcionan así, para que cuando leas un paper o un README de un modelo nuevo, ya tengas dónde encajar la información nueva.

1. De NLP a LLM: qué cambió realmente

NLP (Natural Language Processing) es el campo completo: todo lo que tiene que ver con que una máquina entienda o genere lenguaje humano. Análisis de sentimiento, traducción, reconocer que "Hugging Face" es el nombre de una empresa y no una acción física — todo eso es NLP, y existe desde antes de que existieran los transformers.

Un LLM (Large Language Model) es un tipo particular de modelo de NLP que se volvió tan grande y se entrenó con tantos datos que dejó de necesitar un modelo distinto por tarea. En vez de entrenar un clasificador de sentimiento, otro de traducción y otro de resúmenes, entrenás un solo modelo que aprendió lenguaje de forma tan general que resuelve las tres cosas con el prompt correcto.

📜 Lo que decía el curso original

"Un LLM es un modelo de IA entrenado con enormes cantidades de texto que puede entender y generar texto similar al humano, reconocer patrones del lenguaje, y realizar una amplia variedad de tareas sin entrenamiento específico por tarea." Sigue siendo correcto — es la definición de base que no cambió.

Lo que sí distingue a un LLM del resto:

Escala

De millones a cientos de miles de millones de parámetros.

Generalidad

Una tarea nueva no requiere reentrenar el modelo, solo redactar bien el prompt.

Aprendizaje en contexto

Le mostrás 2-3 ejemplos dentro del prompt y ya ajusta su comportamiento.

Capacidades emergentes

A cierta escala aparecen habilidades que nadie programó explícitamente (razonar paso a paso, seguir instrucciones complejas).

Y las limitaciones que vienen de fábrica con esa escala — las vas a necesitar en la cabeza todo el curso: alucinan con total confianza, no "entienden" el mundo (operan sobre patrones estadísticos), heredan sesgos de sus datos de entrenamiento, y tienen una ventana de contexto finita (aunque hoy esa ventana llega a millones de tokens en algunos modelos, algo impensado en 2022).

2. Qué hace un transformer, en la práctica

Antes de mirar el motor, veamos qué hace el auto andando. La librería transformers de Hugging Face resume todo el trabajo sucio (preprocesar texto, correr el modelo, decodificar la salida) en una sola función: pipeline().

from transformers import pipeline

classifier = pipeline("sentiment-analysis")
classifier("Llevo toda mi vida esperando un curso así.")
→ [{'label': 'POSITIVE', 'score': 0.9598}]

Bajo el capó, ese pipeline() siempre hace tres pasos, sin importar la tarea:

  1. Preprocesa el texto a un formato numérico que el modelo entiende (tokenización).
  2. Pasa esos números por el modelo.
  3. Post-procesa la salida cruda para devolverte algo legible.

La misma función sirve para tareas radicalmente distintas con solo cambiar el nombre de la tarea y, opcionalmente, el modelo:

# Clasificación sin haber entrenado esas categorías (zero-shot)
classifier = pipeline("zero-shot-classification")
classifier(
    "Este es un curso sobre la librería Transformers",
    candidate_labels=["educación", "política", "negocios"],
)

# Generación de texto con un modelo puntual del Hub
generator = pipeline("text-generation", model="HuggingFaceTB/SmolLM2-360M")
generator("En este curso vamos a aprender a", max_new_tokens=30)

🟢 El mismo pipeline con Gemma 4

SmolLM2-360M es un modelo base chico, elegido para que el ejemplo corra en CPU. El modelo que usás hoy entra en el mismo pipeline: pipeline("text-generation", model="google/gemma-4-E2B-it", device_map="cuda", dtype=torch.bfloat16), con una diferencia: es un modelo instruct, así que se le pasan mensajes de chat (gen([{"role": "user", "content": "…"}], max_new_tokens=40)) y no un string suelto. Con texto crudo un instruct degenera ("aprender a aprender a a a…"). Probado en la 5090 con transformers 5.16: 1,5 s por respuesta de 40 tokens y unos 10 GB de VRAM.

Un detalle que muerde la primera vez: el original usa max_length=30, que cuenta prompt + generación — si tu prompt ya tiene 25 tokens, te quedan 5 de respuesta. max_new_tokens cuenta solo los tokens nuevos, que es casi siempre lo que querés decir; si pasás los dos, max_new_tokens tiene prioridad.

Esto es exactamente lo mismo que hacés hoy cuando llamás a una API de un LLM alojado (Claude, GPT, Llama vía un endpoint): mandás texto, el sistema tokeniza, corre el modelo, decodifica, te devuelve texto. La única diferencia es cuánta de esa cadena controlás vos.

✏️ Para probar

Si tenés Python a mano: instalá transformers, corré el ejemplo de zero-shot-classification con una frase tuya y tres etiquetas inventadas. Fijate que el modelo nunca vio esas etiquetas en entrenamiento — igual te da un score por cada una.

Familia completa de tareas que vas a encontrar (texto, imagen, audio, multimodal) — no memorices la lista, volvé a ella cuando la necesites:

PipelineQué hace
text-generationCompleta un prompt
text-classificationAsigna una categoría a un texto
summarizationResume preservando lo esencial
translationTraduce entre idiomas
question-answeringExtrae una respuesta de un contexto dado
image-classification / automatic-speech-recognitionLo mismo, pero para imagen y audio

3. El truco de fondo: atención

El original deja esto para el capítulo 2. Acá lo adelantamos porque sin esta idea, todo lo que sigue (arquitecturas, inferencia) es memorizar nombres sin entender por qué existen.

Antes de los transformers (2017), los modelos de lenguaje procesaban texto palabra por palabra, en orden, arrastrando un "resumen" del contexto anterior (redes recurrentes, RNN/LSTM). El problema: ese resumen se degrada con frases largas — es como intentar recordar el principio de un párrafo largo mientras leés la última línea.

La autoatención (self-attention) resuelve esto de otra forma: en vez de comprimir todo el contexto en un resumen, cada palabra puede "mirar" directamente a cualquier otra palabra de la frase y decidir cuánto peso ponerle, todo en paralelo. Tomemos:

"El banco donde nos sentamos estaba frente al banco donde saqué la plata."

Para saber que el primer "banco" es un asiento y el segundo una entidad financiera, el modelo necesita mirar palabras lejanas ("sentamos", "saqué la plata"). La autoatención calcula, para cada palabra, un puntaje de relevancia contra todas las demás palabras de la frase, y usa esos puntajes para construir una representación de esa palabra que ya incorpora el contexto relevante — sin importar qué tan lejos esté.

Query, Key, Value: cómo se calcula ese puntaje y de dónde salen las matrices

Para entender de dónde salen estas letras sin misterio, hay que mirar el flujo de tensores. Cada token entra a una capa del transformer como un vector numérico continuo de dimensión hidden_size (denotado matemáticamente como dmodel — por ejemplo, 4.096 números en Llama 3 8B, o 2.048 en Gemma 2 2B). Ese vector es su embedding (que ya incluye la información de en qué posición de la frase está).

Dentro de cada capa de atención viven tres matrices de pesos entrenables: WQ, WK y WV. Cuando el vector del token x pasa por la capa, la GPU hace tres multiplicaciones de matrices en paralelo para proyectar ese token en tres roles funcionales distintos:

  • Query (Q) = x · WQ — "Qué estoy buscando": el vector de búsqueda que este token dispara para interrogar al resto de la frase.
  • Key (K) = x · WK — "Qué etiqueta ofrezco": el descriptor o índice con el que este token se hace visible para responder a las consultas de otros.
  • Value (V) = x · WV — "Qué información aporto": el contenido semántico real que este token transmitirá a quien decida prestarle atención.

La metáfora de fondo es un diccionario o base de datos clave-valor continua y diferenciable: hacés una consulta (Query), la contrastás contra las etiquetas de los registros disponibles (Keys), y según la afinidad o coincidencia que encuentres, extraés una combinación de sus contenidos (Values).

El puntaje de coincidencia entre el token i y el token j es el producto punto entre la Query de i y la Key de j (qi·kj⊤): un escalar que resulta alto y positivo cuando ambos vectores apuntan en la misma dirección en el espacio latente.

¿Por qué se divide por dk? Si dos vectores de dimensión dk tienen componentes aleatorias independientes con media 0 y varianza 1, su producto punto tiene media 0 pero varianza igual a dk. Con dimensiones típicas modernas como dk=128, los productos punto pueden llegar fácilmente a magnitudes de 30 o 40. Al aplicar softmax sobre valores tan dispares, la función se satura: el valor mayor se lleva un peso de 0,99999 y los demás quedan en 0. Peor aún para el entrenamiento: las derivadas del softmax en zonas saturadas son casi cero (desvanecimiento del gradiente), impidiendo que la red aprenda. Dividir por dk (por ejemplo, 128≈11,31) escala la varianza de vuelta a 1, manteniendo los puntajes en una zona donde el softmax distribuye atención de forma suave.

Un mini ejemplo numérico trabajado con tres tokens — quedémonos con "banco", "sentamos" y "plata" y miremos qué pasa cuando la Query del primer "banco" se compara con las Keys de los tres. Supongamos que los productos punto, ya divididos por √dk, dan 2,0 · 4,0 · 1,0:

Key de…Puntaje escaladoepuntajePeso (softmax)
banco2,07,397,39 / 64,71 ≈ 0,11
sentamos4,054,6054,60 / 64,71 ≈ 0,84
plata1,02,722,72 / 64,71 ≈ 0,04
salida(banco) = 0,11 · V_banco + 0,84 · V_sentamos + 0,04 · V_plata

El vector nuevo de "banco" es 84% "sentamos" — el modelo acaba de decidir, sin ninguna regla escrita a mano, que en esta frase "banco" es un asiento. Notá dos cosas: los pesos suman 1 (0,11 + 0,84 + 0,04 ≈ 1), y una diferencia de solo 2 puntos entre puntajes se convirtió en una diferencia de 8× entre pesos — así de agresivo es el softmax, y por eso importa el escalado por √dk.

Attention(Q,K,V)=softmax(QK⊤dk)V

Cómo se lee. Para cada Query, el producto punto contra todas las Keys (la matriz Q·Kᵀ tiene una fila por token y una columna por token), dividido por la raíz de la dimensión de las Keys para que el softmax no se sature, convertido en pesos que suman 1, y usados para promediar los Values. Es la tabla de arriba escrita para todos los tokens a la vez.

Atención de un token: Query contra Keys, softmax, suma de Values La Query de «banco» se compara con la Key de cada uno de los tres tokens; los puntajes 2,0, 4,0 y 1,0 pasan por softmax y dan los pesos 0,11, 0,84 y 0,04; la salida es la suma de los Values con esos pesos. 1 · Q CONTRA CADA K PUNTAJE 2 · SOFTMAX → PESO 3 · SUMAR LOS VALUES Q banco Q·K ÷ √dk K banco K sentamos K plata 2,0 4,0 1,0 0,11 0,84 0,04 V banco V sentamos V plata Σ salida(banco) salida(banco) = 0,11·V banco + 0,84·V sentamos + 0,04·V plata · los pesos suman 1
Un token, una head: la Query de «banco» se compara con la Key de cada token, el softmax convierte los tres puntajes en pesos que suman 1, y la salida es la suma de los Values con esos pesos. El grosor de las líneas es el peso: el «banco» nuevo sale hecho un 84 % de «sentamos».

De dónde sale head_dim y por qué Multi-Head Attention

Si el vector de un token tiene 4.096 dimensiones, ¿por qué no hacemos una sola gran atención con vectores de 4.096?

Porque una sola atención calcularía un único número escalar de similitud entre cada par de tokens. El lenguaje humano es inherentemente multidimensional: la palabra "banco" necesita atender a "sentamos" para resolver su rol semántico de asiento, a "estaba" para la concordancia de tiempo verbal, a "frente al" para la relación espacial, y al punto final para el cierre sintáctico de la oración. Un único escalar obligaría a promediar todas esas preguntas distintas en un solo número, borrando los matices.

La solución es Multi-Head Attention (MHA): proyectar el espacio total a múltiples sub-espacios paralelos llamados "cabezas" (heads).

  • Tomamos el tamaño total del embedding: hidden_size (ej. 4.096).
  • Definimos la cantidad de cabezas de atención: num_attention_heads (ej. 32).
  • El tamaño de cada sub-espacio es el cociente exacto entre ambos:
    head_dim = hidden_size // num_attention_heads = 4096 // 32 = 128

head_dim no es un hiperparámetro arbitrario: es la dimensión real del vector sobre el que opera cada cabeza de forma independiente. En Llama 3 8B, cada una de las 32 cabezas tiene vectores Q, K y V de 128 números. Una cabeza se especializa en concordancia de género/número; otra en dependencias a larga distancia; otra en puntuación.

Al terminar el cálculo en paralelo, las salidas de las 32 cabezas (cada una de tamaño 128) se concatenan lado a lado: 32×128=4.096. Ese vector reconstituido se multiplica por una última matriz aprendida, la proyección de salida WO∈R4096×4096, y vuelve a integrarse al flujo del modelo.

🟢 La trampa cuadrática de la atención: O(N²)

Si tu texto tiene N tokens, cada token genera una Query que debe multiplicarse contra las Keys de los N tokens. La matriz de puntajes QK⊤ tiene tamaño N×N.

Con 2.048 tokens, esa matriz tiene unos 4,19 millones de elementos. Con 131.072 tokens (128k de contexto), tiene 17.179 millones de elementos por cabeza y por capa. El costo de cómputo y la memoria temporal para calcular la atención crecen con el cuadrado de la longitud del texto (O(N2)). Todo el esfuerzo de ingeniería de la última década (FlashAttention, ventanas deslizantes, KV cache, GQA) existe para domar esta curva cuadrática.

4. Las tres formas de armar un transformer

Un transformer completo tiene dos mitades: un encoder (lee y entiende texto de entrada) y un decoder (genera texto de salida, palabra por palabra). Casi todo modelo que vas a usar es una de estas tres combinaciones:

Tres arquitecturas de transformer Encoder-only con atención bidireccional, decoder-only con atención causal, y encoder-decoder combinando ambas. Encoder-only — BERT, ModernBERT atención bidireccional · entiende texto completo Encoder clasificación / NER Decoder-only — GPT, Llama, Gemma, Claude atención causal · solo mira hacia atrás · genera token a token Decoder siguiente token Encoder-decoder — T5, BART el encoder entiende la entrada, el decoder genera la salida Encoder Decoder traducción / resumen
Las tres variantes de la arquitectura Transformer. Hoy, casi todo LLM de propósito general es decoder-only: la línea del medio es la que vas a ver en el 90% de los modelos con los que trabajes.
ArquitecturaAtenciónMejor paraEjemplos
Encoder-onlyBidireccional (ve toda la frase)Clasificación, NER, QA extractivoBERT, ModernBERT, RoBERTa
Decoder-onlyCausal (solo ve lo anterior)Generación de texto, chat, códigoGPT, Llama, Gemma, DeepSeek, Claude
Encoder-decoderBidireccional + causalTraducción, resumen, reescrituraT5, BART, mBART

🟢 Actualización 2026

El original le da el mismo peso a las tres familias. En la práctica actual, decoder-only ganó por goleada para propósito general: es más simple de escalar, y un decoder-only bien entrenado puede hacer traducción o resumen igual de bien con el prompt correcto, sin necesitar la mitad "encoder" separada. Los encoder-only sobreviven fuerte en un nicho específico: embeddings (buscar texto similar, RAG) y clasificación de alto volumen donde no necesitás generar texto, solo una etiqueta rápida y barata.

5. Cómo un LLM genera texto, token a token

Esta sección no existía en el curso original de 2022 — se agregó después porque, con LLMs, entender inferencia (cómo se genera texto, no cómo se entrena) se volvió tan importante como entender el entrenamiento. Es, seguramente, la parte más útil de este capítulo para vos si ya usás LLMs por API.

Generar una respuesta pasa por dos fases bien distintas:

1. Prefill

El modelo procesa todo tu prompt de una sola vez: lo tokeniza, lo convierte a vectores, corre una pasada por la red. Es la fase que determina cuánto tardás en ver el primer token (time to first token).

2. Decode

El modelo genera un token, lo agrega a la secuencia, y repite: cada token nuevo depende de todos los anteriores. Es autoregresivo — por eso ves las respuestas "aparecer" de a poco en un chat.

Prefill y decode con KV cache El prefill procesa los cinco tokens del prompt en una sola pasada y escribe sus K y V en el cache. Cada paso de decode recibe solo el token nuevo, lee todas las columnas anteriores del cache, escribe una columna más y produce el siguiente token, que se realimenta como entrada del paso siguiente. Prefill todo el prompt, una pasada La capital de Francia es París 1er token el token generado vuelve como entrada Decode · paso 1 entra solo el token nuevo París lee 5 columnas, escribe 1 . Decode · paso 2 y así hasta el fin de secuencia . lee 6 columnas, escribe 1 <eos> K y V escritos en este paso K y V reutilizados del cache (no se recalculan)
Prefill procesa el prompt entero en paralelo y deja el KV cache lleno: de ahí sale el primer token y el time to first token. Cada paso de decode recibe solo el token nuevo, lee todo el cache y agrega una columna. El cache crece una columna por token, y por eso la VRAM, no el cómputo, es lo que limita el contexto.

En cada paso del decode, el modelo no elige "la" siguiente palabra — calcula una probabilidad para cada palabra posible de su vocabulario y después aplica una estrategia de muestreo para elegir una:

  • Temperature: más alta = más aleatorio/creativo, más baja = más determinista y repetible.
  • Top-p (nucleus sampling): solo considera las palabras cuya probabilidad acumulada llega a, por ejemplo, el 90%, descartando la cola larga de opciones improbables.
  • Top-k: variante más simple — solo mira las k palabras más probables.
  • Penalización por repetición: baja artificialmente la probabilidad de palabras ya usadas, para que el modelo no quede dando vueltas en el mismo lugar.

Estos son, literalmente, los parámetros que ves en cualquier API de LLM (temperature, top_p, max_tokens). Ahora sabés qué está pasando adentro cuando los tocás. Valores de partida razonables — después ajustás mirando la salida, no la teoría:

PerillaQué hace por dentroValores típicos
temperatureDivide los logits antes del softmax: <1 afila la distribución (más determinista), >1 la aplana (más azar). 0 equivale a elegir siempre el más probable (greedy).0–0,3 para extracción, código o JSON; 0,7–1,0 para conversación y escritura
top_pOrdena las opciones por probabilidad y se queda con el menor conjunto cuya suma llega a p — la cola se descarta.0,9–0,95 (1,0 = sin recorte)
top_kSe queda con los k tokens más probables, sin mirar cuánta probabilidad acumulan.40–50 (transformers usa 50 por defecto); muchas APIs ni lo exponen
repetition_penaltyMultiplica hacia abajo la probabilidad de tokens que ya aparecieron en el contexto.1,0 = apagado; 1,1–1,2 si el modelo entra en bucle; más que eso empieza a degradar el texto

Las perillas se combinan: primero se aplica la penalización por repetición y la temperatura, después top_k y top_p recortan la lista, y recién ahí se muestrea. Con temperature=0 (o do_sample=False en transformers) el resto no interviene — siempre sale el token más probable.

Qué le hace cada perilla a la distribución del siguiente token Cinco paneles con la misma distribución de ocho tokens candidatos: la base con temperatura 1; temperatura 0,5, que afila la distribución; temperatura 2, que la aplana; top-k igual a 3, que descarta todo salvo los tres más probables; y top-p igual a 0,9, que descarta la cola una vez acumulado el 90 por ciento de la probabilidad. T = 1 (base) cada barra: un token candidato T = 0,5 · afila los altos suben, la cola cae T = 2 · aplana la cola gana masa top_k = 3 quedan los 3 más probables fuera top_p = 0,9 quedan los que suman 90 % fuera tokens candidatos, ordenados por probabilidad → después del recorte la masa que queda se renormaliza, y el token se sortea según la altura de las barras
La misma distribución del siguiente token bajo cada perilla. La temperatura cambia la forma (afila o aplana antes de sortear); top_k y top_p no cambian la forma, cortan la cola: uno por cantidad fija, el otro por masa de probabilidad acumulada.

🟢 Prefill vs. Decode: el choque físico entre cómputo y ancho de banda

Generar texto expone dos regímenes de hardware totalmente opuestos:

  • Prefill (Compute-bound): El prompt entra entero. Para 1.000 tokens de prompt, la GPU multiplica matrices densas de 1.000×4.096 contra los pesos del modelo (operaciones tipo GEMM). La GPU trabaja en su punto óptimo: los núcleos tensoriales están 100% ocupados porque la intensidad aritmética (cantidad de operaciones matemáticas por cada byte leído de memoria) es altísima. El límite acá son los TFLOPs de cómputo.
  • Decode (Memory-bandwidth bound): Genera un solo token por vez (N=1). Para calcular el siguiente token de un vector de 1×4.096, la GPU está obligada a leer todos los pesos de las 32 capas desde la VRAM a los núcleos de cómputo (en un modelo de 8B en 16 bits, son ~16 GB de pesos transferidos por cada token emitido). La GPU casi no hace cuentas matemáticas: pasa el 95% del tiempo esperando que los datos viajen por el bus de memoria. El límite acá no son los TFLOPs, sino los GB/s de ancho de banda de la memoria (memory bandwidth).

El KV Cache desarmado: ¿por qué K y V, y por qué NUNCA Q?

Imaginemos qué pasaría en el paso de decode sin optimizar. Supongamos que ya se emitieron 100 tokens y la red va a predecir el token 101.

Para que el nuevo token sepa qué responder, debe calcular la atención contra todos los tokens anteriores (posiciones 1 a 100). Si no guardáramos nada en memoria, tendríamos que volver a pasar los 100 tokens previos por las 32 capas de la red. Para el token 102, pasaríamos 101 tokens. Para el token 103, 102 tokens. El costo de generar T tokens escalaría de forma cuadrática: O(T2).

Pero hay un principio fundamental que lo cambia todo: la atención causal. En un modelo decoder-only, los tokens pasados tienen estrictamente prohibido mirar al futuro (sus conexiones hacia adelante están enmascaradas). Eso significa que cuando entra el token 101, las representaciones intermedias de los tokens 1 al 100 en cada capa son idénticas e inmutables. El token 5 siempre produjo la misma Key y el mismo Value en la capa 12, tanto en el paso 10 como en el paso 100.

Ahora miremos qué pasa en el paso 101 a nivel de atención:

  • El token nuevo 101 genera su vector Query (q101), su Key (k101) y su Value (v101).
  • ¿Quién es el que pregunta en este paso? Únicamente el token 101. Él busca información con su q101.
  • ¿A quiénes consulta? A los tokens pasados (1 al 100) más a sí mismo. Compara su q101 contra k1,…,k100,k101.
  • ¿Qué extrae si hay coincidencia? Pondera los valores v1,…,v100,v101.

¿Por qué se guardan K y V? Porque todas las futuras Queries que nazcan en los pasos 102, 103, 104... van a necesitar volver a consultar las Keys y Values de todos los tokens precedentes. Si los guardamos en una memoria rápida (el KV Cache), cada paso de decode solo tiene que procesar el token nuevo (1 token hacia adelante) y concatenar su nueva k y v al cache existente.

¿Y por qué NO se guarda Query (Q)? Porque una vez que el token 101 terminó de consultar al pasado y el modelo emitió la distribución para el token 102, la Query q101 no le sirve a nadie más en la historia del universo. El siguiente paso tendrá su propia Query (q102). Ningún token futuro compara nada contra la Query vieja de un token anterior. Guardar Q sería tirar un 33% más de memoria a la basura sin ninguna utilidad funcional.

La anatomía del tensor en VRAM y la cuenta real

En la memoria de la GPU, el KV Cache no es un concepto abstracto: es un tensor concreto reservado por capa. Para cada capa l, la GPU mantiene dos tensores (uno para K, uno para V) con dimensiones:

shape = [batch_size, num_kv_heads, max_seq_len, head_dim]

Ahora desarmemos término por término la famosa fórmula de memoria:

bytes por token = 2 × capas × kv_heads × head_dim × bytes_por_numero
  • 2: Porque guardamos exactamente dos tensores: el vector Key y el vector Value.
  • capas (L): Cada una de las 32 capas extrae transformaciones distintas, por lo que cada capa tiene su propio par de matrices WK y WV y produce representaciones K y V diferentes.
  • kv_heads: La cantidad de cabezas de atención asignadas a K y V.
  • head_dim: La longitud de cada vector individual por cabeza (dk=hidden_size/num_attention_heads).
  • bytes_por_numero: La precisión numérica utilizada. En BF16 o FP16 son 2 bytes; en FP8 es 1 byte; en INT4 son 0,5 bytes.

Veamos la cuenta con Llama 3 8B (32 capas, 8 KV heads, head_dim=128, en BF16 con 2 bytes):

2 × 32 capas × 8 kv_heads × 128 head_dim × 2 bytes = 131.072 bytes = 128 KiB por token

Multiplicá eso por la longitud de contexto:

  • A 8.192 tokens: 128 KiB×8.192=1.073.741.824 bytes=1 GiB.
  • A 131.072 tokens (128k): 128 KiB×131.072=16 GiB ¡solo para el KV Cache de una sola conversación!

Si sumás los ~16 GB de los pesos del modelo en BF16 más los 16 GB del KV Cache a 128k, necesitás 32 GB de VRAM para atender a un solo usuario. Si quisieras atender 4 usuarios en simultáneo con contexto lleno, necesitarías 16+(4×16)=80 GB de VRAM. Por eso el KV Cache es el factor determinante en el costo y la viabilidad de servir modelos.

MHA vs. MQA vs. GQA: la solución arquitectónica

Fijate en el factor más modificable de la fórmula: kv_heads.

  • MHA (Multi-Head Attention clásica): Cada cabeza de Query tiene su propia cabeza de Key y Value. En Llama 3 8B tendríamos 32 cabezas de Query y 32 cabezas de K/V. El cache ocuparía 2×32×32×128×2=512 KiB por token (¡cuatro veces más, 64 GB a 128k!).
  • MQA (Multi-Query Attention - Shazeer, 2019): Todas las cabezas de Query comparten una única cabeza de Key y Value (kv_heads=1). El cache se reduce 32 veces (16 KiB por token), pero el modelo pierde expresividad y sufre en tareas complejas de razonamiento o múltiples hechos en contexto largo.
  • GQA (Grouped-Query Attention - Ainslie et al., 2023): El estándar de la industria hoy (Llama 3, Gemma 2/4, Mistral, Qwen 2.5). Se dividen las 32 cabezas de Query en 8 grupos de 4. Las 4 cabezas de Query de cada grupo comparten un único par K/V (kv_heads=8). El resultado es el Santo Grial: un 75% menos de memoria en KV Cache comparado con MHA, con una calidad de razonamiento y perplejidad virtualmente idéntica.

Y si además guardás el KV Cache en 8 bits (usando FP8 con el flag --kv-cache-dtype fp8 en vLLM), el multiplicador de bytes baja de 2 a 1, reduciendo la memoria de nuevo a la mitad: de 16 GiB a 8 GiB a 128k tokens.

6. Sesgos y límites reales

Todo modelo entrenado con texto masivo de internet (o incluso con datos "curados" como Wikipedia) hereda los sesgos de esos datos. El ejemplo clásico del curso original sigue siendo ilustrativo:

from transformers import pipeline

unmasker = pipeline("fill-mask", model="bert-base-uncased")
unmasker("This man works as a [MASK].")
# → lawyer, carpenter, doctor, waiter, mechanic
unmasker("This woman works as a [MASK].")
# → nurse, waitress, teacher, maid, prostitute

BERT se entrenó con Wikipedia y BookCorpus — fuentes que en 2018 se consideraban "neutras" comparadas con scrapear todo internet. Igual el sesgo aparece. La conclusión del curso original sigue siendo válida al 100%: fine-tunear el modelo con tus propios datos no borra el sesgo de base, porque ese sesgo vive en los pesos preentrenados, no en la capa que vos agregás encima.

🟢 Actualización 2026

Lo nuevo desde entonces no es que el problema se haya resuelto — es que aparecieron capas de mitigación que lo hacen menos visible en el uso cotidiano: RLHF y post-entrenamiento de alineación (por qué Claude o GPT no te contestan lo mismo que un modelo base sin ajustar), guardrails a nivel de aplicación, y RAG (anclar la respuesta a documentos reales en vez de dejar que el modelo "recuerde" de memoria, que es donde más alucina). Ninguna de estas capas elimina el sesgo de fondo — lo empujan hacia abajo en la pila.

Cuando una alucinación tiene consecuencias reales (un número financiero equivocado, por ejemplo) las capas de arriba no alcanzan — hace falta verificación explícita. Ver el gate anti-alucinación para un caso real, con un incidente concreto de por medio.

7. Errores comunes

Cosas que van a pasarte la primera vez que uses estas piezas a mano — y cómo leerlas.

SíntomaCausa probableQué hacer
text-generation devuelve una respuesta cortada o casi vacíamax_length cuenta el prompt; con un prompt largo no queda lugar para generarUsar max_new_tokens (§2)
El modelo repite la misma frase en bucleDecodificación greedy o temperatura muy baja sin penalización por repeticiónMuestrear con temperature≈0,7, top_p≈0,9 y repetition_penalty≈1,1 (§5)
Texto incoherente, palabras inventadasTemperatura > 1 o top_p/top_k demasiado permisivos: la cola improbable entra en juegoBajar la temperatura a ≤ 1,0 y recortar con top_p 0,9 (§5)
Le hacés una pregunta y el modelo la "continúa" en vez de contestarlaEs un modelo base, no instruct: solo aprendió a completar textoUsar la variante -Instruct y su chat template (Capítulo 2, §3)
La conversación se pone lenta o revienta la VRAM a medida que creceEl KV cache crece linealmente con el contexto; el cómputo de atención, cuadráticamenteAcotar el contexto, elegir modelos con GQA, KV cache en 8 bits (§5)
zero-shot-classification da un score alto a una etiqueta absurdaSiempre reparte probabilidad entre las etiquetas que le diste — no tiene opción "ninguna"Agregar una etiqueta "otro" o exigir un umbral mínimo (§2)
Después de fine-tunear, el modelo sigue mostrando el sesgo del originalEl sesgo vive en los pesos preentrenados, no en la capa que ajustasteMitigar en la aplicación (RAG, verificación, guardrails), no esperar que el fine-tune lo borre (§6)

8. Resumen y mapa mental

Si te quedás con cinco ideas de este capítulo, que sean estas:

  1. NLP es el campo; LLM es un tipo de modelo de NLP que resuelve muchas tareas con un solo modelo grande.
  2. pipeline() siempre hace lo mismo: tokenizar → correr el modelo → decodificar. Es el mismo patrón detrás de cualquier API de LLM que uses.
  3. La autoatención deja que cada palabra mire directamente a cualquier otra, sin importar la distancia — por eso reemplazó a las redes recurrentes.
  4. Hay tres arquitecturas (encoder / decoder / encoder-decoder), pero hoy casi todo lo que usás es decoder-only.
  5. Generar texto tiene dos fases (prefill y decode) y un puñado de perillas (temperature, top-p, top-k) que ahora sabés qué hacen de verdad.
  6. El KV cache es memoria pura: 2 × capas × kv_heads × head_dim × bytes por token — y GQA es la razón por la que un modelo de 8B entra con 128k de contexto en una sola GPU.

El capítulo original de Hugging Face sigue con "Cómo 🤗 Transformers resuelve tareas" y un quiz. Lo primero se cubre en el Capítulo 2 (tokenizer, cuerpo del modelo, head); el quiz lo tenés acá abajo, en Ponete a prueba.

9. Ponete a prueba

Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.

1. ¿Por qué un modelo decoder-only puede traducir o resumir si "solo" aprendió a predecir el siguiente token?

Porque a la escala de un LLM, predecir bien el siguiente token exige haber aprendido la estructura del lenguaje de forma tan general que cualquier tarea se puede plantear como "continuá este texto": el prompt describe la tarea y el modelo la completa. Por eso no hace falta un head ni un modelo distinto por tarea, que era la norma con BERT. (§1, §4)

2. En el ejemplo de tres tokens, los puntajes escalados eran 2,0 · 4,0 · 1,0. ¿Qué pasaría con los pesos si no dividieras por √dk y los puntajes crudos fueran 16 · 32 · 8?

El softmax se satura: e32 es tan mayor que e16 que el peso de "sentamos" se va a ≈1,0 y los otros dos a ≈0. El token deja de mezclar contexto y, peor, el gradiente por esas posiciones se vuelve casi cero — el escalado existe justamente para mantener los puntajes en un rango donde el softmax reparte y aprende. (§3)

3. ¿Cuál de las dos fases de generación determina el time to first token, y cuál se hace más lenta a medida que la respuesta crece?

El prefill (procesar todo el prompt de una vez) determina el tiempo hasta el primer token. El decode es autoregresivo: cada token nuevo atiende a todos los anteriores, así que el trabajo por token — y el KV cache — crece con la longitud de la conversación. (§5)

4. Calculá el KV cache de Llama 3 8B en bf16 con 32.768 tokens de contexto.

128 KiB por token × 32.768 tokens = 4 GiB. Si el mismo modelo usara MHA (32 KV heads en vez de 8) serían 16 GiB; con el cache en fp8, 2 GiB. (§5)

5. ¿Por qué se cachean las Keys y Values pero nunca las Queries, y por qué las Keys y Values viejas no necesitan recalcularse?

Porque en un decoder causal los tokens pasados no pueden mirar hacia adelante: la representación de los tokens anteriores en cada capa es matemáticamente inmutable. En cada paso de decode, solo el token nuevo genera una Query para interrogar al pasado; una vez emitido el nuevo token, esa Query ya cumplió su función y no le sirve a ningún paso posterior. Las Keys y Values, en cambio, son el catálogo que todos los tokens futuros van a seguir consultando. (§5)

6. Un modelo te devuelve la misma frase repetida cinco veces. ¿Qué perillas mirás primero y en qué dirección?

Primero temperature: si está en 0 o muy baja, el modelo siempre elige el token más probable y un bucle se retroalimenta. Subirla a ≈0,7 con top_p ≈0,9 suele alcanzar; si persiste, repetition_penalty ≈1,1. Ir más allá de 1,2–1,3 en la penalización empieza a romper el texto. (§5)

7. Fine-tuneaste BERT con datos propios, sin sesgo de género. ¿Por qué el ejemplo de [MASK] sigue dando "nurse" para "woman"?

Porque el fine-tune ajusta (o agrega) una capa encima de los pesos preentrenados, y el sesgo vive en esos pesos, aprendidos de Wikipedia y BookCorpus. Las mitigaciones reales operan en otro nivel: alineación post-entrenamiento, RAG para anclar respuestas a documentos, guardrails y verificación explícita en la aplicación. (§6)

10. Para practicar

✏️ Ejercicio 1 — max_length vs. max_new_tokens, medido

Corré el mismo prompt con las dos perillas y contá tokens. Corre en CPU (el modelo pesa ~700 MB la primera vez). Deberías ver que con max_length=30 la respuesta es más corta que con max_new_tokens=30, y que la diferencia es exactamente la longitud del prompt en tokens.

from transformers import pipeline, AutoTokenizer

model_id = "HuggingFaceTB/SmolLM2-360M"
tok = AutoTokenizer.from_pretrained(model_id)
gen = pipeline("text-generation", model=model_id)

prompt = "En este curso vamos a aprender a usar transformers para"
print("tokens del prompt:", len(tok(prompt)["input_ids"]))

for kwargs in ({"max_length": 30}, {"max_new_tokens": 30}):
    salida = gen(prompt, do_sample=False, **kwargs)[0]["generated_text"]
    print(kwargs, "→", len(tok(salida)["input_ids"]), "tokens en total")
Pista / qué esperar

Con max_length=30 el total da 30 (o menos, si el modelo emite fin de secuencia antes); con max_new_tokens=30 da prompt + 30. Probá después con do_sample=True, temperature=0.7, top_p=0.9 y corré tres veces: la salida cambia cada vez — con do_sample=False, nunca.

✏️ Ejercicio 2 — el softmax del ejemplo, y qué le hace la temperatura

Reproducí la tabla de §3 en NumPy y después dividí los puntajes por distintas temperaturas. Solo NumPy, sin modelos.

import numpy as np

def softmax(x):
    e = np.exp(x - x.max())
    return e / e.sum()

puntajes = np.array([2.0, 4.0, 1.0])   # banco, sentamos, plata
print(softmax(puntajes).round(3))          # ≈ [0.114 0.844 0.042]

for T in (0.2, 0.5, 1.0, 2.0, 5.0):
    print(T, softmax(puntajes / T).round(3))
Pista / qué esperar

Con T = 0,2 el peso de "sentamos" se va a ≈1,0 (esto es lo que hace temperature baja al muestrear: casi greedy). Con T = 5 los tres pesos se acercan a 0,33 — la distribución se aplana y cualquier token puede salir. Es la misma fórmula, con los mismos puntajes; solo cambió el divisor.

✏️ Ejercicio 3 — el KV cache de cualquier modelo, desde su config.json

Cargá solo la configuración (sin pesos) y aplicá la fórmula de §5, capa por capa: en Gemma 4 el número de cabezas KV, el head_dim y la ventana cambian entre capas locales y globales. Compará cuánto ocupa el contexto en un modelo homogéneo y en Gemma 4.

from transformers import AutoConfig

def kv_cache_bytes(model_id, contexto, bytes_por_valor=2):
    c = AutoConfig.from_pretrained(model_id).get_text_config()      # en multimodales, el config del texto
    capas = c.num_hidden_layers
    tipos = getattr(c, "layer_types", None) or ["full_attention"] * capas
    ventana = getattr(c, "sliding_window", None) or contexto
    k_eq_v = getattr(c, "attention_k_eq_v", False)                  # Gemma 4: las globales guardan solo K
    compartidas = getattr(c, "num_kv_shared_layers", 0) or 0         # Gemma 4 E2B/E4B: las últimas N no guardan
    total = 0
    for i in range(capas - compartidas):
        lc = c.per_layer_config[i]                                    # kv_heads y head_dim pueden variar por capa
        kv_heads = getattr(lc, "num_key_value_heads", None) or lc.num_attention_heads
        head_dim = getattr(lc, "head_dim", None) or lc.hidden_size // lc.num_attention_heads
        if tipos[i] == "sliding_attention":
            total += 2 * kv_heads * head_dim * bytes_por_valor * min(contexto, ventana)
        else:
            total += (1 if k_eq_v else 2) * kv_heads * head_dim * bytes_por_valor * contexto
    return total

for m in ["HuggingFaceTB/SmolLM2-360M", "Qwen/Qwen2.5-0.5B", "google/gemma-4-E2B-it", "google/gemma-4-31B-it"]:
    print(f"{m:28s}", " | ".join(f"{kv_cache_bytes(m, n) / 2**30:5.2f} GiB @ {n // 1024}K" for n in (8192, 32768, 131072)))
Pista / qué esperar

Salida en transformers 5.16 (los configs se descargan solos, sin pesos): SmolLM2-360M 0,31 / 1,25 / 5,00 GiB; Qwen2.5-0.5B 0,09 / 0,38 / 1,50; Gemma 4 E2B 0,05 / 0,19 / 0,76; Gemma 4 31B 1,09 / 2,03 / 5,78 GiB a 8K / 32K / 128K. Cuando num_key_value_heads es menor que num_attention_heads el modelo usa GQA y el cache baja en proporción. Y fijate por qué el 31B, con 60 capas y 16 cabezas KV, ocupa menos que SmolLM2 a 128K: 50 de sus capas guardan solo 1024 tokens. Si leés c.num_key_value_heads directo en el 31B, transformers 5.16 lanza AmbiguousGlobalPerLayerAttributeError: desde esa versión esos dos atributos son por capa, que es exactamente el supuesto que rompió el backend de tus fixes de vLLM.

11. Fuentes y para seguir