<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Lakefs on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/lakefs/</link><description>Recent content in Lakefs on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Fri, 22 May 2026 11:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/lakefs/index.xml" rel="self" type="application/rss+xml"/><item><title>Data versioning para LLMOps: DVC, lakeFS y el reto del golden dataset reproducible</title><link>https://blog.lo0.es/posts/data-versioning-dvc-lakefs/</link><pubDate>Fri, 22 May 2026 11:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/data-versioning-dvc-lakefs/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La etapa &lt;strong>Data&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">pipeline LLMOps de seis etapas&lt;/a> tiene un eslabón silencioso del que depende todo lo demás: &lt;strong>versionar los datasets&lt;/strong> con la misma disciplina que se versiona el código. No es opcional. Un sistema LLM en producción consume al menos &lt;strong>cuatro tipos de dataset diferenciados&lt;/strong> —training/fine-tuning, corpus RAG, golden eval set, dataset enriquecido del bucle Retrain— y cada uno tiene exigencias propias. Git resuelve el código pero falla en datos por dos razones técnicas (tamaño y diff binario inútil) y una operativa (no propaga lineage hasta el bucket de pesos del modelo entrenado). Las dos herramientas OSS dominantes —&lt;strong>DVC&lt;/strong> y &lt;strong>lakeFS&lt;/strong>— se &lt;strong>unificaron en noviembre de 2025&lt;/strong> bajo una sola organización con hoja de ruta orientada a LLM training y RAG datalakes; siguen siendo proyectos complementarios (file-level vs branching de bucket completo) pero ya bajo gobierno común. El patrón productivo que el mercado ha consolidado: identificar cada artefacto con &lt;code>(dataset_id, version)&lt;/code> inmutable, propagar el par hasta el experiment tracking (MLflow / W&amp;amp;B), versionar también el &lt;strong>schema&lt;/strong> del dataset (no solo el contenido), aplicar &lt;strong>holdout estricto&lt;/strong> al golden eval set para no medir memorización, y mantener trazabilidad bidireccional &lt;code>dataset_version ↔ model_version ↔ deployment ↔ trace_id&lt;/code>. Sin esto, la promesa de &amp;ldquo;podemos auditar qué modelo respondió qué&amp;rdquo; se cae en el primer incidente serio.&lt;/p>
&lt;h2 id="estás-aquí-data-con-efecto-transversal-sobre-tune-eval-y-retrain">Estás aquí: Data (con efecto transversal sobre Tune, Eval y Retrain)&lt;/h2>
&lt;p>Este post entra al detalle del &lt;strong>eslabón de versionado&lt;/strong> dentro de la etapa &lt;strong>1 · Data&lt;/strong>. El versionado pertenece operativamente a Data, pero los artefactos que produce viajan a Tune (training set), Eval (golden set) y Retrain (dataset enriquecido). Por eso el diagrama marca Data como activa &lt;strong>y&lt;/strong> una banda transversal indicando el lineage end-to-end.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 135" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Data con lineage transversal">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.cross{fill:#ffe9d6;stroke-width:1.4;stroke:#c66;rx:6}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#444}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#dvm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#dvm)}&lt;/style>
&lt;defs>&lt;marker id="dvm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DATA · versionado de datasets con lineage hasta el trace de producción&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box active"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;rect x="30" y="98" width="735" height="25" class="cross"/>
&lt;text x="397" y="115" text-anchor="middle" class="sm">Lineage de dataset: training set → Tune · golden set → Eval · enriched set → Retrain (que vuelve a Data)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-analogía-maestra-trazabilidad-de-lote-en-una-fábrica-seria">La analogía maestra: trazabilidad de lote en una fábrica seria&lt;/h2>
&lt;p>Una fábrica farmacéutica seria no produce sin &lt;strong>trazabilidad de lote&lt;/strong>. Cada caja de pastillas lleva un número de lote impreso; ese lote se asocia a fechas de fabricación, a los lotes concretos de cada materia prima que se usó, a las pruebas de calidad que pasó, y a los técnicos que firmaron cada paso. Si un paciente reporta un efecto adverso, la fábrica puede rebobinar en horas: este envase → este lote → estas materias primas → este turno → esta línea de producción → este resultado de control de calidad. Sin esa cadena, el incidente es un misterio permanente.&lt;/p>
&lt;p>Un sistema LLM serio funciona igual. El &amp;ldquo;envase&amp;rdquo; es la respuesta que un usuario vio en producción. El &amp;ldquo;lote&amp;rdquo; es la combinación de &lt;strong>modelo, adapter, prompt, contexto y configuración&lt;/strong> que la generó. Y las &amp;ldquo;materias primas&amp;rdquo; son los datasets: el training set sobre el que se entrenó el modelo base, el dataset del fine-tuning del adapter, el corpus RAG que alimenta el retrieval, el golden eval set que valida la promotion. Si un cliente dice &lt;em>&amp;quot;¿con qué datos se entrenó el modelo que el 14 de marzo respondió X a mi pregunta Y?&amp;quot;&lt;/em>, sin trazabilidad de lote la respuesta es &lt;em>&amp;ldquo;no lo sabemos&amp;rdquo;&lt;/em>. Y eso, en un cliente con compliance encima, mata el contrato.&lt;/p>
&lt;p>Git versiona la receta (el código). Data versioning versiona los ingredientes. Sin las dos cosas, no hay fábrica auditable.&lt;/p>
&lt;h2 id="los-cuatro-artefactos-que-conviene-versionar-con-exigencias-diferenciadas">Los cuatro artefactos que conviene versionar (con exigencias diferenciadas)&lt;/h2>
&lt;p>No todos los datasets se versionan igual ni con la misma frecuencia. El sistema LLM en producción típico maneja &lt;strong>cuatro artefactos&lt;/strong> que conviene gobernar por separado.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Artefacto&lt;/th>
&lt;th>Qué es&lt;/th>
&lt;th>Tamaño típico&lt;/th>
&lt;th>Frecuencia de versión nueva&lt;/th>
&lt;th>Quién la consume&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Training / fine-tuning dataset&lt;/strong>&lt;/td>
&lt;td>Pares input/output (o conversaciones) que entrenan el adapter o el modelo.&lt;/td>
&lt;td>10⁴ – 10⁷ ejemplos · 1 – 100 GB&lt;/td>
&lt;td>Por experimento de Tune&lt;/td>
&lt;td>Trainer (Axolotl, TRL, Unsloth)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>RAG corpus&lt;/strong>&lt;/td>
&lt;td>Documentos indexados que alimentan retrieval.&lt;/td>
&lt;td>10⁵ – 10⁹ chunks · 10 GB – 10 TB&lt;/td>
&lt;td>Casi continuo (ingest streaming)&lt;/td>
&lt;td>Indexer + vector store&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Golden eval set&lt;/strong>&lt;/td>
&lt;td>Ejemplos curados con respuesta esperada para medir calidad.&lt;/td>
&lt;td>10² – 10⁴ ejemplos · MB&lt;/td>
&lt;td>Por release del producto&lt;/td>
&lt;td>Eval gates en CI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Enriched retrain dataset&lt;/strong>&lt;/td>
&lt;td>Casos donde el sistema falló + corrección humana.&lt;/td>
&lt;td>Cientos a miles por trimestre&lt;/td>
&lt;td>Por ciclo de &lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">retrain&lt;/a>&lt;/td>
&lt;td>Siguiente Tune&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Los cuatro tienen requisitos comunes (identidad inmutable, lineage, schema) y diferencias relevantes:&lt;/p>
&lt;ul>
&lt;li>El &lt;strong>training set&lt;/strong> suele ser &lt;strong>grande, estable por experimento&lt;/strong>, y el coste de un error es un experimento perdido (caro pero acotado).&lt;/li>
&lt;li>El &lt;strong>RAG corpus&lt;/strong> es &lt;strong>enorme, en continuo cambio&lt;/strong>, y el versionado se gestiona por snapshots periódicos del índice (no del raw text). Usualmente lakeFS o branches del bucket; DVC no es la mejor encaja.&lt;/li>
&lt;li>El &lt;strong>golden eval set&lt;/strong> es &lt;strong>pequeño pero crítico&lt;/strong>: errores aquí contaminan toda la cadena de promotion. Aquí la rigidez del versionado importa más que en ningún otro.&lt;/li>
&lt;li>El &lt;strong>enriched retrain dataset&lt;/strong> es &lt;strong>incremental por naturaleza&lt;/strong>: cada ciclo de Retrain aporta un delta sobre el anterior. La versión nueva no sobrescribe; hereda y añade.&lt;/li>
&lt;/ul>
&lt;p>Confundirlos —tratar el RAG corpus como si fuera el training set, o el golden eval como si fuera un dataset más— es el origen de la mitad de los problemas operacionales en data versioning.&lt;/p>
&lt;h2 id="por-qué-git-no-basta">Por qué Git no basta&lt;/h2>
&lt;p>La pregunta evidente: si Git ya resuelve el código, ¿por qué no resuelve también los datos? Tres razones, dos técnicas y una operacional.&lt;/p>
&lt;p>&lt;strong>Razón 1: tamaño.&lt;/strong> Un repositorio Git con un dataset de 50 GB se vuelve inmanejable. &lt;code>git clone&lt;/code> baja todo el histórico; &lt;code>git status&lt;/code> recorre todos los archivos; el pack file en &lt;code>.git/objects&lt;/code> infla hasta el doble del dataset. Git LFS resuelve la primera parte (el binario sale del pack) pero introduce su propia complejidad sin abordar las otras dos razones.&lt;/p>
&lt;p>&lt;strong>Razón 2: diff binario inútil.&lt;/strong> Git asume que los diffs de texto son útiles. Cuando cambia una columna en un parquet de 8 GB, el diff es opaco —el archivo es binario, comprimido, columnar—. No puedes hacer code review sobre un cambio de dataset igual que sobre un cambio de función. Necesitas &lt;strong>diff semántico&lt;/strong>: cuántas filas cambiaron, qué columnas cambiaron, qué distribución se movió. Ningún Git nativo te da eso.&lt;/p>
&lt;p>&lt;strong>Razón 3: lineage que cruza fronteras de repositorio.&lt;/strong> Esta es la más importante y la más sutil. El dataset de training vive en un bucket. El código del trainer vive en un repo Git. El modelo entrenado se publica a un model registry. La inferencia en producción genera traces en un sistema de observability. Conectar &lt;code>dataset_v3 → adapter_v7 → deployment_d2 → trace t_x9&lt;/code> requiere propagar identificadores &lt;strong>a través de cuatro sistemas distintos&lt;/strong>, no dentro de un repo. Git no tiene opinión sobre esto.&lt;/p>
&lt;p>Las herramientas de data versioning (DVC, lakeFS, Pachyderm, Quilt) existen porque resuelven los tres problemas a la vez: cuelgan los datos fuera del repo Git, ofrecen alguna forma de diff semántico, y exponen identidades estables propagables hacia experiment tracking y model registry.&lt;/p>
&lt;h2 id="dvc-vs-lakefs-antes-de-la-unificación">DVC vs lakeFS antes de la unificación&lt;/h2>
&lt;p>Hasta noviembre de 2025, las dos herramientas dominantes OSS coexistían como aproximaciones complementarias.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Eje&lt;/th>
&lt;th>DVC&lt;/th>
&lt;th>lakeFS&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Modelo mental&lt;/td>
&lt;td>&amp;ldquo;Git para datos&amp;rdquo;&lt;/td>
&lt;td>&amp;ldquo;Branching para el data lake&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Granularidad&lt;/td>
&lt;td>Archivo individual&lt;/td>
&lt;td>Bucket entero (con namespacing por branch)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Storage&lt;/td>
&lt;td>Remote-agnóstico (S3, GCS, Azure, MinIO, SSH)&lt;/td>
&lt;td>S3-compatible (S3, MinIO, Ceph)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Workflow&lt;/td>
&lt;td>&lt;code>dvc add&lt;/code> + &lt;code>dvc push&lt;/code> + &lt;code>dvc.yaml&lt;/code> pipelines&lt;/td>
&lt;td>&lt;code>lakectl commit&lt;/code> + branches/merges sobre el bucket&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Diff&lt;/td>
&lt;td>Hash del archivo + metadata externa&lt;/td>
&lt;td>Diff a nivel de objeto + commit log&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Casos fuertes&lt;/td>
&lt;td>Training datasets discretos, model files, pipelines reproducibles&lt;/td>
&lt;td>RAG corpora grandes, branching de un data lake compartido, experimentos en paralelo sin duplicar datos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Integración con Git&lt;/td>
&lt;td>Profunda (los &lt;code>.dvc&lt;/code> files se commitean a Git)&lt;/td>
&lt;td>Tangencial (lakeFS vive en paralelo)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quién lo opera&lt;/td>
&lt;td>Equipo MLE&lt;/td>
&lt;td>Equipo data engineering&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>En la práctica, muchos equipos los usaban &lt;strong>a la vez&lt;/strong>: DVC para los datasets discretos que alimentaban un experimento (cabe en un repo Git por la indirección de los &lt;code>.dvc&lt;/code> pointers), y lakeFS para el bucket grande del corpus RAG sobre el que querían branching sin duplicar terabytes.&lt;/p>
&lt;h2 id="qué-cambió-con-la-adquisición-de-noviembre-2025">Qué cambió con la adquisición de noviembre 2025&lt;/h2>
&lt;p>&lt;a href="https://medium.com/the-modern-scientist/reproducible-ai-versioning-models-prompts-and-data-96dd0337af65">lakeFS adquirió DVC&lt;/a> en noviembre de 2025. La consecuencia operacional a mayo de 2026 es modesta pero relevante:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No hay (todavía) fusión técnica de los proyectos.&lt;/strong> DVC sigue siendo DVC y lakeFS sigue siendo lakeFS. Las CLIs, los formatos y los workflows actuales no han cambiado.&lt;/li>
&lt;li>&lt;strong>Hoja de ruta combinada explícita hacia LLM training y RAG datalakes.&lt;/strong> La organización fusionada ha enunciado prioridades específicas: branching consistente entre el dataset y el modelo entrenado, integraciones nativas con MLflow / W&amp;amp;B / Langfuse, soporte para los formatos típicos de LLM (jsonl, parquet con tokenización embebida), e indexación vectorial branch-aware.&lt;/li>
&lt;li>&lt;strong>Convergencia esperada en 2026-2027.&lt;/strong> El mercado anticipa un único registry con dos modos operativos (file-level + bucket-branching) bajo CLI unificada. A día de hoy, los equipos siguen combinando ambos.&lt;/li>
&lt;/ul>
&lt;p>La lectura práctica para 2026: &lt;strong>adopta DVC para training/eval datasets discretos y lakeFS para el RAG corpus&lt;/strong>, pero diseña el lineage para que un futuro registry unificado pueda absorber ambos sin re-versionar todo. En concreto: usa identificadores estables (&lt;code>dataset_id&lt;/code>, &lt;code>version&lt;/code>, &lt;code>commit_hash&lt;/code>) que sean propagables independientemente de la herramienta.&lt;/p>
&lt;h2 id="el-patrón-operativo-lineage-de-cuatro-saltos">El patrón operativo: lineage de cuatro saltos&lt;/h2>
&lt;p>Una vez aceptado que hay que versionar datasets, la pregunta no es &amp;ldquo;qué herramienta&amp;rdquo; sino &amp;ldquo;qué cadena de identificadores conecta producción con el dato origen&amp;rdquo;. El patrón que ha consolidado el mercado tiene &lt;strong>cuatro saltos&lt;/strong>:&lt;/p>
&lt;pre tabindex="0">&lt;code>(dataset_id, dataset_version)
│ versiona en DVC o lakeFS
▼
(model_id, model_version)
│ registra en MLflow / W&amp;amp;B con dataset como input
▼
(deployment_id, prompt_version)
│ registra en model registry + prompt registry
▼
(trace_id)
│ emite el motor de inferencia con OTel
▼
respuesta visible al usuario
&lt;/code>&lt;/pre>&lt;p>Cada flecha es un escritura de metadata que cruza el límite entre dos sistemas. Si una sola flecha falta, el lineage se rompe y la promesa de auditabilidad se evapora.&lt;/p>
&lt;p>Ejemplo concreto del flujo, usando DVC + MLflow:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Etapa Data: versionar el dataset&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dvc add data/finetune_v3.jsonl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add data/finetune_v3.jsonl.dvc data/.gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;data: finetune dataset v3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dvc push &lt;span class="c1"># sube el binario al remote (MinIO/S3)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Etapa Tune: entrenar registrando lineage&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mlflow run train.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">dataset_id&lt;/span>&lt;span class="o">=&lt;/span>finetune &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">dataset_version&lt;/span>&lt;span class="o">=&lt;/span>v3 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">dataset_hash&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>dvc get-url data/finetune_v3.jsonl &lt;span class="p">|&lt;/span> sha256sum&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># El run registra: input dataset + model output&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Etapa Eval: validar registrando lineage&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mlflow run eval.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">model_id&lt;/span>&lt;span class="o">=&lt;/span>adapter_customer_v7 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">golden_set_id&lt;/span>&lt;span class="o">=&lt;/span>customer_support &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">golden_set_version&lt;/span>&lt;span class="o">=&lt;/span>v12
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Etapa Deploy: el deployment hereda dataset + golden ids&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Cada trace en Observe lleva model_version + prompt_version&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># que rebobinan hasta dataset_version&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Versión equivalente con lakeFS sobre el RAG corpus:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Branch para los embeddings del nuevo corpus&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lakectl branch create lakefs://corpus/embed-2026q2 --source main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Indexar el corpus en ese branch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python index_corpus.py --branch embed-2026q2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Validar antes de mergear a main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python eval_retrieval.py --branch embed-2026q2 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --metric recall@10 --threshold 0.78
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Si pasa, mergear (cambia el corpus que sirve producción)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lakectl commit lakefs://corpus/embed-2026q2 -m &lt;span class="s2">&amp;#34;embed: corpus 2026q2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lakectl merge lakefs://corpus/embed-2026q2 lakefs://corpus/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La virtud del segundo flujo: durante la validación del nuevo corpus, &lt;strong>el sistema de producción sigue sirviendo desde &lt;code>main&lt;/code> sin interferencia&lt;/strong>. La rama paralela funciona como un staging real sobre el bucket completo.&lt;/p>
&lt;h2 id="schema-contracts-data-versioning-sin-esto-es-ilusión">Schema contracts: data versioning sin esto es ilusión&lt;/h2>
&lt;p>Versionar el contenido de un dataset sin versionar su &lt;strong>schema&lt;/strong> es un error frecuente. El problema: un dataset versionado pero con schema implícito sigue rompiendo silenciosamente cuando un productor (el equipo de ingestión, el equipo de annotation, un script ad-hoc) cambia un campo.&lt;/p>
&lt;p>Caso concreto: golden eval set de soporte al cliente, 1000 ejemplos, campo &lt;code>expected_output&lt;/code> originalmente &lt;code>string&lt;/code>. Alguien decide que necesita capturar varias respuestas válidas y cambia el campo a &lt;code>list[string]&lt;/code>. El loader del eval acepta ambos formatos por casualidad (Python es laxa) pero el judge LLM downstream recibe un objeto diferente. El eval sigue pasando pero ahora &lt;strong>mide otra cosa&lt;/strong>.&lt;/p>
&lt;p>Patrón productivo: el dataset se versiona con DVC/lakeFS &lt;strong>y&lt;/strong> su schema se versiona con &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Schema Registry&lt;/a> (Confluent o Apicurio) o, en sistemas menos maduros, con un JSON Schema embebido junto al dataset. CI bloquea cualquier PR que rompa el contract sin bump de versión.&lt;/p>
&lt;p>Schema mínimo de un golden eval entry (ilustrativo):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">$schema&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://json-schema.org/draft/2020-12/schema&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">$id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://example.org/schemas/golden_eval_entry/v3.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">required&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">example_id, input, expected_outputs, rubric, segment]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">example_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: string, format&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">uuid}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">required&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">user_query, retrieved_context]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user_query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retrieved_context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: array, items&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expected_outputs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">array&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">minItems&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">items&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rubric&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">required&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">must_include, must_not_include, format]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">must_include&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: array, items&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">must_not_include&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: array, items&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">format&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">enum&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">text, json, markdown]}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">segment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">difficulty&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">enum&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">easy, medium, hard]}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">added_at&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: string, format&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">date-time}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">curated_by&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reglas operativas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Compatibility forward/backward&lt;/strong> explícita: añadir un campo opcional es backward-compatible; quitar uno requerido es breaking. La política se enforza con un compatibility check en CI.&lt;/li>
&lt;li>&lt;strong>Versión del schema embebida&lt;/strong> en cada fila del dataset (un campo &lt;code>_schema_version&lt;/code>). El loader valida que la versión coincide con lo que espera el código que lo consume.&lt;/li>
&lt;li>&lt;strong>Schema registry como única fuente de verdad&lt;/strong>, no como copia opcional del JSON Schema en cuatro repos.&lt;/li>
&lt;/ul>
&lt;p>Sin este nivel de disciplina, &amp;ldquo;tenemos data versioning&amp;rdquo; significa &amp;ldquo;guardamos los bytes pero no controlamos qué significan&amp;rdquo;.&lt;/p>
&lt;h2 id="golden-eval-set-la-versión-más-crítica">Golden eval set: la versión más crítica&lt;/h2>
&lt;p>De los cuatro artefactos, el &lt;strong>golden eval set&lt;/strong> es el que más rigor exige. Un fallo aquí contamina toda la cadena de promotion: si el eval miente, los gates aprueban modelos que no deberían.&lt;/p>
&lt;p>Tres disciplinas extra sobre el golden set:&lt;/p>
&lt;p>&lt;strong>Anotación con calidad medida.&lt;/strong> Cada ejemplo lo etiqueta un humano, y un porcentaje (10-20 %) se anota por dos personas independientes. El &lt;strong>acuerdo inter-anotador&lt;/strong> (Cohen&amp;rsquo;s kappa o F1 pairwise) se mide y se publica; un golden set con kappa &amp;lt; 0.7 está midiendo ruido humano, no comportamiento del modelo. Argilla y Label Studio dan la mecánica; lo importante es la disciplina, no la herramienta.&lt;/p>
&lt;p>&lt;strong>Holdout estricto contra contaminación.&lt;/strong> El golden set &lt;strong>nunca&lt;/strong> debe entrar al training set. Mecanismo concreto: hash de cada &lt;code>input&lt;/code> del golden set (sha256 normalizado por lowercasing + stripping de puntuación trivial) → check en CI contra todos los hashes del training set. Si hay intersección, el CI bloquea hasta resolución. Sin este check, el modelo aprueba el eval por memorización, no por capacidad. La consecuencia es desastrosa en producción: el modelo &amp;ldquo;validado&amp;rdquo; falla en casos análogos al golden set que no estaban memorizados.&lt;/p>
&lt;p>&lt;strong>Versionado aditivo, nunca destructivo.&lt;/strong> Cuando el golden set crece (cada ciclo de retrain añade casos), &lt;code>golden_v3 = golden_v2 ∪ new_examples&lt;/code>. Nunca &lt;code>golden_v3 = nuevo set distinto&lt;/code>. Sólo así puedes comparar dos modelos entrenados a meses de distancia sobre la &lt;strong>misma base&lt;/strong> + el delta nuevo. Si reescribes el golden set, no puedes decir si el modelo de marzo era peor que el de mayo o si simplemente medías cosas distintas.&lt;/p>
&lt;p>Tabla resumen de la disciplina por artefacto:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Práctica&lt;/th>
&lt;th>Training set&lt;/th>
&lt;th>RAG corpus&lt;/th>
&lt;th>Golden eval set&lt;/th>
&lt;th>Enriched retrain&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Versionado inmutable&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí (snapshots)&lt;/td>
&lt;td>&lt;strong>Sí, crítico&lt;/strong>&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Schema con contract&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Recomendado&lt;/td>
&lt;td>&lt;strong>Sí, crítico&lt;/strong>&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Doble anotación&lt;/td>
&lt;td>No&lt;/td>
&lt;td>No aplica&lt;/td>
&lt;td>&lt;strong>Sí (10-20 %)&lt;/strong>&lt;/td>
&lt;td>Sí (10-20 %)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Holdout vs otros datasets&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>&lt;strong>Sí, hash check&lt;/strong>&lt;/td>
&lt;td>Sí (vs golden)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drift check vs versión anterior&lt;/td>
&lt;td>Recomendado&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Recomendado&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Lineage hasta deployment&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="promotion-gates-el-dataset-es-promovido-como-el-modelo">Promotion gates: el dataset es promovido como el modelo&lt;/h2>
&lt;p>Un dataset candidato (un &lt;code>golden_v13&lt;/code> recién enriquecido, un &lt;code>enriched_retrain_2026_q2&lt;/code> resultado del ciclo de Retrain) no entra a producción por estar en el bucket. Pasa por &lt;strong>gates&lt;/strong> equivalentes a los del modelo o del prompt:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Schema validation&lt;/strong> — el contract se cumple. Bloqueo en CI si no.&lt;/li>
&lt;li>&lt;strong>Quality validation&lt;/strong> — muestra aleatoria del 5-10 % revisada por humano con quality score ≥ 4/5. Bloqueo si la muestra falla.&lt;/li>
&lt;li>&lt;strong>Holdout segregation check&lt;/strong> — para golden sets y enriched datasets, hash check contra todos los demás datasets activos. Bloqueo si hay solapamiento.&lt;/li>
&lt;li>&lt;strong>Drift check vs versión anterior&lt;/strong> — KS test sobre distribución de embeddings de los inputs, o métricas más simples (longitud media, distribución de segmentos, ratio de cada label). Aviso si el drift es alto sin causa documentada; bloqueo si es muy alto.&lt;/li>
&lt;li>&lt;strong>Lineage check&lt;/strong> — el dataset declara explícitamente de qué versión hereda y qué cambió. Sin esa metadata, no entra.&lt;/li>
&lt;/ol>
&lt;p>Sólo cuando los cinco gates pasan, el dataset se etiqueta como &lt;code>production-ready&lt;/code> y se desbloquean los pipelines downstream que dependen de él (el siguiente Tune, el siguiente release del producto, el siguiente ciclo de eval).&lt;/p>
&lt;h2 id="el-stack-on-premise-aplicado">El stack on-premise aplicado&lt;/h2>
&lt;p>En una infraestructura genérica con &lt;strong>RTX 4090&lt;/strong> (24 GB VRAM, perfil de desarrollo / batch chico) y un &lt;strong>cluster 4×H100 SXM&lt;/strong> (80 GB VRAM cada una, NVLink, entrenamientos y inferencia productiva), el data versioning encaja sin GPU dedicado para el versionado en sí —el versionado vive en CPU + storage— pero sí toca la GPU para los drift checks que requieren embeddings.&lt;/p>
&lt;p>Topología típica:&lt;/p>
&lt;pre tabindex="0">&lt;code>┌────────────────────────────────────────────────────────────┐
│ Object store (MinIO o Ceph) │
│ buckets: /training-sets /corpus-rag │
│ /golden-evals /enriched-retrain │
└────────────────────────┬───────────────────────────────────┘
│
┌─────────────────┼──────────────────┐
│ │ │
┌───▼────┐ ┌────▼────┐ ┌────▼─────┐
│ DVC │ │ lakeFS │ │ MLflow │
│ remote │ │ branches│ │ Tracking │
└───┬────┘ └────┬────┘ └────┬─────┘
│ │ │
└─────────────────┴──────────────────┘
│
┌──────▼──────┐
│ CI/CD gates │
│ (Forgejo / │
│ GitLab) │
└──────┬──────┘
│
┌──────────┴───────────┐
│ │
┌─────▼──────┐ ┌─────▼─────┐
│ RTX 4090 │ │ 4×H100 │
│ (drift │ │ (training │
│ embeds, │ │ + │
│ validates)│ │ serving) │
└────────────┘ └───────────┘
&lt;/code>&lt;/pre>&lt;p>Notas operativas:&lt;/p>
&lt;ul>
&lt;li>El &lt;strong>object store&lt;/strong> (MinIO o Ceph) sirve a la vez como DVC remote y como storage de lakeFS. Un solo plano de almacenamiento, dos vistas.&lt;/li>
&lt;li>Los &lt;strong>schema checks&lt;/strong> y &lt;strong>hash de holdout&lt;/strong> son tareas CPU-bound rápidas; el CI runner las ejecuta sin GPU.&lt;/li>
&lt;li>El &lt;strong>drift check por embeddings&lt;/strong> requiere encoder; la RTX 4090 sirve para esto sin tocar el cluster productivo. Un encoder pequeño (BGE-small, E5-small, ~100M parámetros) procesa 10⁴ ejemplos en pocos minutos.&lt;/li>
&lt;li>El &lt;strong>cluster H100&lt;/strong> queda libre para training y serving, sin contaminación por jobs de versionado.&lt;/li>
&lt;/ul>
&lt;h3 id="cuándo-no-hace-falta-dvclakefs">¿Cuándo NO hace falta DVC/lakeFS?&lt;/h3>
&lt;p>Hay una posición opuesta defendida con datos en el &lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">post de fine-tuning continuo&lt;/a>: para sistemas pequeños con un único equipo, datasets &amp;lt; 1 GB y un puñado de adapters, &lt;strong>Postgres + pgvector + un bucket S3 + filenames con hash&lt;/strong> son suficientes. La complejidad operativa de DVC/lakeFS no se amortiza.&lt;/p>
&lt;p>La línea divisoria es razonable:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No hace falta DVC/lakeFS&lt;/strong>: un solo equipo, datasets pequeños, pocos adapters, sin múltiples productos compartiendo datos.&lt;/li>
&lt;li>&lt;strong>Sí hace falta&lt;/strong>: múltiples equipos, datasets &amp;gt; 10 GB, varios productos que comparten golden eval set, compliance externo que exige trazabilidad de lote, o un ciclo de retrain trimestral institucionalizado.&lt;/li>
&lt;/ul>
&lt;p>Adoptar DVC + lakeFS antes de necesitarlos es overhead. Adoptarlos seis meses tarde es perder seis meses de lineage de manera irrecuperable.&lt;/p>
&lt;h2 id="siete-pitfalls-que-convierten-data-versioning-en-teatro">Siete pitfalls que convierten data versioning en teatro&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Versionar los datos pero no los schemas.&lt;/strong> El contenido se versiona, el contrato cambia silenciosamente, el sistema rompe sin que el versionado lo capture. Schema Registry no es opcional; es la mitad del problema.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Mismo S3 path sobrescrito.&lt;/strong> &amp;ldquo;Sube &lt;code>training.jsonl&lt;/code> al bucket&amp;rdquo; y el siguiente experimento reescribe el archivo. El versionado de S3 (si está habilitado) salva la lana, pero sin un identificador inmutable propagado a MLflow no se puede rebobinar. Patrón correcto: &lt;code>training_v3.jsonl&lt;/code> o &lt;code>training/2026q2/&amp;lt;sha&amp;gt;.jsonl&lt;/code>, nunca el mismo nombre.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Golden eval set sin holdout estricto.&lt;/strong> Sin hash check contra training, el modelo memoriza el eval y aprueba sin haber aprendido. Es el equivalente LLM de un examen que el profesor anuncia: aprueba todo el mundo, no se ha medido nada.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>No registrar lineage dataset → modelo.&lt;/strong> Cuando un incidente requiere saber con qué datos se entrenó cierto modelo, la respuesta correcta es un query a MLflow / W&amp;amp;B. Si la respuesta es &amp;ldquo;preguntemos a quien lo entrenó&amp;rdquo; (suponiendo que siga en el equipo), el lineage no existe.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>DVC añadido seis meses tarde.&lt;/strong> Adoptar versionado en mes 1 = molestia. Adoptarlo en mes 6 = pérdida irrecuperable de seis meses de datasets que ya no se pueden reconstruir. La maldición del &amp;ldquo;lo metemos después&amp;rdquo;.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>lakeFS con branches que nunca se mergean.&lt;/strong> Branches paralelos sobre el corpus son útiles para experimentar; mantenidos indefinidamente sin merge, el operativo se vuelve un cementerio de branches medio actualizados. Política explícita: merge o destruir en N semanas.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Validación de schema solo en producción.&lt;/strong> El contract se valida cuando el dataset ya está en producción y el modelo entrenado. Para entonces, el incidente ya pasó. La validación tiene que ser &lt;strong>en CI&lt;/strong>, antes del merge, sobre el delta que el PR introduce.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="el-ciclo-de-un-dataset-en-una-pantalla">El ciclo de un dataset en una pantalla&lt;/h2>
&lt;pre tabindex="0">&lt;code>┌─────────────────────────────────────────────────────────────┐
│ Productor (ingest / annotation / retrain bucle) │
└────────────────┬────────────────────────────────────────────┘
│
▼ (commit a candidate version)
┌─────────────────────────┐
│ CI gates │
│ - Schema validation │
│ - Quality sampled │
│ - Holdout hash check │ ── falla → PR bloqueado
│ - Drift vs anterior │
│ - Lineage declarado │
└────────────┬────────────┘
│ pasa
▼
┌─────────────────────────┐
│ DVC tag o lakeFS commit│
│ + MLflow registry │ ← versión inmutable
│ + Schema Registry │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Pipeline downstream │
│ Tune / Eval / Deploy │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Trace de producción │
│ → rebobina hasta dataset│
└─────────────────────────┘
&lt;/code>&lt;/pre>&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;p>A primer nivel queda fuera de este post:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Vector store versioning&lt;/strong> propiamente dicho: un índice de embeddings no se versiona como un dataset crudo porque depende del modelo de embedding. Cambiar el embedder reescribe todo el índice. Es otro animal y merece tratamiento aparte (recall, ANN parameters, branching del índice vs reembedding completo).&lt;/li>
&lt;li>&lt;strong>Tooling de lineage estandarizado&lt;/strong> (OpenLineage, Marquez): cómo emitir y consumir lineage events de manera interoperable entre sistemas.&lt;/li>
&lt;li>&lt;strong>Data quality frameworks&lt;/strong> (Great Expectations, Soda, Deequ): cómo escribir suites de &amp;ldquo;expectations&amp;rdquo; sobre un dataset y enforzarlas en cada versión.&lt;/li>
&lt;li>&lt;strong>Privacy-preserving versioning&lt;/strong>: federated learning sin centralizar el dataset, differential privacy aplicada a la versión que se distribuye.&lt;/li>
&lt;li>&lt;strong>Contaminación entre golden sets de proveedores externos&lt;/strong> (HumanEval, MMLU, etc.) y datasets de training de modelos open: el problema de &amp;ldquo;el modelo aprueba HumanEval porque HumanEval está en su pretraining&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>Cada uno da para un post propio cuando el campo lo justifique.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro donde encaja esta pieza, sección Data.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle&lt;/a> — cómo el enriched dataset producido por Retrain vuelve a Data; este post detalla cómo versionarlo bien.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">Prompt versioning con Langfuse y MLflow&lt;/a> — la otra pieza transversal del lineage; el &lt;code>prompt_version&lt;/code> viaja junto al &lt;code>dataset_version&lt;/code> en cada trace.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a> — defiende un stack minimalista (Postgres + pgvector + S3) sin DVC/lakeFS para sistemas pequeños; este post explica cuándo se cruza la línea hacia el otro lado.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs: la capa después del tracing&lt;/a> — el consumidor principal del golden eval set.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">MLOps específico para LLMs en 2026&lt;/a> — contexto de mercado del stack LLMOps completo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">PostgreSQL + Qdrant para ingestión&lt;/a> — cómo se materializa la ingestión que precede al versionado.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://dvc.org/doc">DVC documentation&lt;/a> — workflows de versionado, pipelines y remotes.&lt;/li>
&lt;li>&lt;a href="https://docs.lakefs.io/">lakeFS documentation&lt;/a> — branching, merging y commits sobre el bucket.&lt;/li>
&lt;li>&lt;a href="https://medium.com/the-modern-scientist/reproducible-ai-versioning-models-prompts-and-data-96dd0337af65">lakeFS adquiere DVC, noviembre 2025&lt;/a> — anuncio y hoja de ruta combinada.&lt;/li>
&lt;li>&lt;a href="https://docs.confluent.io/platform/current/schema-registry/index.html">Confluent Schema Registry&lt;/a> y &lt;a href="https://www.apicur.io/registry/">Apicurio&lt;/a> — schema contracts para datos en streaming.&lt;/li>
&lt;li>&lt;a href="https://openlineage.io/">OpenLineage&lt;/a> y &lt;a href="https://marquezproject.ai/">Marquez&lt;/a> — estándar abierto de eventos de lineage.&lt;/li>
&lt;li>&lt;a href="https://greatexpectations.io/">Great Expectations&lt;/a> — data quality expectations en CI.&lt;/li>
&lt;li>&lt;a href="https://mlflow.org/docs/latest/tracking.html">MLflow Tracking&lt;/a> — input datasets como artefactos de primera clase desde MLflow 2.4.&lt;/li>
&lt;li>&lt;a href="https://www.pachyderm.com/">Pachyderm&lt;/a> y &lt;a href="https://quiltdata.com/">Quilt&lt;/a> — alternativas históricas a DVC/lakeFS.&lt;/li>
&lt;li>Sobre contaminación de eval sets: &lt;em>&amp;ldquo;Stop Uploading Test Data in Plain Text&amp;rdquo;&lt;/em> (Magar &amp;amp; Schwartz, 2022) y trabajo posterior sobre detección de contaminación en pretraining corpora.&lt;/li>
&lt;/ul></description></item></channel></rss>