1Un experimento es una función, no un ritual
La causa número uno de resultados que "ya no salen" es que faltaba anotar algo. Un run de entrenamiento es determinista solo si se fija todo lo que lo determina. Conviene pensarlo como una función pura: dadas las mismas entradas, produce la misma salida.
La seed global fija los streams de números aleatorios de Python, NumPy, PyTorch y CUDA — la inicialización de pesos, el orden del shuffle, el dropout, todo lo que consume aleatoriedad. Pero la seed sola no alcanza: dos GPUs distintas, o el mismo hardware con kernels no deterministas, pueden divergir. Por eso el registro incluye también las banderas de determinismo y una huella del hardware.
| Se registra | Por qué importa |
|---|---|
| Modelo base exacto + revisión | El mismo nombre en otro commit del hub ya no es el mismo modelo. |
| Versión de cada librería | Un cambio de minor en el trainer altera el resultado en silencio. |
| Dataset + versión/hash | Identifica exactamente qué datos entraron (ver M2/M3). |
| Seed | Fija todos los streams RNG del run. |
| Config LoRA | Rango, alfa, módulos objetivo, dropout (M4/M5). |
| Config del trainer | Learning rate, batch, épocas, scheduler, warmup (M6). |
| Chat template | El formato de turnos con el que se entrenó (M1). |
| Hardware + banderas de determinismo | GPU, precisión, kernels deterministas. |
| Métricas + checkpoint | Qué se midió y qué artefacto se produjo. |
| Config de generación | temperature/top_p con que se evaluó (M0). |
2Config declarativa: el run vive en un archivo
La disciplina práctica es simple: ningún hiperparámetro vive en la línea de comandos ni en el cuaderno. Cada valor de M4/M5/M6 se escribe en un archivo declarativo, y ese archivo se hashea para producir el identificador del run. Si el hash coincide, el run es el mismo; si difiere, algo cambió y el archivo dice exactamente qué.
Esto vuelve el experimento revisable (se lee como texto), versionable (entra
en git junto al código) y reejecutable (un solo comando lo consume). La diferencia
entre dos experimentos se convierte en un diff de dos archivos.
3Reportar varianza, no un número suelto
Con la seed fija, un run es reproducible; pero un solo run no dice si una mejora es real. La aleatoriedad de la seed induce dispersión entre corridas. La forma honesta de reportar es correr $n$ seeds y dar la media con su desviación estándar, además del error estándar de la media:
La regla de oro: una mejora afirmada debe superar el ruido entre corridas. Si tu cambio sube la métrica en menos que la desviación entre seeds del baseline, no has demostrado nada — has observado ruido. Fijar la seed es para reproducir; barrer varias seeds es para concluir.
Reproducibilidad y varianza no se contradicen. Cada corrida individual es determinista (seed fija ⇒ mismos bits). Lo que varía es qué seed elegiste. Reportas la distribución sobre seeds precisamente porque cada punto de esa distribución es, en sí mismo, reproducible.
4La seguridad no está dentro del modelo
Un modelo fine-tuned no es seguro por haber sido entrenado con datos limpios. La seguridad es una propiedad del sistema alrededor del modelo: validaciones a la entrada, políticas de alcance y una respuesta segura cuando el modelo no sabe. Para audiencias sensibles la superficie a cubrir es amplia y concreta:
- Límites por edad y alcance: qué temas están dentro del dominio permitido y cuáles no.
- Contenido fuera de dominio: reconocer y declinar peticiones que caen fuera del propósito.
- Datos personales: no solicitar, no filtrar y no retener información identificable del usuario final.
- Prompt injection directa e indirecta: instrucciones hostiles inyectadas por el usuario o por datos externos.
- Temas sensibles y control parental: rutas de escalado y supervisión para audiencias vulnerables.
- Respuesta segura ante la ignorancia: decir "no lo sé" es preferible a inventar (evitar la alucinación de M9).
- Pruebas adversariales: probar el sistema como lo probaría un atacante, no como lo usaría un usuario amable.
Mover un caso de seguridad "al fine-tuning" es tentador pero frágil: el modelo generaliza de forma imperfecta y un prompt algo distinto puede sortear lo aprendido. Las barreras robustas se ponen fuera del modelo (validación de entrada/salida, límites de alcance) y se miden, no se asumen.
5Ataques adversariales y la métrica que los mide
Un ataque de sufijo adversarial tipo GCG optimiza un texto añadido al prompt para forzar que el modelo empiece a cooperar con una petición dañina — típicamente maximizando la probabilidad de que la respuesta arranque con una frase afirmativa:
Que esto sea posible motiva medir la robustez con una métrica concreta sobre un conjunto de red-team. La refusal-rate es la fracción de casos dañinos que el modelo efectivamente rechaza:
No es un número que se reporte una vez y se olvide: se mide antes y después de cada mitigación (por ejemplo un guardrail en el system-prompt) para saber si la defensa realmente subió la tasa de rechazo o solo lo pareció.
6Inyección indirecta: el dato también instruye
La inyección de prompt directa la escribe el usuario. La indirecta es más insidiosa: instrucciones hostiles vienen incrustadas en los datos recuperados — el contexto de RAG del M8 — y entran al prompt con el mismo nivel de confianza que las órdenes legítimas del usuario. El modelo no distingue "instrucción del usuario" de "texto que encontré en un documento".
La defensa es arquitectónica: establecer fronteras de confianza y aislar la entrada, de modo que el contenido recuperado se trate como datos no confiables y no como instrucciones. La eficacia se mide con la tasa de éxito del ataque antes vs. después de la mitigación — si baja de forma sostenida sobre un set adversarial, la frontera funciona.
usuario ──▶ [ validación de entrada ] ──▶ prompt ──▶ modelo ──▶ [ validación de salida ] ──▶ respuesta documento recuperado (M8) ──▶ [ frontera de confianza · datos ≠ instrucciones ] ──┘
La seguridad no termina en el modelo: en una app on-device los guardrails corren localmente — validación de entrada, filtros y límites por edad/alcance se aplican en el dispositivo, sin red. La inyección indirecta (instrucciones escondidas en datos recuperados por el RAG local) entra con el mismo nivel de confianza que el usuario: aísla ese contenido en la frontera de la app.
- Escribe un
experiment.yamlcon seed, hash del dataset, modelo e hiperparámetros. - Prepara un comando que reproduzca el run y verifica que dos corridas con la misma seed coinciden.
- Reporta media±std sobre 3 seeds.
- Arma una suite red-team: jailbreak directo + un payload de inyección indirecta escondido en un “documento recuperado”.
- Mide refusal-rate / tasa de éxito de inyección antes y después de un guardrail en el system-prompt.
La capa de seguridad de una app on-device suele vivir en Rust, justo en la frontera FFI: validas y saneas el prompt del usuario y el contenido recuperado antes de pasarlos al runtime, y aplicas los filtros de salida antes de mostrarlos.
Rust hace esa frontera robusta y sin sorpresas de memoria. Para el red-teaming del modelo, herramientas como garak o promptfoo automatizan los ataques.
Lecturas y recursos
La config declarativa en concreto
# experiment.yaml — describe el run por completo; su hash es el run_id run: seed: 1337 # fija RNG de Python/NumPy/PyTorch/CUDA deterministic: true # kernels deterministas + cudnn.benchmark=false model: base: "Qwen/Qwen3-0.6B" revision: "a1b2c3d" # commit exacto del hub, no solo el nombre dataset: path: "data/sft_v3.jsonl" sha256: "9f2c…e71a" # huella del split exacto (M2/M3) lora: # M4/M5 r: 16 alpha: 32 dropout: 0.05 target_modules: ["q_proj", "k_proj", "v_proj", "o_proj"] trainer: # M6 learning_rate: 2.0e-4 batch_size: 8 grad_accum: 4 epochs: 3 scheduler: "cosine" warmup_ratio: 0.03 chat_template: "qwen3" # M1 generation: # M0 — con qué se evaluó temperature: 0.7 top_p: 0.9
✦Ejercicios
De menor a mayor complejidad. El último cierra el curso: reproducibilidad demostrada y seguridad medida.
Fija la seed global
Escribe una función set_seed(seed) que fije los streams de Python, NumPy, PyTorch y CUDA, y active las banderas de determinismo. Genera dos tensores aleatorios tras llamarla dos veces con la misma seed y verifica que son idénticos.
Entrega: la función + un assert de igualdad bit a bit. Pista: random.seed, np.random.seed, torch.manual_seed, torch.use_deterministic_algorithms(True).
Hashea tu dataset y tu config
Calcula el sha256 del archivo de datos y de tu experiment.yaml. Deriva un run_id a partir del hash de la config e imprímelo. Cambia un solo hiperparámetro y muestra que el run_id cambia.
Entrega: los dos hashes + el run_id antes y después del cambio. Pista: hashlib.sha256(path.read_bytes()).
Reporta media ± std sobre seeds
Entrena (o evalúa) el mismo setup con 3 seeds distintas y recoge una métrica. Reporta la media, la desviación estándar y el error estándar $\text{std}/\sqrt{n}$. Decide, con esos números, si una mejora de $+0.4$ puntos sería creíble.
Entrega: tabla seed → métrica + la fila resumen media ± std. Pista: $\mathrm{SE} = \text{std}/\sqrt{3}$; compara la mejora contra la dispersión.
Mide la refusal-rate
Construye un set pequeño de casos dañinos y clasifica, para cada uno, si el modelo lo rechaza. Calcula la refusal-rate. Luego añade un guardrail en el system-prompt y vuelve a medir sobre el mismo set.
Entrega: refusal-rate antes y después + el system-prompt usado. Pista: $\text{refusal-rate}=\frac{1}{N}\sum_i \mathbb{1}[\text{rechaza}_i]$; fija la config de generación para que la comparación sea justa.
Reproducibilidad demostrada + scorecard de seguridad
Escribe un único experiment.yaml que capture seed, hash del dataset, modelo y
todos los hiperparámetros de M4–M6, y conéctalo para que un comando reproduzca una corrida
de entrenamiento bit-a-bit (verifica que dos runs con la misma seed coinciden, y reporta
media±std sobre 3 seeds); luego construye una pequeña suite de red-team — prompts de
jailbreak directo + un payload de inyección indirecta escondido en un "documento
recuperado" — y mide la refusal-rate / tasa de éxito de inyección sobre tu modelo
fine-tuned antes y después de añadir un guardrail en el system-prompt.
Entrega: la prueba de reproducibilidad (runs con seed emparejada + tabla de varianza a 3 seeds) y una scorecard de seguridad adversarial antes/después. Pista: hashea la config para el run_id; para la inyección indirecta, mide la tasa de éxito del ataque con el payload dentro del contexto recuperado vs. tras aislar los datos del canal de instrucciones.
- Un experimento es una función: fija seed, data_hash, code_commit, config y hardware y se reconstruye bit a bit.
- Cada hiperparámetro de M4/M5/M6 vive en una config declarativa cuyo hash es el
run_id. - Reproducir usa una seed; concluir exige varias: reporta media ± std y supera el ruido entre corridas.
- La seguridad envuelve al modelo — validaciones y políticas — no se resuelve solo dentro del fine-tuning.
- Mide la robustez: refusal-rate y tasa de éxito de inyección, antes y después de cada mitigación; la inyección indirecta entra por los datos recuperados.