Data versioning para LLMOps: DVC, lakeFS y el reto del golden dataset reproducible

TL;DR

La etapa Data del pipeline LLMOps de seis etapas tiene un eslabón silencioso del que depende todo lo demás: versionar los datasets con la misma disciplina que se versiona el código. No es opcional. Un sistema LLM en producción consume al menos cuatro tipos de dataset diferenciados —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 —DVC y lakeFS— se unificaron en noviembre de 2025 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 (dataset_id, version) inmutable, propagar el par hasta el experiment tracking (MLflow / W&B), versionar también el schema del dataset (no solo el contenido), aplicar holdout estricto al golden eval set para no medir memorización, y mantener trazabilidad bidireccional dataset_version ↔ model_version ↔ deployment ↔ trace_id. Sin esto, la promesa de “podemos auditar qué modelo respondió qué” se cae en el primer incidente serio.

Estás aquí: Data (con efecto transversal sobre Tune, Eval y Retrain)

Este post entra al detalle del eslabón de versionado dentro de la etapa 1 · Data. 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 y una banda transversal indicando el lineage end-to-end.

Estás aquí: DATA · versionado de datasets con lineage hasta el trace de producción1 · Data2 · Tune3 · Eval4 · Deploy5 · Observe6 · RetrainLineage de dataset: training set → Tune · golden set → Eval · enriched set → Retrain (que vuelve a Data)

La analogía maestra: trazabilidad de lote en una fábrica seria

Una fábrica farmacéutica seria no produce sin trazabilidad de lote. 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.

Un sistema LLM serio funciona igual. El “envase” es la respuesta que un usuario vio en producción. El “lote” es la combinación de modelo, adapter, prompt, contexto y configuración que la generó. Y las “materias primas” 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 "¿con qué datos se entrenó el modelo que el 14 de marzo respondió X a mi pregunta Y?", sin trazabilidad de lote la respuesta es “no lo sabemos”. Y eso, en un cliente con compliance encima, mata el contrato.

Git versiona la receta (el código). Data versioning versiona los ingredientes. Sin las dos cosas, no hay fábrica auditable.

Los cuatro artefactos que conviene versionar (con exigencias diferenciadas)

No todos los datasets se versionan igual ni con la misma frecuencia. El sistema LLM en producción típico maneja cuatro artefactos que conviene gobernar por separado.

ArtefactoQué esTamaño típicoFrecuencia de versión nuevaQuién la consume
Training / fine-tuning datasetPares input/output (o conversaciones) que entrenan el adapter o el modelo.10⁴ – 10⁷ ejemplos · 1 – 100 GBPor experimento de TuneTrainer (Axolotl, TRL, Unsloth)
RAG corpusDocumentos indexados que alimentan retrieval.10⁵ – 10⁹ chunks · 10 GB – 10 TBCasi continuo (ingest streaming)Indexer + vector store
Golden eval setEjemplos curados con respuesta esperada para medir calidad.10² – 10⁴ ejemplos · MBPor release del productoEval gates en CI
Enriched retrain datasetCasos donde el sistema falló + corrección humana.Cientos a miles por trimestrePor ciclo de retrainSiguiente Tune

Los cuatro tienen requisitos comunes (identidad inmutable, lineage, schema) y diferencias relevantes:

  • El training set suele ser grande, estable por experimento, y el coste de un error es un experimento perdido (caro pero acotado).
  • El RAG corpus es enorme, en continuo cambio, 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.
  • El golden eval set es pequeño pero crítico: errores aquí contaminan toda la cadena de promotion. Aquí la rigidez del versionado importa más que en ningún otro.
  • El enriched retrain dataset es incremental por naturaleza: cada ciclo de Retrain aporta un delta sobre el anterior. La versión nueva no sobrescribe; hereda y añade.

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.

Por qué Git no basta

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.

Razón 1: tamaño. Un repositorio Git con un dataset de 50 GB se vuelve inmanejable. git clone baja todo el histórico; git status recorre todos los archivos; el pack file en .git/objects 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.

Razón 2: diff binario inútil. 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 diff semántico: cuántas filas cambiaron, qué columnas cambiaron, qué distribución se movió. Ningún Git nativo te da eso.

Razón 3: lineage que cruza fronteras de repositorio. 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 dataset_v3 → adapter_v7 → deployment_d2 → trace t_x9 requiere propagar identificadores a través de cuatro sistemas distintos, no dentro de un repo. Git no tiene opinión sobre esto.

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.

DVC vs lakeFS antes de la unificación

Hasta noviembre de 2025, las dos herramientas dominantes OSS coexistían como aproximaciones complementarias.

EjeDVClakeFS
Modelo mental“Git para datos”“Branching para el data lake”
GranularidadArchivo individualBucket entero (con namespacing por branch)
StorageRemote-agnóstico (S3, GCS, Azure, MinIO, SSH)S3-compatible (S3, MinIO, Ceph)
Workflowdvc add + dvc push + dvc.yaml pipelineslakectl commit + branches/merges sobre el bucket
DiffHash del archivo + metadata externaDiff a nivel de objeto + commit log
Casos fuertesTraining datasets discretos, model files, pipelines reproduciblesRAG corpora grandes, branching de un data lake compartido, experimentos en paralelo sin duplicar datos
Integración con GitProfunda (los .dvc files se commitean a Git)Tangencial (lakeFS vive en paralelo)
Quién lo operaEquipo MLEEquipo data engineering

En la práctica, muchos equipos los usaban a la vez: DVC para los datasets discretos que alimentaban un experimento (cabe en un repo Git por la indirección de los .dvc pointers), y lakeFS para el bucket grande del corpus RAG sobre el que querían branching sin duplicar terabytes.

Qué cambió con la adquisición de noviembre 2025

lakeFS adquirió DVC en noviembre de 2025. La consecuencia operacional a mayo de 2026 es modesta pero relevante:

  • No hay (todavía) fusión técnica de los proyectos. DVC sigue siendo DVC y lakeFS sigue siendo lakeFS. Las CLIs, los formatos y los workflows actuales no han cambiado.
  • Hoja de ruta combinada explícita hacia LLM training y RAG datalakes. La organización fusionada ha enunciado prioridades específicas: branching consistente entre el dataset y el modelo entrenado, integraciones nativas con MLflow / W&B / Langfuse, soporte para los formatos típicos de LLM (jsonl, parquet con tokenización embebida), e indexación vectorial branch-aware.
  • Convergencia esperada en 2026-2027. 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.

La lectura práctica para 2026: adopta DVC para training/eval datasets discretos y lakeFS para el RAG corpus, pero diseña el lineage para que un futuro registry unificado pueda absorber ambos sin re-versionar todo. En concreto: usa identificadores estables (dataset_id, version, commit_hash) que sean propagables independientemente de la herramienta.

El patrón operativo: lineage de cuatro saltos

Una vez aceptado que hay que versionar datasets, la pregunta no es “qué herramienta” sino “qué cadena de identificadores conecta producción con el dato origen”. El patrón que ha consolidado el mercado tiene cuatro saltos:

(dataset_id, dataset_version)
        │  versiona en DVC o lakeFS
        ▼
(model_id, model_version)
        │  registra en MLflow / W&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

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.

Ejemplo concreto del flujo, usando DVC + MLflow:

# Etapa Data: versionar el dataset
dvc add data/finetune_v3.jsonl
git add data/finetune_v3.jsonl.dvc data/.gitignore
git commit -m "data: finetune dataset v3"
dvc push  # sube el binario al remote (MinIO/S3)

# Etapa Tune: entrenar registrando lineage
mlflow run train.py \
  -P dataset_id=finetune \
  -P dataset_version=v3 \
  -P dataset_hash=$(dvc get-url data/finetune_v3.jsonl | sha256sum)
# El run registra: input dataset + model output

# Etapa Eval: validar registrando lineage
mlflow run eval.py \
  -P model_id=adapter_customer_v7 \
  -P golden_set_id=customer_support \
  -P golden_set_version=v12

# Etapa Deploy: el deployment hereda dataset + golden ids
# Cada trace en Observe lleva model_version + prompt_version
# que rebobinan hasta dataset_version

Versión equivalente con lakeFS sobre el RAG corpus:

# Branch para los embeddings del nuevo corpus
lakectl branch create lakefs://corpus/embed-2026q2 --source main

# Indexar el corpus en ese branch
python index_corpus.py --branch embed-2026q2

# Validar antes de mergear a main
python eval_retrieval.py --branch embed-2026q2 \
  --metric recall@10 --threshold 0.78

# Si pasa, mergear (cambia el corpus que sirve producción)
lakectl commit lakefs://corpus/embed-2026q2 -m "embed: corpus 2026q2"
lakectl merge lakefs://corpus/embed-2026q2 lakefs://corpus/main

La virtud del segundo flujo: durante la validación del nuevo corpus, el sistema de producción sigue sirviendo desde main sin interferencia. La rama paralela funciona como un staging real sobre el bucket completo.

Schema contracts: data versioning sin esto es ilusión

Versionar el contenido de un dataset sin versionar su schema 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.

Caso concreto: golden eval set de soporte al cliente, 1000 ejemplos, campo expected_output originalmente string. Alguien decide que necesita capturar varias respuestas válidas y cambia el campo a list[string]. 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 mide otra cosa.

Patrón productivo: el dataset se versiona con DVC/lakeFS y su schema se versiona con Schema Registry (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.

Schema mínimo de un golden eval entry (ilustrativo):

$schema: https://json-schema.org/draft/2020-12/schema
$id: https://example.org/schemas/golden_eval_entry/v3.json
type: object
required: [example_id, input, expected_outputs, rubric, segment]
properties:
  example_id: {type: string, format: uuid}
  input:
    type: object
    required: [user_query, retrieved_context]
    properties:
      user_query: {type: string}
      retrieved_context: {type: array, items: {type: string}}
  expected_outputs:
    type: array
    minItems: 1
    items: {type: string}
  rubric:
    type: object
    required: [must_include, must_not_include, format]
    properties:
      must_include: {type: array, items: {type: string}}
      must_not_include: {type: array, items: {type: string}}
      format: {enum: [text, json, markdown]}
  segment: {type: string}
  difficulty: {enum: [easy, medium, hard]}
  added_at: {type: string, format: date-time}
  curated_by: {type: string}

Reglas operativas:

  • Compatibility forward/backward 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.
  • Versión del schema embebida en cada fila del dataset (un campo _schema_version). El loader valida que la versión coincide con lo que espera el código que lo consume.
  • Schema registry como única fuente de verdad, no como copia opcional del JSON Schema en cuatro repos.

Sin este nivel de disciplina, “tenemos data versioning” significa “guardamos los bytes pero no controlamos qué significan”.

Golden eval set: la versión más crítica

De los cuatro artefactos, el golden eval set 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.

Tres disciplinas extra sobre el golden set:

Anotación con calidad medida. Cada ejemplo lo etiqueta un humano, y un porcentaje (10-20 %) se anota por dos personas independientes. El acuerdo inter-anotador (Cohen’s kappa o F1 pairwise) se mide y se publica; un golden set con kappa < 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.

Holdout estricto contra contaminación. El golden set nunca debe entrar al training set. Mecanismo concreto: hash de cada input 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 “validado” falla en casos análogos al golden set que no estaban memorizados.

Versionado aditivo, nunca destructivo. Cuando el golden set crece (cada ciclo de retrain añade casos), golden_v3 = golden_v2 ∪ new_examples. Nunca golden_v3 = nuevo set distinto. Sólo así puedes comparar dos modelos entrenados a meses de distancia sobre la misma base + 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.

Tabla resumen de la disciplina por artefacto:

PrácticaTraining setRAG corpusGolden eval setEnriched retrain
Versionado inmutableSí (snapshots)Sí, crítico
Schema con contractRecomendadoSí, crítico
Doble anotaciónNoNo aplicaSí (10-20 %)Sí (10-20 %)
Holdout vs otros datasetsN/AN/ASí, hash checkSí (vs golden)
Drift check vs versión anteriorRecomendadoRecomendado
Lineage hasta deployment

Promotion gates: el dataset es promovido como el modelo

Un dataset candidato (un golden_v13 recién enriquecido, un enriched_retrain_2026_q2 resultado del ciclo de Retrain) no entra a producción por estar en el bucket. Pasa por gates equivalentes a los del modelo o del prompt:

  1. Schema validation — el contract se cumple. Bloqueo en CI si no.
  2. Quality validation — muestra aleatoria del 5-10 % revisada por humano con quality score ≥ 4/5. Bloqueo si la muestra falla.
  3. Holdout segregation check — para golden sets y enriched datasets, hash check contra todos los demás datasets activos. Bloqueo si hay solapamiento.
  4. Drift check vs versión anterior — 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.
  5. Lineage check — el dataset declara explícitamente de qué versión hereda y qué cambió. Sin esa metadata, no entra.

Sólo cuando los cinco gates pasan, el dataset se etiqueta como production-ready y se desbloquean los pipelines downstream que dependen de él (el siguiente Tune, el siguiente release del producto, el siguiente ciclo de eval).

El stack on-premise aplicado

En una infraestructura genérica con RTX 4090 (24 GB VRAM, perfil de desarrollo / batch chico) y un cluster 4×H100 SXM (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.

Topología típica:

┌────────────────────────────────────────────────────────────┐
│                 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) │
        └────────────┘         └───────────┘

Notas operativas:

  • El object store (MinIO o Ceph) sirve a la vez como DVC remote y como storage de lakeFS. Un solo plano de almacenamiento, dos vistas.
  • Los schema checks y hash de holdout son tareas CPU-bound rápidas; el CI runner las ejecuta sin GPU.
  • El drift check por embeddings 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.
  • El cluster H100 queda libre para training y serving, sin contaminación por jobs de versionado.

¿Cuándo NO hace falta DVC/lakeFS?

Hay una posición opuesta defendida con datos en el post de fine-tuning continuo: para sistemas pequeños con un único equipo, datasets < 1 GB y un puñado de adapters, Postgres + pgvector + un bucket S3 + filenames con hash son suficientes. La complejidad operativa de DVC/lakeFS no se amortiza.

La línea divisoria es razonable:

  • No hace falta DVC/lakeFS: un solo equipo, datasets pequeños, pocos adapters, sin múltiples productos compartiendo datos.
  • Sí hace falta: múltiples equipos, datasets > 10 GB, varios productos que comparten golden eval set, compliance externo que exige trazabilidad de lote, o un ciclo de retrain trimestral institucionalizado.

Adoptar DVC + lakeFS antes de necesitarlos es overhead. Adoptarlos seis meses tarde es perder seis meses de lineage de manera irrecuperable.

Siete pitfalls que convierten data versioning en teatro

  1. Versionar los datos pero no los schemas. 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.

  2. Mismo S3 path sobrescrito. “Sube training.jsonl al bucket” 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: training_v3.jsonl o training/2026q2/<sha>.jsonl, nunca el mismo nombre.

  3. Golden eval set sin holdout estricto. 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.

  4. No registrar lineage dataset → modelo. Cuando un incidente requiere saber con qué datos se entrenó cierto modelo, la respuesta correcta es un query a MLflow / W&B. Si la respuesta es “preguntemos a quien lo entrenó” (suponiendo que siga en el equipo), el lineage no existe.

  5. DVC añadido seis meses tarde. 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 “lo metemos después”.

  6. lakeFS con branches que nunca se mergean. 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.

  7. Validación de schema solo en producción. 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 en CI, antes del merge, sobre el delta que el PR introduce.

El ciclo de un dataset en una pantalla

┌─────────────────────────────────────────────────────────────┐
│  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│
       └─────────────────────────┘

Lo que no hemos cubierto

A primer nivel queda fuera de este post:

  • Vector store versioning 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).
  • Tooling de lineage estandarizado (OpenLineage, Marquez): cómo emitir y consumir lineage events de manera interoperable entre sistemas.
  • Data quality frameworks (Great Expectations, Soda, Deequ): cómo escribir suites de “expectations” sobre un dataset y enforzarlas en cada versión.
  • Privacy-preserving versioning: federated learning sin centralizar el dataset, differential privacy aplicada a la versión que se distribuye.
  • Contaminación entre golden sets de proveedores externos (HumanEval, MMLU, etc.) y datasets de training de modelos open: el problema de “el modelo aprueba HumanEval porque HumanEval está en su pretraining”.

Cada uno da para un post propio cuando el campo lo justifique.

Ver también

Referencias