Curso / HF LLM Course / Capítulo 2
● piloto de formato

Hugging Face · LLM Course · Capítulo 2

Usando Transformers: abrir la caja negra de pipeline()

El Capítulo 1 te mostró qué hace pipeline(). Este capítulo te muestra cómo, con las dos piezas que lo componen — tokenizer y modelo — manejadas a mano, y termina con cómo se sirve todo esto cuando ya no es un experimento sino un servicio corriendo 24/7.

0. Abrir la caja negra

Todo lo que sigue es, literalmente, desarmar la función pipeline("sentiment-analysis") del Capítulo 1 en sus tres pasos — preprocesar, correr el modelo, postprocesar — y mostrar cada uno por separado. La razón para hacer este ejercicio no es académica: el día que necesites algo que pipeline() no ofrece de fábrica (batching custom, un head propio, streaming token a token), vas a estar trabajando exactamente en esta capa.

1. Detrás del pipeline

Los modelos de transformers no leen texto — leen números. El primer trabajo es siempre de un tokenizer, que convierte texto crudo en input_ids (y algunos metadatos más), usando AutoTokenizer:

from transformers import AutoTokenizer

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

raw_inputs = ["Llevo toda mi vida esperando un curso así.", "Odio esto tanto."]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")

El resultado es un diccionario con input_ids (los números) y attention_mask (qué posiciones son texto real vs. relleno — sección 4). Ese diccionario se pasa directo al modelo:

from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)
print(outputs.logits.shape)  # torch.Size([2, 2])
Detrás del pipeline Texto crudo pasa por el tokenizer, el cuerpo del modelo, el head, y softmax hasta llegar a probabilidades. Texto crudo Tokenizer input_ids + mask Modelo (cuerpo) hidden states Head *ForXxx Softmax probabilidad
Las tres etapas de pipeline() desarmadas: tokenizar, correr el cuerpo del modelo, proyectar con un head específico de la tarea.

El vector que sale del "cuerpo" del modelo (antes del head) es de alta dimensión — [batch, secuencia, hidden_size], con hidden_size típicamente 768 en modelos chicos y varios miles en modelos grandes. Ese vector por sí solo no significa nada legible; el head (una o pocas capas lineales) lo proyecta a algo interpretable — en este caso, 2 números (uno por clase). Esos números son logits, no probabilidades: necesitan un softmax para convertirse en algo que sume 1.

El punto que vale la pena remarcar: la librería no tiene un solo "modelo", tiene una familia de heads (*ForCausalLM, *ForSequenceClassification, *ForQuestionAnswering, *ForTokenClassification...) que se enchufan sobre el mismo cuerpo transformer. El cuerpo no cambia entre tareas — lo que cambia es qué head le ponés encima.

🟢 Actualización 2026

El head que más vas a usar hoy no es *ForSequenceClassification (el ejemplo clásico de clasificación de sentimiento) — es *ForCausalLM, el head de generación que llevan todos los LLM decoder-only del Capítulo 1. La mecánica de "cuerpo + head" es idéntica; simplemente el head generativo proyecta a un vector del tamaño del vocabulario completo (para elegir el siguiente token), no a un puñado de clases.

2. Modelos: from_pretrained / save_pretrained

Cargar un modelo pre-entrenado y guardar uno propio usan el mismo par de métodos en cualquier clase de la librería — modelos, tokenizers, todo:

from transformers import AutoModel

model = AutoModel.from_pretrained("bert-base-cased")
model.save_pretrained("mi_carpeta")
# genera: config.json + model.safetensors

config.json guarda la arquitectura (cuántas capas, tamaño oculto, cabezas de atención); el archivo de pesos guarda los números entrenados en sí. Son dos cosas separadas a propósito: la config te dice cómo construir el modelo vacío, los pesos te dicen qué valores ponerle adentro.

🟢 Actualización 2026

El original ya menciona .safetensors, pero vale la pena remarcar por qué importa hoy: el formato viejo (pytorch_model.bin) usa pickle de Python, que puede ejecutar código arbitrario al deserializarse. Un checkpoint .bin de una fuente no confiable es, literalmente, un vector de ataque. safetensors guarda solo tensores planos — no ejecuta nada al cargar. Herramientas de producción como vLLM hoy directamente esperan safetensors; un repo que solo trae .bin es una señal de alerta, no un detalle de formato.

3. Tokenizers: de texto a números

Hay tres formas básicas de cortar texto en piezas, y cada una tiene un problema que la siguiente intenta resolver:

EstrategiaCómo cortaProblema
Por palabra"Hola mundo".split()Vocabulario enorme (500k+ palabras en inglés); "perro" y "perros" son tokens sin relación
Por carácterH-o-l-a (letra por letra)Vocabulario chico, pero secuencias muy largas y cada token casi no significa nada por sí solo
Por subpalabra (BPE/WordPiece)"tokenization" → "token" + "ización"El estándar hoy — palabras frecuentes quedan enteras, las raras se parten en pedazos con significado

En la práctica vas a usar siempre subpalabras: BPE a nivel de byte (GPT, Llama), WordPiece (BERT), o SentencePiece/Unigram (muchos modelos multilingües). El flujo de dos pasos es siempre el mismo:

tokens = tokenizer.tokenize("Using a Transformer network is simple")
# ['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']

ids = tokenizer.convert_tokens_to_ids(tokens)
# [7993, 170, 11303, 1200, 2443, 1110, 3014]

Y decodificar es el camino inverso — de IDs a texto legible, reagrupando subpalabras en palabras completas.

🟢 Actualización 2026: chat templates

Lo que el original no cubre porque en 2022 casi no existía: los modelos de chat de hoy no reciben una sola frase — reciben una conversación con roles (system, user, assistant), y el tokenizer necesita saber cómo envolver eso en texto plano antes de tokenizarlo. Esa receta vive en un chat template (un archivo .jinja asociado al tokenizer), con tokens especiales propios de cada familia de modelos — <start_of_turn> en Gemma, <|im_start|> en la convención ChatML. Es la razón por la que nunca deberías armar el prompt de un modelo de chat concatenando strings a mano: el chat template es parte del contrato del modelo, tanto como su vocabulario.

4. Batching, padding y attention masks

Un modelo siempre espera un lote (batch), incluso si es de un solo elemento — por eso tokenizer(...) agrega una dimensión extra que fácilmente se te puede pasar por alto la primera vez. El problema real aparece cuando el lote tiene más de una frase: las frases casi nunca miden lo mismo, y un tensor tiene que ser rectangular.

La solución es padding: rellenar las frases más cortas con un token especial hasta igualar la más larga. Pero eso introduce un problema nuevo — la atención (Capítulo 1, §3) mira todos los tokens, incluido el relleno, y eso contamina el resultado. La solución al problema del padding es el attention mask: un tensor paralelo de 1s y 0s que le dice al modelo qué posiciones ignorar.

batched_ids = [
    [200, 200, 200],
    [200, 200, tokenizer.pad_token_id],
]
attention_mask = [
    [1, 1, 1],
    [1, 1, 0],  # el modelo ignora esta posición
]
outputs = model(torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask))

Sin el attention_mask correcto, el resultado del elemento con padding sale distinto al de procesarlo solo — un bug silencioso clásico, porque el código corre sin errores, solo da números levemente equivocados.

5. Servir en serio: TGI, vLLM, llama.cpp

Esta sección es enteramente nueva desde 2022 — el original ni la tenía. Todo lo anterior asume que corrés el modelo vos mismo, en tu proceso de Python. En producción, casi nadie hace eso: se usa un servidor de inferencia dedicado, optimizado para atender muchas requests a la vez.

FrameworkTécnica claveMejor para
TGIFlash Attention 2 + batching continuoProducción enterprise (Kubernetes, Prometheus/Grafana de fábrica)
vLLMPagedAttentionMáximo throughput, API compatible con OpenAI, control fino en Python
llama.cppCuantización GGUF + kernels de CPU optimizadosHardware de consumo, edge, cuando instalar un stack de Python es un problema

La idea detrás de PagedAttention (vLLM) conecta directo con el KV cache del Capítulo 1: en vez de reservar un bloque contiguo de memoria del tamaño máximo posible para cada request (desperdicio brutal cuando la mayoría de las respuestas son más cortas que el máximo), vLLM divide el KV cache en páginas chicas de tamaño fijo — el mismo truco que usa la memoria virtual de un sistema operativo — y las reparte dinámicamente entre requests. El resultado documentado: hasta 24× más throughput que reservar memoria de forma ingenua.

# Levantar un servidor vLLM compatible con la API de OpenAI
python -m vllm.entrypoints.openai.api_server \
    --model HuggingFaceTB/SmolLM2-360M-Instruct \
    --host 0.0.0.0 --port 8000

¿Te suena familiar ese comando? Es la misma familia de flags que ves en serve_lacan_vllm.sh — con --quantization, --kv-cache-dtype y --speculative-config encima, que son las variantes de producción de exactamente este mismo comando base.

🟢 Cómo elegir en la práctica

Si necesitás exprimir throughput en GPU con control fino → vLLM. Si necesitás algo enterprise-ready con monitoreo y autoscaling de fábrica → TGI. Si el target es una laptop, un edge device, o un entorno donde ni siquiera podés instalar PyTorch → llama.cpp con un checkpoint GGUF cuantizado. Los tres hoy exponen una API compatible con OpenAI, así que cambiar de uno a otro no debería tocar el código de tu aplicación — solo la infraestructura detrás.

6. Resumen

  1. pipeline() es tokenizer + modelo + postprocesado — ahora sabés manejar cada pieza por separado.
  2. Un modelo es cuerpo (produce hidden states) + head (los proyecta a algo útil para la tarea) — el mismo cuerpo sirve para clasificar o para generar, según qué head le pongas.
  3. La tokenización por subpalabras (BPE/WordPiece) es el estándar; los chat templates son la capa 2026 que decide cómo se arma el prompt de un modelo de chat.
  4. Padding + attention mask son inseparables: sin la mask correcta, el padding contamina el resultado en silencio.
  5. En producción no corrés el modelo a mano — lo servís con TGI, vLLM o llama.cpp, cada uno optimizado para un escenario distinto.