Módulo 00 · Fundamentos

Cómo funciona un LLM

Antes de reentrenar nada hay que entender qué está pasando por dentro. Un modelo de lenguaje no "sabe" respuestas: recibe una secuencia de tokens, calcula una distribución de probabilidad sobre el vocabulario y predice el siguiente token. Todo lo demás — fine-tuning, LoRA, cuantización — modifica o aprovecha esa única mecánica. Este módulo fija el modelo mental y la matemática mínima para distinguir qué cambia el entrenamiento y qué solo afecta la inferencia, y te dice dónde leerlo y cómo practicarlo — incluido correrlo en un teléfono con Rust.

Al terminar sabrás

  • Trazar el camino tokens → embeddings → atención → logits → siguiente token.
  • Leer la fórmula de la atención causal y por qué la máscara impide "ver el futuro".
  • Distinguir temperature, top_k y top_p como decisiones de inferencia, no de entrenamiento.
  • Explicar qué guarda el KV cache y por qué convierte un costo cuadrático en lineal.
  • Separar tres cosas que se confunden: pesos, activaciones y contexto.
  • Reproducir un forward de Qwen3-0.6B y ver los logits reales — en Python y en Rust.

1La tubería de una predicción

El modelo recibe texto ya convertido en token ids (enteros). Cada id se transforma en un vector denso mediante una tabla de embeddings $E \in \mathbb{R}^{V \times d}$, donde $V$ es el tamaño del vocabulario y $d$ la dimensión oculta. La representación inicial de la secuencia es simplemente la fila correspondiente de $E$ para cada token:

$$ h_0 = E[x] \qquad x = (x_1, x_2, \dots, x_t),\quad x_i \in \{0,\dots,V-1\} $$

Esos vectores pasan por una pila de bloques Transformer. Cada bloque hace dos cosas: una capa de atención que mezcla información entre posiciones (cada token mira a los anteriores), y un MLP (feed-forward) que transforma cada posición por separado. El MLP es donde vive la mayor parte de los parámetros del modelo, y ambas subcapas se envuelven con conexiones residuales y normalización para que el entrenamiento sea estable en decenas de capas. Al final, la representación de la última posición se proyecta al vocabulario para producir los logits, un número por cada token posible.

token_ids → embeddings → [ bloque × N: atención causal + MLP + residual/norm ] → logits → softmax → p(siguiente token)
El único trabajo del modelo: dado lo anterior, ¿cuál es el próximo token?

2Atención causal — el corazón del Transformer

Cada posición construye tres vectores a partir de su representación $h$: una consulta (query), una clave (key) y un valor (value), mediante matrices aprendidas:

$$ Q = h\,W_Q, \qquad K = h\,W_K, \qquad V = h\,W_V $$

La atención compara cada query con todas las keys (producto punto), normaliza con softmax y usa esos pesos para promediar los values. El escalado por $\sqrt{d_k}$ evita que los productos punto crezcan demasiado (donde $d_k$ es la dimensión por cabeza, que veremos abajo). La máscara causal $M$ pone $-\infty$ en las posiciones futuras, de modo que el token en la posición $i$ solo puede mirar hacia atrás ($j \le i$):

$$ \mathrm{Attention}(Q,K,V) = \mathrm{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}} + M\right) V, \qquad M_{ij} = \begin{cases} 0 & j \le i \\ -\infty & j > i \end{cases} $$

Se usan varias cabezas en paralelo (multi-head): con $H$ cabezas, cada una opera en dimensión $d_k = d/H$ sobre su propia proyección, y sus salidas se concatenan y se proyectan con $W_O$:

$$ \mathrm{MHA}(h) = \big[\,\mathrm{head}_1;\dots;\mathrm{head}_H\,\big]\,W_O $$
Nota · Qwen3

Qwen3 no suma un vector de posición al embedding: usa RoPE (rotary position embedding), que rota los pares $(q,k)$ por un ángulo dependiente de la posición dentro de la atención. Así el producto punto codifica la posición relativa. Lo verás de nuevo en el KV cache y en el runtime on-device.

3De logits a probabilidades

La representación final de la última posición, $h_L$, se proyecta con la matriz de "des-embedding" $W_U \in \mathbb{R}^{d \times V}$ para obtener el vector de logits $z$. El softmax lo convierte en una distribución de probabilidad válida (suma 1) sobre todo el vocabulario $\mathcal{V} = \{0,\dots,V-1\}$:

$$ z = h_L\,W_U, \qquad p(x_{t+1}=v \mid x_{\le t}) = \mathrm{softmax}(z)_v = \frac{e^{z_v}}{\sum_{j \in \mathcal{V}} e^{z_j}} $$

Aquí está la frontera conceptual más importante del curso: el entrenamiento mueve estos logits (ajustando los pesos para que el token correcto reciba más probabilidad); la inferencia solo elige entre las probabilidades que ya existen.

4Muestreo: temperature, top-k, top-p

Con la distribución en mano hay que elegir un token. La opción más simple es greedy: tomar el más probable.

$$ x_{t+1} = \arg\max_{v}\; z_v $$

La temperatura $T$ reescala los logits antes del softmax: $T \lt 1$ agudiza la distribución (más determinista), $T \gt 1$ la aplana (más creativa/aleatoria):

$$ p_i \;\propto\; \exp\!\left(\frac{z_i}{T}\right) $$
  • top-k: se conservan solo los $k$ tokens más probables y se re-normaliza.
  • top-p (nucleus): se conserva el conjunto más pequeño cuya probabilidad acumulada alcanza $p$, es decir $\sum p \ge p_{\text{top}}$.
Cuidado

Estos parámetros no son parte del modelo entrenado: son ajustes de generación. Cambiarlos nunca "reentrena" nada. Si el modelo dice tonterías con temperature=1.2, bájala antes de culpar al fine-tuning.

5KV cache: por qué la generación es viable

Generar token a token repetiría el cálculo de la atención sobre toda la secuencia en cada paso — costo cuadrático. El KV cache guarda las claves y valores ya calculados $K_{\le t}, V_{\le t}$; en el paso siguiente solo se computa la query nueva $q_{t}$ y se atiende contra el caché. El costo por token pasa de recomputar $O(t^{2}\,d)$ a $O(t\,d)$:

$$ \underbrace{O(t^{2}\,d)}_{\text{sin caché, por paso}} \;\longrightarrow\; \underbrace{O(t\,d)}_{\text{con KV cache}} $$

A cambio, el caché ocupa memoria que crece con la longitud del contexto — un tema central cuando llevemos el modelo a un dispositivo (Módulo 10).

6Tres cosas que no son lo mismo

ConceptoQué esLo cambia…
PesosLos parámetros aprendidos ($E, W_Q, W_U$…). Fijos tras entrenar.El entrenamiento / fine-tuning
ActivacionesLos vectores intermedios ($h$) que aparecen al procesar una entrada.Cada entrada distinta
ContextoLos tokens que le pasas (prompt + lo generado) dentro de la ventana.Lo que escribes / RAG

Fine-tuning cambia pesos. RAG (Módulo 8) cambia contexto. Confundirlos es la causa número uno de meter en el modelo algo que debía ir en el prompt.

En el teléfono

Todo este ciclo — embeddings, atención, softmax, muestreo — es exactamente lo que corre en el dispositivo cuando el modelo es on-device: no hay servidor. El modelo va cuantizado (Módulo 9) y un runtime como llama.cpp hace el forward, el softmax y el muestreo en el CPU/GPU del teléfono. Cada concepto de este módulo (el KV cache que come RAM, la temperatura, greedy vs muestreo) es una perilla que controlas desde la app.

Cómo practicar
  1. Instala el entorno: pip install transformers torch (o usa un notebook en Colab, gratis).
  2. Carga el modelo y su tokenizer: AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B") y AutoTokenizer.from_pretrained(...).
  3. Tokeniza un prompt y mira los input_ids — son los enteros del §1.
  4. Haz un forward y extrae logits[0, -1] (la última posición). Aplícale softmax y mira los 10 tokens más probables.
  5. Juega con la temperatura: divide los logits por T ∈ {0.5, 1.0, 2.0} antes del softmax y observa cómo la distribución se agudiza o se aplana.
Herramientas: Python · transformers · torch · (opcional) Google Colab
Rust · on-device

La tokenización que usas en Python ya es Rust por debajo: la librería tokenizers de Hugging Face está escrita en Rust. Y el forward completo puedes correrlo en Rust con candle (el framework de ML de Hugging Face), que carga pesos de Qwen y calcula logits sin tocar Python — ideal para empaquetarlo en una app.

// candle: cargar Qwen y sacar los logits del último token
let logits = model.forward(&input_ids, 0)?;   // [batch, seq, vocab]
let last = logits.i((0, seq_len - 1))?;        // logits del siguiente token
let probs = softmax(&last, D::Minus1)?;      // distribución

En este curso el entrenamiento lo haremos en Python (es donde está el ecosistema maduro), pero la inferencia on-device es territorio de Rust — y a eso volvemos en serio en el Módulo 10.

Lecturas y recursos

Ejercicios

De menor a mayor complejidad. El último es el que hace un practicante de verdad.

Ejercicio 1 · calentamiento

Tokeniza y cuenta

Carga el tokenizer de Qwen3-0.6B y tokeniza tres frases. Imprime los token_ids y su longitud.

Entrega: tabla frase → nº de tokens.   Pista: AutoTokenizer.from_pretrained + tokenizer(text)["input_ids"].

Ejercicio 2

Softmax a mano

Implementa softmax(z) en NumPy con estabilidad numérica (resta el máximo) y verifica que suma 1 para un vector de logits aleatorio.

Entrega: función + assert de que $\sum p = 1$.   Pista: $\mathrm{softmax}(z) = e^{z-\max z}/\sum e^{z-\max z}$.

Ejercicio 3

Efecto de la temperatura

Toma un vector de logits fijo y grafica la distribución resultante para $T \in \{0.5, 1.0, 2.0\}$. Observa cómo se agudiza o aplana.

Entrega: una figura con tres distribuciones.   Pista: aplica $z/T$ antes del softmax.

Ejercicio 4

Los logits reales del modelo

Haz un forward de Qwen3-0.6B sobre un prompt, extrae los logits de la última posición y muestra los 10 tokens más probables con su probabilidad.

Entrega: top-10 (token decodificado, prob).   Pista: model(**inputs).logits[0, -1] → softmax → topk(10).

Ejercicio 5 · ejercicio top

Instrumenta un forward completo con KV cache

Para un prompt fijo: (1) extrae los logits de la última posición y grafica la distribución del siguiente token para T ∈ {0.0, 0.7, 1.0} y top_p ∈ {1.0, 0.9}, lado a lado. (2) Implementa a mano un KV cache y demuestra que la generación es idéntica a la ruta sin caché, midiendo la latencia por token frente a la longitud de secuencia (mostrando el paso de $O(t^2)$ a $O(t)$).

Entrega: una figura con las 4 distribuciones de muestreo + un gráfico latencia-vs-longitud.   Pista: usa use_cache=True y guarda past_key_values entre pasos; compara salida token a token con use_cache=False.

Puntos clave
  • Un LLM predice el siguiente token: tokens → logits → softmax → distribución.
  • La atención causal deja que cada posición mire solo hacia atrás (máscara $-\infty$).
  • El entrenamiento mueve los logits; temperature/top_p/top_k solo eligen entre ellos.
  • El KV cache vuelve la generación lineal por token, a costa de memoria que crece con el contexto.
  • Pesos ≠ activaciones ≠ contexto. Fine-tuning toca pesos; RAG toca contexto.
  • El mismo ciclo corre on-device; en Rust lo mueven candle y tokenizers.