<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Dvc on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/dvc/</link><description>Recent content in Dvc on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 23 May 2026 07:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/dvc/index.xml" rel="self" type="application/rss+xml"/><item><title>The OSS catalogue for LLMOps in six stages: tool by tool, what each one does and when to choose it</title><link>https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/</link><pubDate>Sat, 23 May 2026 07:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>For each of the six LLMOps stages (Data, Tune, Eval, Deploy, Observe, Retrain) and the two cross-cutting components (prompt + data versioning), the open source ecosystem has canonical pieces that this blog has been citing again and again. This post gathers them in one place with &lt;strong>cards of roughly 150 words per core tool&lt;/strong>: what it does, how it differs from the alternatives inside the same bucket, its &lt;strong>licence and governance model&lt;/strong>, and a typical gotcha that you only learn in production. Plus more alternatives as bullets, a &lt;strong>decision matrix per stage&lt;/strong> according to the case (small / large corpus, single tenant / multi-tenant…), a &lt;strong>diagram&lt;/strong> of the connected OSS stack and a &lt;strong>master table&lt;/strong> of licences / EE offerings. The intention: that the reader closes the post knowing what is available, which company maintains it, what gap each piece fills, and when to choose it. This is not opinion: it is a curated catalogue.&lt;/p>
&lt;h2 id="you-are-here-every-stage-but-by-oss-column">You are here: every stage, but by OSS column&lt;/h2>
&lt;p>This post shares a map with the two previous ones in the series, the six stages and the two cross-cutting pieces are all active, but it zooms in on the &lt;strong>open source column&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OSS catalogue by LLMOps pipeline stage">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:2}.cross{fill:#ffe9d6;stroke:#c66;stroke-width:1.4;rx:6}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#333}.tiny{font:10px sans-serif;fill:#444}.oss{fill:#dfe9f5;stroke:#356}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="lbl">OSS catalogue: the consultant's toolbox by stage&lt;/text>
&lt;rect x="20" y="38" width="120" height="32" class="box active"/>&lt;text x="80" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="150" y="38" width="120" height="32" class="box active"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="38" width="120" height="32" class="box active"/>&lt;text x="340" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="410" y="38" width="120" height="32" class="box active"/>&lt;text x="470" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="540" y="38" width="120" height="32" class="box active"/>&lt;text x="600" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="670" y="38" width="120" height="32" class="box active"/>&lt;text x="730" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;rect x="20" y="88" width="120" height="78" class="box oss"/>
&lt;text x="80" y="103" text-anchor="middle" class="tiny">DVC · lakeFS&lt;/text>
&lt;text x="80" y="117" text-anchor="middle" class="tiny">MinIO · Ceph&lt;/text>
&lt;text x="80" y="131" text-anchor="middle" class="tiny">Qdrant · pgvector&lt;/text>
&lt;text x="80" y="145" text-anchor="middle" class="tiny">Kafka · Flink&lt;/text>
&lt;text x="80" y="159" text-anchor="middle" class="tiny">Debezium · Karapace&lt;/text>
&lt;rect x="150" y="88" width="120" height="78" class="box oss"/>
&lt;text x="210" y="103" text-anchor="middle" class="tiny">HF Transformers&lt;/text>
&lt;text x="210" y="117" text-anchor="middle" class="tiny">PEFT · bitsandbytes&lt;/text>
&lt;text x="210" y="131" text-anchor="middle" class="tiny">DeepSpeed · FSDP&lt;/text>
&lt;text x="210" y="145" text-anchor="middle" class="tiny">Axolotl · LLaMA-Factory&lt;/text>
&lt;text x="210" y="159" text-anchor="middle" class="tiny">Ray Train · MLflow&lt;/text>
&lt;rect x="280" y="88" width="120" height="78" class="box oss"/>
&lt;text x="340" y="103" text-anchor="middle" class="tiny">DeepEval · RAGAS&lt;/text>
&lt;text x="340" y="117" text-anchor="middle" class="tiny">Promptfoo&lt;/text>
&lt;text x="340" y="131" text-anchor="middle" class="tiny">lm-eval-harness&lt;/text>
&lt;text x="340" y="145" text-anchor="middle" class="tiny">NeMo Guardrails&lt;/text>
&lt;text x="340" y="159" text-anchor="middle" class="tiny">Presidio · LlamaGuard&lt;/text>
&lt;rect x="410" y="88" width="120" height="78" class="box oss"/>
&lt;text x="470" y="103" text-anchor="middle" class="tiny">vLLM · TGI · SGLang&lt;/text>
&lt;text x="470" y="117" text-anchor="middle" class="tiny">TensorRT-LLM&lt;/text>
&lt;text x="470" y="131" text-anchor="middle" class="tiny">llama.cpp · Triton&lt;/text>
&lt;text x="470" y="145" text-anchor="middle" class="tiny">KServe · KubeRay&lt;/text>
&lt;text x="470" y="159" text-anchor="middle" class="tiny">Envoy AI · LiteLLM&lt;/text>
&lt;rect x="540" y="88" width="120" height="78" class="box oss"/>
&lt;text x="600" y="103" text-anchor="middle" class="tiny">OpenTelemetry&lt;/text>
&lt;text x="600" y="117" text-anchor="middle" class="tiny">Tempo · Jaeger&lt;/text>
&lt;text x="600" y="131" text-anchor="middle" class="tiny">Prometheus · Loki&lt;/text>
&lt;text x="600" y="145" text-anchor="middle" class="tiny">Langfuse · Phoenix&lt;/text>
&lt;text x="600" y="159" text-anchor="middle" class="tiny">Tetragon · Evidently&lt;/text>
&lt;rect x="670" y="88" width="120" height="78" class="box oss"/>
&lt;text x="730" y="103" text-anchor="middle" class="tiny">Airflow · Prefect&lt;/text>
&lt;text x="730" y="117" text-anchor="middle" class="tiny">Dagster · Argo&lt;/text>
&lt;text x="730" y="131" text-anchor="middle" class="tiny">Kubeflow Pipelines&lt;/text>
&lt;text x="730" y="145" text-anchor="middle" class="tiny">Feast&lt;/text>
&lt;text x="730" y="159" text-anchor="middle" class="tiny">Argilla · Label Studio&lt;/text>
&lt;rect x="20" y="180" width="380" height="22" class="cross"/>
&lt;text x="210" y="195" text-anchor="middle" class="sm">Prompt versioning: Langfuse Prompts · MLflow Prompt Registry&lt;/text>
&lt;rect x="410" y="180" width="380" height="22" class="cross"/>
&lt;text x="600" y="195" text-anchor="middle" class="sm">Data versioning: DVC · lakeFS · OpenLineage · DataHub&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-the-electricians-toolbox">The analogy: the electrician&amp;rsquo;s toolbox&lt;/h2>
&lt;p>A professional electrician arrives at an installation with a box organised into compartments. Nothing is improvised: for every type of cable there is a specific wire stripper, for every screw a screwdriver of the exact gauge, for every measurement a multimeter and a clamp meter, for every connection the right terminal block or connector. The difference between a professional electrician and a handyman is not that one knows more theory, the handyman has often read the manuals, it is that &lt;strong>the right tool is within arm&amp;rsquo;s reach and he knows when to use each one&lt;/strong>. The day the specific stripper is missing, improvising with a box cutter breaks the insulation, leaves a badly terminated cable, and the panel comes back under warranty two months later.&lt;/p>
&lt;p>The OSS LLMOps stack works the same way. For every canonical problem, versioning a dataset, indexing a corpus for retrieval, serving tokens with dynamic batching, propagating &lt;code>trace_id&lt;/code> end-to-end, managing prompts with a &lt;code>production&lt;/code> label, orchestrating retraining pipelines, there is a canonical piece of the open source ecosystem that solves it, maintained by a serious community or foundation, with a clear licence and a well documented gotcha. The consultant who knows which tool to use for each job builds a robust system in weeks; the one who improvises with &amp;ldquo;whatever the team already knows&amp;rdquo; pays later in operations, usually once the system is carrying real load and any replacement is expensive.&lt;/p>
&lt;p>This post opens the toolbox and shows each card. It is not a user manual, that is what the deep-dive posts linked at the end are for; it is the &lt;strong>curated catalogue&lt;/strong>.&lt;/p>
&lt;h2 id="diagram-of-the-connected-reference-oss-stack">Diagram of the connected reference OSS stack&lt;/h2>
&lt;p>The catalogue makes sense once you see how the pieces connect into a single coherent architecture, the one this blog has been describing throughout the series. The boxes do not float; they talk to each other over stable contracts (HTTP, gRPC, OTel, Kafka, S3/MinIO API).&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="reference OSS LLMOps stack with connected boxes">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6;fill:#eef2f7}.serv{fill:#ff8a4c;stroke:#a44}.data{fill:#dfe9f5;stroke:#356}.obs{fill:#d8eecf;stroke:#373}.ctrl{fill:#f5e3d8;stroke:#763}.bg{fill:#fafafa;stroke:#ccc;rx:8}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}.otel{stroke:#1a73e8;stroke-width:1.4;fill:none;stroke-dasharray:3 2;marker-end:url(#ab)}&lt;/style>
&lt;defs>&lt;marker id="a" 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;marker id="ab" 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="#1a73e8"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="20" text-anchor="middle" class="lbl">Connected OSS LLMOps stack: serving, data, observability and control plane&lt;/text>
&lt;rect x="20" y="40" width="780" height="80" class="b bg"/>
&lt;text x="30" y="58" class="tiny">SERVING PLANE&lt;/text>
&lt;rect x="40" y="68" width="120" height="40" class="b serv"/>&lt;text x="100" y="93" text-anchor="middle" class="sm">Envoy AI Gateway&lt;/text>
&lt;rect x="180" y="68" width="120" height="40" class="b serv"/>&lt;text x="240" y="93" text-anchor="middle" class="sm">vLLM (LoRA hot-swap)&lt;/text>
&lt;rect x="320" y="68" width="120" height="40" class="b serv"/>&lt;text x="380" y="93" text-anchor="middle" class="sm">KServe / Operator&lt;/text>
&lt;rect x="460" y="68" width="120" height="40" class="b serv"/>&lt;text x="520" y="93" text-anchor="middle" class="sm">Triton (multi-model)&lt;/text>
&lt;rect x="600" y="68" width="180" height="40" class="b serv"/>&lt;text x="690" y="93" text-anchor="middle" class="sm">Kubernetes (RKE2 + Cilium)&lt;/text>
&lt;path class="arr" d="M160,88 L180,88"/>&lt;path class="arr" d="M300,88 L320,88"/>
&lt;rect x="20" y="140" width="380" height="170" class="b bg"/>
&lt;text x="30" y="158" class="tiny">DATA PLANE&lt;/text>
&lt;rect x="40" y="168" width="160" height="36" class="b data"/>&lt;text x="120" y="190" text-anchor="middle" class="sm">PostgreSQL + pgvector&lt;/text>
&lt;rect x="220" y="168" width="160" height="36" class="b data"/>&lt;text x="300" y="190" text-anchor="middle" class="sm">Qdrant (RAG)&lt;/text>
&lt;rect x="40" y="214" width="160" height="36" class="b data"/>&lt;text x="120" y="236" text-anchor="middle" class="sm">MinIO / Ceph (S3)&lt;/text>
&lt;rect x="220" y="214" width="160" height="36" class="b data"/>&lt;text x="300" y="236" text-anchor="middle" class="sm">Kafka + Debezium&lt;/text>
&lt;rect x="40" y="260" width="160" height="36" class="b data"/>&lt;text x="120" y="282" text-anchor="middle" class="sm">DVC + lakeFS&lt;/text>
&lt;rect x="220" y="260" width="160" height="36" class="b data"/>&lt;text x="300" y="282" text-anchor="middle" class="sm">Flink / Spark&lt;/text>
&lt;path class="arr" d="M380,186 L460,186 L460,88 L460,108"/>
&lt;rect x="420" y="140" width="380" height="170" class="b bg"/>
&lt;text x="430" y="158" class="tiny">OBSERVABILITY PLANE&lt;/text>
&lt;rect x="440" y="168" width="160" height="36" class="b obs"/>&lt;text x="520" y="190" text-anchor="middle" class="sm">OTel Collector&lt;/text>
&lt;rect x="620" y="168" width="160" height="36" class="b obs"/>&lt;text x="700" y="190" text-anchor="middle" class="sm">Langfuse&lt;/text>
&lt;rect x="440" y="214" width="160" height="36" class="b obs"/>&lt;text x="520" y="236" text-anchor="middle" class="sm">Tempo (traces)&lt;/text>
&lt;rect x="620" y="214" width="160" height="36" class="b obs"/>&lt;text x="700" y="236" text-anchor="middle" class="sm">Phoenix Arize OSS&lt;/text>
&lt;rect x="440" y="260" width="160" height="36" class="b obs"/>&lt;text x="520" y="282" text-anchor="middle" class="sm">Prometheus + Grafana&lt;/text>
&lt;rect x="620" y="260" width="160" height="36" class="b obs"/>&lt;text x="700" y="282" text-anchor="middle" class="sm">Loki + Tetragon&lt;/text>
&lt;path class="otel" d="M240,108 L240,130 L460,130 L460,168"/>
&lt;text x="350" y="146" class="tiny" fill="#1a73e8">OTel spans (gen_ai.*)&lt;/text>
&lt;rect x="20" y="330" width="780" height="110" class="b bg"/>
&lt;text x="30" y="348" class="tiny">CONTROL PLANE (Tune + Eval + Retrain + Prompt versioning)&lt;/text>
&lt;rect x="40" y="358" width="140" height="36" class="b ctrl"/>&lt;text x="110" y="380" text-anchor="middle" class="sm">Axolotl + PEFT&lt;/text>
&lt;rect x="200" y="358" width="140" height="36" class="b ctrl"/>&lt;text x="270" y="380" text-anchor="middle" class="sm">MLflow Tracking&lt;/text>
&lt;rect x="360" y="358" width="140" height="36" class="b ctrl"/>&lt;text x="430" y="380" text-anchor="middle" class="sm">Promptfoo + RAGAS&lt;/text>
&lt;rect x="520" y="358" width="140" height="36" class="b ctrl"/>&lt;text x="590" y="380" text-anchor="middle" class="sm">Argo / Kubeflow Pipelines&lt;/text>
&lt;rect x="680" y="358" width="100" height="36" class="b ctrl"/>&lt;text x="730" y="380" text-anchor="middle" class="sm">Argilla&lt;/text>
&lt;rect x="40" y="402" width="140" height="30" class="b ctrl"/>&lt;text x="110" y="420" text-anchor="middle" class="sm">Langfuse Prompts&lt;/text>
&lt;rect x="200" y="402" width="140" height="30" class="b ctrl"/>&lt;text x="270" y="420" text-anchor="middle" class="sm">Feast&lt;/text>
&lt;rect x="360" y="402" width="140" height="30" class="b ctrl"/>&lt;text x="430" y="420" text-anchor="middle" class="sm">NeMo Guardrails&lt;/text>
&lt;rect x="520" y="402" width="140" height="30" class="b ctrl"/>&lt;text x="590" y="420" text-anchor="middle" class="sm">OpenLineage + DataHub&lt;/text>
&lt;rect x="680" y="402" width="100" height="30" class="b ctrl"/>&lt;text x="730" y="420" text-anchor="middle" class="sm">Presidio&lt;/text>
&lt;path class="arr" d="M110,358 L110,300 L110,300"/>&lt;path class="arr" d="M110,300 L110,260 L120,260"/>
&lt;/svg>
&lt;/div>
&lt;p>Solid arrows mark data / control flow; the dashed blue ones are OTel traces. The K8s plane holds everything up. The control plane at the bottom is where the retraining pipelines, the evals in CI, the versioned prompts and the lineage live. The data plane on the left feeds both the serving side (RAG, configs) and the control plane (datasets, lineage). The observability plane receives from serving and from everything else.&lt;/p>
&lt;p>Now we go stage by stage. Each one opens with a context paragraph, then cards for the core tools (about 150 words each), bullets with relevant alternatives, and a specific decision matrix at the end.&lt;/p>
&lt;h2 id="stage-1--data--cross-cutting-data-versioning">Stage 1 — Data + cross-cutting Data versioning&lt;/h2>
&lt;p>The Data stage solves three distinct problems that beginners confuse: &lt;strong>versioning&lt;/strong> datasets (so that &lt;code>(dataset_id, version, hash)&lt;/code> exists and propagates), &lt;strong>storing and serving&lt;/strong> the operational corpus (object store + vector index + structured text), and &lt;strong>moving it&lt;/strong> between systems with CDC and stable schemas. Covered in detail in the posts on &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">data versioning with DVC and lakeFS&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">PostgreSQL + Qdrant in ingestion&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka&lt;/a>.&lt;/p>
&lt;h3 id="dvc-data-version-control">DVC (Data Version Control)&lt;/h3>
&lt;p>DVC puts datasets under version control with the same discipline that git applies to code. The &lt;code>.dvc&lt;/code> pointers live in git (plain text, about 200 bytes per dataset), the bulk content lives in a remote object store (S3, MinIO, Azure Blob, GCS). Each &lt;code>dvc add&lt;/code> computes a SHA-256 hash of the dataset, uploads it to the remote and stores the pointer. The key line: the &lt;code>dataset_hash&lt;/code> becomes the luggage tag that travels to the trainer, to experiment tracking and to lineage. The same dataset retrained twice produces the same hash, and therefore reproducible experiments. DVC integrates with MLflow and W&amp;amp;B as an input artefact. &lt;strong>Gotcha:&lt;/strong> it works well for datasets that change by replacement (I swap &lt;code>train.jsonl&lt;/code> for a new version) and worse for datasets with thousands of small files that change individually. For that case, combine it with lakeFS. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Iterative.ai&lt;/strong> since 2017. There is DVC Studio (managed) and &lt;code>dvc data&lt;/code> (pure CLI) on different planes.&lt;/p>
&lt;h3 id="lakefs">lakeFS&lt;/h3>
&lt;p>lakeFS brings git semantics (branch, commit, merge, rollback) to an entire S3/MinIO/ADLS bucket. Where DVC versions individual files as pointers in git, lakeFS versions &lt;strong>the whole bucket&lt;/strong>: you can branch the corpus, ingest new data into the branch, validate that it passes checks (recall@10 over golden queries for embeddings, completeness for a tabular corpus), and only then merge into &lt;code>main&lt;/code>. It is the piece that makes continuous RAG safe: the production corpus is always on &lt;code>main&lt;/code>, updates are tested on branches. It offers hooks (pre-merge, pre-commit) that fire automatic validations, and time-travel to reproduce the state of the bucket at a past date. &lt;strong>Gotcha:&lt;/strong> the manifest overhead on huge buckets (hundreds of millions of objects) deserves sizing; lakeFS keeps metadata in its own Postgres, not in the bucket. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Treeverse&lt;/strong> since 2020. Managed offering: &lt;strong>lakeFS Cloud&lt;/strong>.&lt;/p>
&lt;h3 id="minio">MinIO&lt;/h3>
&lt;p>MinIO is the S3-compatible object store that fills the &amp;ldquo;S3 on-premise&amp;rdquo; gap without surprises. API identical to S3 (the AWS SDKs work by pointing them at a different endpoint), its own CLI client (&lt;code>mc&lt;/code>), erasure-coded mode for fault tolerance, bucket-to-bucket replication, encryption at rest. It is the base on which the other data plane components are built: DVC remote, lakeFS underlying storage, Postgres snapshots, MLflow artefacts, eval datasets, saved models, distributed KV cache fabric. In small deployments it runs single-node multi-disk; in serious ones, distributed clusters. &lt;strong>Gotcha:&lt;/strong> the licence changed to &lt;strong>AGPLv3&lt;/strong> in 2021 (it was Apache 2.0 before), which means distributing software connected to MinIO obliges you to open the code that connects to it. For internal on-premise use this is not a problem; for a vendor packaging MinIO inside a commercial product, it is. Maintained by &lt;strong>MinIO Inc.&lt;/strong> with a SUBNET enterprise offering and a community fork called &lt;strong>AIStor&lt;/strong> launched in 2025.&lt;/p>
&lt;h3 id="qdrant">Qdrant&lt;/h3>
&lt;p>Qdrant is the OSS vector database most aligned with this blog&amp;rsquo;s &amp;ldquo;RAG corpus per tenant with strict ACLs&amp;rdquo; pattern. Written in Rust, it exposes a REST + gRPC API, indexes with HNSW + scalar/binary quantisation to cut memory, supports efficient payload filtering (it is not post-filtering: it integrates the filter into the HNSW search), and allows collections isolated per tenant. For the &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">multi-tenant chatbot&lt;/a> scenario, Qdrant is where the &lt;code>tenant_&amp;lt;id&amp;gt;_kb_v3&lt;/code> collections with strict ACL live. It scales well horizontally (sharding by payload) and vertically (millions of chunks on a node with 64GB RAM). &lt;strong>Gotcha:&lt;/strong> binary quantisation is aggressive, it cuts VRAM by 32× but degrades recall by 10-20 %; enabling it without re-tuning the threshold breaks retrieval silently. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Qdrant Solutions GmbH&lt;/strong> (Germany). There is Qdrant Cloud (managed) and EU-only support for ENS cases.&lt;/p>
&lt;h3 id="postgresql--pgvector">PostgreSQL + pgvector&lt;/h3>
&lt;p>Postgres 18 with the &lt;code>pgvector&lt;/code> extension is the stack&amp;rsquo;s &amp;ldquo;hidden vector database&amp;rdquo;: when the corpus is small (under a million embeddings) and Postgres is already in production for operational data, running a separate Qdrant is expensive operations. pgvector adds a &lt;code>vector(dim)&lt;/code> type, HNSW and IVF indexes, and the operators &lt;code>&amp;lt;-&amp;gt;&lt;/code>, &lt;code>&amp;lt;#&amp;gt;&lt;/code>, &lt;code>&amp;lt;=&amp;gt;&lt;/code> for cosine, L2 and dot product. Combined with &lt;code>tsvector&lt;/code> (Postgres full-text search) it allows &lt;strong>hybrid search&lt;/strong>, dense + sparse, in a single SQL query. Version 0.8 (2025) introduced halfvec and bit support to reduce size by 4×-8×. &lt;strong>Gotcha:&lt;/strong> HNSW in pgvector consumes a fair amount of RAM to build the index (roughly 2× the size of the embeddings) and blocks inserts during the build; in production you build on a secondary, promote it, and discard the primary. Licence &lt;strong>PostgreSQL License&lt;/strong> (permissive BSD-style) for both the core and pgvector. Maintained by the &lt;strong>PostgreSQL Development Group&lt;/strong> plus pgvector by &lt;strong>Andrew Kane&lt;/strong> + Crunchy Data + Neon.&lt;/p>
&lt;h3 id="apache-kafka--debezium">Apache Kafka + Debezium&lt;/h3>
&lt;p>Kafka is the event bus where &amp;ldquo;everything that happens in the company is a stream&amp;rdquo; becomes real. For LLMOps in production it serves two functions: &lt;strong>CDC from source systems&lt;/strong> (Debezium captures changes in Postgres / MySQL / MongoDB and publishes them as topics) and &lt;strong>LLM event buffer&lt;/strong> (every request, every piece of feedback, every eval result ends up in a topic with the &lt;code>trace_id&lt;/code> propagated). As the &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">post on RAG over Kafka&lt;/a> explains, the RAG corpus is kept fresh by capturing changes from the CMS / source system as CDC, running the embedding in Flink streaming, and ingesting into Qdrant continuously. &lt;strong>Gotcha:&lt;/strong> a badly sized Kafka with long retention plus multi-client topics turns into a fast disk sink; measuring throughput per topic and key cardinality before production is mandatory. Kafka licence &lt;strong>Apache 2.0&lt;/strong> (&lt;strong>ASF&lt;/strong> project); Debezium &lt;strong>Apache 2.0&lt;/strong> (project incubated by &lt;strong>Red Hat&lt;/strong>). Kafka-compatible drop-in alternative: &lt;strong>Redpanda&lt;/strong> (BSL, restricted commercial use).&lt;/p>
&lt;h3 id="apache-flink-brief-mention">Apache Flink (brief mention)&lt;/h3>
&lt;p>Flink processes streams with sub-second latency and exactly-once semantics. On the LLM plane it is used to: run embeddings in streaming (over CDC topics), aggregate online metrics, materialise features for retraining. Licence Apache 2.0, ASF. Common alternative: &lt;strong>Spark Structured Streaming&lt;/strong> (also ASF, micro-batch latency).&lt;/p>
&lt;p>&lt;strong>More options for Data&lt;/strong>, mentioned on the blog:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Ceph&lt;/strong> — object store for large clusters with geo-distributed replication. LGPL/Apache licence, &lt;strong>Red Hat / IBM&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Milvus&lt;/strong> — C++ vector database, an alternative to Qdrant; better for corpora of billions. Apache 2.0, &lt;strong>Zilliz&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Karapace&lt;/strong> — Confluent-compatible OSS Schema Registry. Apache 2.0, &lt;strong>Aiven&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>DataHub / Apache Atlas / OpenMetadata&lt;/strong> — catalogue + lineage. Apache 2.0, &lt;strong>Acryl Data / ASF / Collate&lt;/strong> respectively.&lt;/li>
&lt;li>&lt;strong>OpenLineage&lt;/strong> — cross-system lineage event standard. Apache 2.0, &lt;strong>Linux Foundation AI&amp;amp;Data&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Decision matrix — Data:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If your case is&lt;/th>
&lt;th>Choose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Corpus &amp;lt; 1M embeddings, you already have Postgres&lt;/td>
&lt;td>&lt;strong>pgvector&lt;/strong> (one component fewer)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Corpus 1M-100M, multi-tenant with ACL&lt;/td>
&lt;td>&lt;strong>Qdrant&lt;/strong> (integrated filtering, ACLs per collection)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Corpus &amp;gt; 100M, aggressive sharding&lt;/td>
&lt;td>&lt;strong>Milvus&lt;/strong> (scales linearly better into the billions)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Training datasets + experiment tracking&lt;/td>
&lt;td>&lt;strong>DVC&lt;/strong> over MinIO + MLflow integration&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RAG corpus with controlled releases&lt;/td>
&lt;td>&lt;strong>lakeFS&lt;/strong> over MinIO + pre-merge hooks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>You want both&lt;/td>
&lt;td>&lt;strong>DVC + lakeFS&lt;/strong> as complements (the blog&amp;rsquo;s recommendation)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="stage-2--tune">Stage 2 — Tune&lt;/h2>
&lt;p>The Tune stage produces a new &lt;code>model_id, model_version&lt;/code>, typically a LoRA adapter over a stable base, with lineage back to the dataset and experiment tracking to reproduce it. Detail in the &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">post on continuous fine-tuning&lt;/a>.&lt;/p>
&lt;h3 id="huggingface-transformers--peft">HuggingFace Transformers + PEFT&lt;/h3>
&lt;p>&lt;code>transformers&lt;/code> is the canonical library for loading and training models of the decoder-only family (Llama, Mistral, Qwen, Gemma…) and encoder-decoder. &lt;code>peft&lt;/code> (Parameter-Efficient Fine-Tuning) is the complement that adds declarative support for LoRA, QLoRA, IA3 and assorted adapters. Together they form the &lt;strong>mandatory core&lt;/strong> of the OSS Tune stack: any higher-level framework (Axolotl, LLaMA-Factory) uses them underneath. PEFT allows you to train an adapter of about 280 MB (order of magnitude) instead of a full model of about 140 GB, with a functionally equivalent result on most style / domain adjustment tasks. &lt;strong>Gotcha:&lt;/strong> PEFT with a badly configured &lt;code>target_modules&lt;/code> trains an adapter that covers only Q and V of the attention, leaving out key, output proj and MLP. The result looks trained but performs poorly; adding &lt;code>target_modules=[&amp;quot;all-linear&amp;quot;]&lt;/code> fixes it (at the cost of a larger adapter). Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Hugging Face SAS&lt;/strong> (a French company); open governance model with active external maintainers.&lt;/p>
&lt;h3 id="bitsandbytes">bitsandbytes&lt;/h3>
&lt;p>bitsandbytes implements weight quantisation to 8-bit and 4-bit with NF4 for models loaded with &lt;code>transformers&lt;/code>. It reduces the 140 GB of Llama 3 70B FP16 to about 40 GB in NF4, allowing QLoRA training on a single H100 80GB. The trick is that the weights stay quantised in memory while the sensitive computations (attention, gradient updates in the adapter) are done in FP16/BF16 with on-the-fly dequantisation. Ideal for fine-tuning on limited hardware and for serving with vLLM when you want to cut VRAM. &lt;strong>Gotcha:&lt;/strong> NF4 quantisation is lossy; on small models (&amp;lt; 7B) the quality degradation is noticeable. For production serving of models under 7B, INT8 is preferred (more memory, less loss) or FP8 if the hardware supports it (the H100 does). Licence &lt;strong>MIT&lt;/strong>, maintained by &lt;strong>Tim Dettmers&lt;/strong> (originally at U. Washington, now with support from Anthropic and HuggingFace).&lt;/p>
&lt;h3 id="mlflow-tracking">MLflow Tracking&lt;/h3>
&lt;p>MLflow is the reference OSS experiment tracking tool: each trainer run records parameters (lr, batch size, epochs, target_modules), metrics (loss curves, eval scores), artefacts (model, tokeniser, configs) and, crucially, &lt;strong>input artefacts&lt;/strong> (dataset_id, dataset_hash, parent_run). The model registry associates each &lt;code>model_version&lt;/code> with a reproducible &lt;code>run_id&lt;/code>. The line of continuity between Tune and Deploy passes through here: the deployment reads the model to serve from the registry, with its lineage made explicit. MLflow 2.x integrates &lt;strong>MLflow Prompts&lt;/strong> (prompt registry) and &lt;strong>MLflow Tracing&lt;/strong> (OTel-compatible spans), cutting the number of components needed. &lt;strong>Gotcha:&lt;/strong> the default backend store is SQLite, which works for personal experiments and breaks on a shared cluster. In production: Postgres as the backend store + MinIO/S3 as the artefact store. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>LF AI &amp;amp; Data&lt;/strong> (donated by Databricks in 2020).&lt;/p>
&lt;h3 id="axolotl">Axolotl&lt;/h3>
&lt;p>Axolotl wraps &lt;code>transformers + PEFT + bitsandbytes + DeepSpeed + FSDP&lt;/code> in a declarative YAML configuration: instead of writing a script of about 300 lines to configure a fine-tuning run, you define a &lt;code>config.yml&lt;/code> with base model, dataset path, LoRA config and training hyperparameters, and run it in one line. It supports Llama, Mistral, Qwen, Gemma, Phi… loads. It keeps compatibility with the HuggingFace Hub to download models and datasets, and with MLflow / W&amp;amp;B for tracking. It is the convenience framework this blog cites when it talks about &amp;ldquo;productive fine-tuning without reinventing the wheel&amp;rdquo;. &lt;strong>Gotcha:&lt;/strong> the pace of change in the community is fast; a &lt;code>config.yml&lt;/code> that worked six months ago can break with a current version because of internal refactors. Pinning the exact Axolotl version in the environment mitigates this. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by the &lt;strong>OpenAccess AI Collective&lt;/strong> (community-driven). A very similar alternative, more widely used in China: &lt;strong>LLaMA-Factory&lt;/strong> (Apache 2.0, Beihang U.).&lt;/p>
&lt;h3 id="ray-train">Ray Train&lt;/h3>
&lt;p>Ray Train scales fine-tuning to multiple nodes by distributing workers across a Ray cluster. While DeepSpeed and FSDP are &lt;strong>intra-job parallelism&lt;/strong> (several GPUs collaborating on one job), Ray Train is the &lt;strong>orchestration plane&lt;/strong> that builds the cluster, launches workers, manages checkpoints, recovers from node failures, and integrates with Slurm or Kubernetes. For training runs beyond 8 GPUs on changing clusters, Ray Train avoids the operational burden of &amp;ldquo;manually launching N torchrun processes with NCCL&amp;rdquo;. It combines with MLflow for tracking. &lt;strong>Gotcha:&lt;/strong> the Ray learning curve is real; for a single node with 4-8 GPUs, &lt;code>torchrun&lt;/code> or Hugging Face Accelerate are simpler. Ray Train shines when there are N changing nodes. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Anyscale Inc.&lt;/strong> (commercial backer) + community. A more K8s-native alternative: &lt;strong>Kubeflow Training Operator&lt;/strong> (Apache 2.0, LF AI &amp;amp; Data).&lt;/p>
&lt;p>&lt;strong>More options for Tune:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>DeepSpeed&lt;/strong> — ZeRO parallelism in 3 stages, mixed precision, CPU/NVMe offload. MIT, &lt;strong>Microsoft&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>FSDP&lt;/strong> (Fully Sharded Data Parallel) — native PyTorch parallelism, an alternative to DeepSpeed. BSD, &lt;strong>Meta&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>LLaMA-Factory&lt;/strong> — equivalent to Axolotl with a focus on the Llama family. Apache 2.0, &lt;strong>Beihang University&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Decision matrix — Tune:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If your case is&lt;/th>
&lt;th>Choose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Fine-tune on 1 GPU with 24GB (RTX 4090)&lt;/td>
&lt;td>&lt;strong>QLoRA with bitsandbytes NF4&lt;/strong> + Axolotl&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fine-tune on 1 H100 80GB, models &amp;lt; 13B&lt;/td>
&lt;td>&lt;strong>LoRA bf16&lt;/strong> + Axolotl&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fine-tune on 4-8 GPUs, single node&lt;/td>
&lt;td>&lt;strong>transformers + PEFT + Accelerate&lt;/strong> + MLflow&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-node fine-tune on a K8s cluster&lt;/td>
&lt;td>&lt;strong>Kubeflow Training Operator&lt;/strong> or &lt;strong>Ray Train&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reproducible tracking is mandatory&lt;/td>
&lt;td>&lt;strong>MLflow + DVC input artefact&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>You want the minimum viable&lt;/td>
&lt;td>&lt;strong>Axolotl + MLflow&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="stage-3--eval--guardrails">Stage 3 — Eval + Guardrails&lt;/h2>
&lt;p>Eval validates candidates pre and post promotion against a golden set with operational metrics; Guardrails runs safety online. Detailed in the posts on &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">evals&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a>.&lt;/p>
&lt;h3 id="deepeval">DeepEval&lt;/h3>
&lt;p>DeepEval is the &amp;ldquo;pytest-style&amp;rdquo; OSS eval suite: you define tests with assertions over faithfulness, answer relevancy, contextual precision, hallucination rate, summarisation quality… and run them in CI. Each metric is an evaluator: some rule-based, others LLM-as-judge with auditable prompts. The philosophy is &amp;ldquo;evals as unit tests&amp;rdquo;: parameterisable by dataset, failable in CI, integrable with GitHub Actions. &lt;strong>Gotcha:&lt;/strong> LLM-as-judge metrics vary between judge model versions, so if the judge moves up a version, the thresholds lose their earlier statistical meaning. Explicit pinning of the judge model in config plus periodic threshold recalibration is mandatory discipline. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Confident AI&lt;/strong> (a company); parallel commercial SaaS offering. Comparable: &lt;strong>TruLens&lt;/strong> (MIT, &lt;strong>TruEra&lt;/strong>) and &lt;strong>G-Eval&lt;/strong> (academic).&lt;/p>
&lt;h3 id="ragas-rag-assessment">RAGAS (RAG Assessment)&lt;/h3>
&lt;p>RAGAS specialises in evaluating RAG pipelines. It defines four canonical metrics: &lt;strong>faithfulness&lt;/strong> (the answer is supported by the retrieved chunks), &lt;strong>answer relevancy&lt;/strong> (the answer addresses the query), &lt;strong>context precision&lt;/strong> (the retrieved chunks are relevant), &lt;strong>context recall&lt;/strong> (all the relevant chunks were retrieved). Each metric is computed with LLM-as-judge over a dataset of (query, context, expected answer). For a RAG system, RAGAS is the evaluator that measures whether retrieval is aligned with generation. It integrates with Langfuse and MLflow to store results. &lt;strong>Gotcha:&lt;/strong> RAGAS works well with golden sets of under 1000 examples; over huge golden sets the cost of the LLM judge per evaluation explodes, and the practice is to sample. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Exploding Gradients&lt;/strong> (the authors&amp;rsquo; company).&lt;/p>
&lt;h3 id="promptfoo">Promptfoo&lt;/h3>
&lt;p>Promptfoo is the declarative, CI-oriented evaluator: in &lt;code>promptfooconfig.yaml&lt;/code> you define a set of prompts and a set of assertions (contains text X, does not contain Y, faithfulness &amp;gt; 0.8, judge approves…), point at a provider (OpenAI compatible, vLLM, Ollama…), and &lt;code>promptfoo eval&lt;/code> runs the matrix of prompts × providers × assertions, returns a diff against the baseline and fails CI if anything regresses. It is the most &amp;ldquo;DevOps-friendly&amp;rdquo; piece of the evals ecosystem: it integrates trivially with GitHub Actions, GitLab CI or Jenkins. &lt;strong>Gotcha:&lt;/strong> assertion thresholds have to be calibrated with real data; starting at a default &lt;code>&amp;gt; 0.5&lt;/code> produces false positives that erode the team&amp;rsquo;s trust. Calibrate after the first week. Licence &lt;strong>MIT&lt;/strong>, maintained by &lt;strong>Promptfoo, Inc.&lt;/strong> (a company); a commercial Promptfoo Cloud SaaS offering exists but the OSS version is complete.&lt;/p>
&lt;h3 id="nemo-guardrails">NeMo Guardrails&lt;/h3>
&lt;p>NeMo Guardrails is NVIDIA&amp;rsquo;s framework for defining and enforcing policies in LLM systems through a DSL called &lt;strong>Colang&lt;/strong>. It lets you express rules such as &amp;ldquo;if the user asks about topic X, answer with template Y&amp;rdquo; or &amp;ldquo;if the model tries to do Z, block it&amp;rdquo; in a conversational-script syntax rather than in Python. It runs as middleware between the app and the model: input rails (validate what comes in), output rails (validate what goes out), dialog rails (control the flow). Designed for complex multi-turn systems where the policies are nontrivial. &lt;strong>Gotcha:&lt;/strong> Colang adds latency per turn (roughly 50-200 ms depending on the policy graph); for high-throughput conversational chat, dialog rails are disabled and only input + output remain. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>NVIDIA&lt;/strong>.&lt;/p>
&lt;h3 id="microsoft-presidio">Microsoft Presidio&lt;/h3>
&lt;p>Presidio is the most mature OSS PII (Personally Identifiable Information) detector in the ecosystem. It detects DNI, NIE, IBAN, phone numbers, emails, physical addresses, credit card numbers, personal names, dates of birth… with recognisers based on regex + NER (spaCy) + custom validators. It supports &lt;strong>redaction&lt;/strong> (replace with placeholders), &lt;strong>masking&lt;/strong> (asterisks) or &lt;strong>deterministic anonymisation&lt;/strong> (repeatable hash). For ENS/NIS2 scenarios, it is the piece placed in front (on input) and behind (on output) the LLM to guarantee that no PII is processed or emitted. &lt;strong>Gotcha:&lt;/strong> the built-in recognisers cover English well and the rest badly; for Spanish, Catalan and Basque you have to add custom recognisers, which is disciplined work but doable. Licence &lt;strong>MIT&lt;/strong>, maintained by &lt;strong>Microsoft&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>More options for Eval:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Phoenix Arize OSS&lt;/strong> — combines tracing + evals, an alternative to Langfuse Evals. ELv2, &lt;strong>Arize AI&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>lm-eval-harness&lt;/strong> — academic suite with standard benchmarks (MMLU, HellaSwag…). MIT, &lt;strong>EleutherAI&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>HELM&lt;/strong> — holistic academic evals. Apache 2.0, &lt;strong>Stanford CRFM&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Guardrails AI&lt;/strong> — a pythonic alternative to NeMo Guardrails. Apache 2.0, &lt;strong>Guardrails AI Inc.&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>LlamaGuard / PromptGuard / ShieldGemma&lt;/strong> — safety models, not frameworks. Open weights, Meta / Google.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Decision matrix — Eval + Guardrails:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If your case is&lt;/th>
&lt;th>Choose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Eval in CI, &amp;ldquo;pytest for LLMs&amp;rdquo; style&lt;/td>
&lt;td>&lt;strong>Promptfoo + GitHub Actions&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Eval specific to a RAG pipeline&lt;/td>
&lt;td>&lt;strong>RAGAS + Langfuse datasets&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>General eval with custom metrics&lt;/td>
&lt;td>&lt;strong>DeepEval + MLflow dataset&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dialog policy with declarative rules&lt;/td>
&lt;td>&lt;strong>NeMo Guardrails (Colang)&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PII redaction in/out only&lt;/td>
&lt;td>&lt;strong>Presidio&lt;/strong> (you do not need NeMo)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Open safety model in Spanish&lt;/td>
&lt;td>&lt;strong>LlamaGuard 3&lt;/strong> or &lt;strong>ShieldGemma&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="stage-4--deploy">Stage 4 — Deploy&lt;/h2>
&lt;p>Deploy serves tokens to the user with predictable throughput and latency, adapter hot-swap and multi-tenancy where it applies. Covered in the posts on &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on K8s&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM operators&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">multi-tenant cluster&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">disaggregated serving&lt;/a>.&lt;/p>
&lt;h3 id="vllm">vLLM&lt;/h3>
&lt;p>vLLM is &lt;strong>the&lt;/strong> reference OSS inference engine. It implements &lt;strong>PagedAttention&lt;/strong> (KV cache paging in the style of virtual memory, avoiding fragmentation), &lt;strong>continuous batching&lt;/strong> (requests join the batch as they arrive, rather than waiting for the next batch), &lt;strong>prefix caching&lt;/strong> (common prefixes, system prompts, do not recompute the KV cache), &lt;strong>LoRA hot-swap&lt;/strong> (&lt;code>--enable-lora&lt;/code> allows loading and unloading adapters without restarting the engine), an &lt;strong>OpenAI-compatible&lt;/strong> API, and &lt;strong>disaggregated prefill/decode&lt;/strong> support since 2025. It covers almost everything from Llama 3 / Mistral / Qwen / DeepSeek. &lt;strong>Gotcha:&lt;/strong> maximum throughput is only reached with &lt;code>--max-num-seqs&lt;/code> and &lt;code>--gpu-memory-utilization&lt;/code> tuned for the specific model and hardware; the defaults are conservative. The initial tuning session pays off: 2-3× throughput. Licence &lt;strong>Apache 2.0&lt;/strong>, originated at UC Berkeley, today maintained by the &lt;strong>vLLM Project / LF AI &amp;amp; Data&lt;/strong> plus a broad community (Red Hat, NVIDIA, AWS, IBM contribute). Serious alternatives in the same bucket: &lt;strong>TGI&lt;/strong> (Apache 2.0, &lt;strong>Hugging Face&lt;/strong>), &lt;strong>SGLang&lt;/strong> (Apache 2.0, &lt;strong>LMSys&lt;/strong>), &lt;strong>TensorRT-LLM&lt;/strong> (Apache 2.0, &lt;strong>NVIDIA&lt;/strong>, requires conversion).&lt;/p>
&lt;h3 id="kserve">KServe&lt;/h3>
&lt;p>KServe is the Kubernetes operator for serving ML models, LLMs included, in a declarative pattern: you define an &lt;code>InferenceService&lt;/code> YAML with the model and predictor (which can be vLLM, TGI, Triton, or a custom container) and KServe handles scheduling onto GPU nodes, autoscaling (scale-to-zero included), traffic splitting for canary, and model registry integration. It is the layer that standardises &amp;ldquo;how a model is deployed on K8s&amp;rdquo; across multiple engines, instead of inventing engine-specific YAML. It supports multi-model with &lt;strong>Inference Graphs&lt;/strong> (chaining preprocessor → model → postprocessor) and integrates with KEDA/Karpenter for GPU pool autoscaling. &lt;strong>Gotcha:&lt;/strong> scale-to-zero on GPU works badly in practice because the warm-up (loading weights into VRAM) takes tens of seconds; &lt;code>minReplicas: 1&lt;/code> is better. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Kubeflow / LF AI &amp;amp; Data&lt;/strong>. Alternatives: &lt;strong>KubeRay&lt;/strong> (Apache 2.0, &lt;strong>Anyscale&lt;/strong>), &lt;strong>llm-d&lt;/strong> (Apache 2.0, &lt;strong>CNCF&lt;/strong>), &lt;strong>KAITO&lt;/strong> (MIT, &lt;strong>Microsoft Azure&lt;/strong>).&lt;/p>
&lt;h3 id="triton-inference-server">Triton Inference Server&lt;/h3>
&lt;p>Triton serves heterogeneous models on a single backend: LLMs (via the vLLM or TensorRT-LLM backend), traditional models (ONNX, TorchScript, TensorFlow), custom models. For systems that mix LLM inference with traditional classifiers, embedding encoders, reranking models, OCR and so on, Triton avoids having N different engines in N pods. It supports ensemble models (chaining models in a single request), dynamic batching, model versioning, model warmup. &lt;strong>Gotcha:&lt;/strong> Triton is flexible but heavy to operate; for systems serving only LLMs, vLLM directly is simpler and more optimised. Triton shines when there is real heterogeneity. Licence &lt;strong>BSD-3-Clause&lt;/strong>, maintained by &lt;strong>NVIDIA&lt;/strong>.&lt;/p>
&lt;h3 id="envoy-ai-gateway">Envoy AI Gateway&lt;/h3>
&lt;p>Envoy AI Gateway is the CNCF ecosystem&amp;rsquo;s &amp;ldquo;LLM-aware API gateway&amp;rdquo;. Built on Envoy Proxy, it adds knowledge of the OpenAI-compatible APIs (chat completions, embeddings, etc.), routing across multiple backends (local vLLM + OpenAI + Anthropic + Bedrock), &lt;strong>token-based rate limiting&lt;/strong> (limits by tokens per minute, not by requests), intelligent retries, fallback between providers, and built-in OTel observability. It is the piece that makes &amp;ldquo;AI Gateway&amp;rdquo; real as an architectural category. &lt;strong>Gotcha:&lt;/strong> integration with authentication (OIDC, JWT) is flexible but requires detailed Envoy configuration; an &amp;ldquo;out of the box&amp;rdquo; AI Gateway with no configuration produces an Envoy that passes everything through. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>CNCF&lt;/strong> since the initial donation from Tetrate. Alternatives: &lt;strong>LiteLLM Proxy&lt;/strong> (MIT, &lt;strong>BerriAI&lt;/strong>), &lt;strong>Portkey&lt;/strong> (MIT, &lt;strong>Portkey AI&lt;/strong>), &lt;strong>Kong AI Gateway&lt;/strong> (Apache 2.0 base + EE, &lt;strong>Kong Inc.&lt;/strong>).&lt;/p>
&lt;h3 id="llamacpp">llama.cpp&lt;/h3>
&lt;p>llama.cpp serves LLMs on CPUs (and Apple Silicon, GPUs via Vulkan/Metal/CUDA) with very aggressive quantisation (GGUF format, down to 2-bit). It is the canonical option for inference on hardware without a dedicated GPU: edge devices, workstations, development machines. It covers everything from small models (Phi-3, Gemma 2B) to Llama 70B on hardware with enough RAM. &lt;strong>Gotcha:&lt;/strong> latency on CPU is orders of magnitude worse than on a dedicated GPU, so it is useful for offline evals, drift checks and local development, not for production serving under real load. Licence &lt;strong>MIT&lt;/strong>, maintained by &lt;strong>Georgi Gerganov&lt;/strong> + community.&lt;/p>
&lt;p>&lt;strong>More options for Deploy:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TensorRT-LLM&lt;/strong> — maximum optimisation on NVIDIA Hopper/Ada. Apache 2.0, NVIDIA.&lt;/li>
&lt;li>&lt;strong>SGLang&lt;/strong> — good for workloads with structured generation and JSON. Apache 2.0, LMSys.&lt;/li>
&lt;li>&lt;strong>TGI&lt;/strong> — a mature alternative, focused on the HuggingFace ecosystem. Apache 2.0, HuggingFace.&lt;/li>
&lt;li>&lt;strong>NVIDIA Dynamo&lt;/strong> — multi-node disaggregated serving. Apache 2.0, NVIDIA.&lt;/li>
&lt;li>&lt;strong>llm-d&lt;/strong> — K8s operator specific to LLMs. Apache 2.0, CNCF.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Decision matrix — Deploy:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If your case is&lt;/th>
&lt;th>Choose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Production serving on NVIDIA H100/A100&lt;/td>
&lt;td>&lt;strong>vLLM&lt;/strong> (the safe default)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Absolute throughput squeezing on Hopper&lt;/td>
&lt;td>&lt;strong>TensorRT-LLM&lt;/strong> + vLLM plugin or standalone&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Edge / local dev without GPU&lt;/td>
&lt;td>&lt;strong>llama.cpp&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-model (LLM + classifiers + encoders)&lt;/td>
&lt;td>&lt;strong>Triton&lt;/strong> with the vLLM backend&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Declarative K8s with autoscaling&lt;/td>
&lt;td>&lt;strong>KServe&lt;/strong> + vLLM as predictor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AI Gateway with token rate limiting&lt;/td>
&lt;td>&lt;strong>Envoy AI Gateway&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-node disaggregated GPU cluster&lt;/td>
&lt;td>&lt;strong>NVIDIA Dynamo&lt;/strong> over vLLM&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="stage-5--observe">Stage 5 — Observe&lt;/h2>
&lt;p>Observe propagates &lt;code>trace_id&lt;/code> end-to-end, emits runtime metrics, runs an LLM judge over sampling and detects drift. Detailed in &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">tracing with AgentSight&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP observability with OTel&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">eBPF + drift&lt;/a>.&lt;/p>
&lt;h3 id="opentelemetry-collector">OpenTelemetry Collector&lt;/h3>
&lt;p>The OTel Collector is the agent that receives traces, metrics and logs in OTel format (or in any other via receivers), processes them (filters, sampling, attribute enrichment, PII redaction), and routes them to one or several backends (Tempo, Jaeger, Prometheus, Loki, Langfuse…). It is &lt;strong>the piece that decouples the apps from the observability backend&lt;/strong>: moving from Tempo to Jaeger means changing the Collector&amp;rsquo;s exporter, not the app. For LLMOps it matters especially because the &lt;strong>OTel GenAI semantic conventions&lt;/strong> spec defines the attributes &lt;code>gen_ai.request.model&lt;/code>, &lt;code>gen_ai.prompt.version&lt;/code>, &lt;code>gen_ai.response.tokens&lt;/code> and so on, which stitch the &lt;code>trace_id&lt;/code> to the system&amp;rsquo;s lineage. &lt;strong>Gotcha:&lt;/strong> the Collector configuration tends to grow; without discipline and periodic review it ends up as an 800-line YAML that nobody understands. Modularising with &lt;code>extensions&lt;/code> helps. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by the &lt;strong>CNCF / OpenTelemetry Project&lt;/strong>.&lt;/p>
&lt;h3 id="tempo-traces--jaeger">Tempo (traces) + Jaeger&lt;/h3>
&lt;p>Grafana Tempo is the distributed trace backend optimised for cost: it uses an object store (S3/MinIO) instead of Elasticsearch, deduplicates by &lt;code>trace_id&lt;/code>, and integrates natively with Grafana for visualisation. For LLMOps, where a real request generates 10-30 spans (gateway, prompt pull, RAG retrieval, prefill, decode N times, scoring), Tempo copes with high volumes at a reasonable cost. &lt;strong>Jaeger&lt;/strong> is the more established CNCF alternative, better for cases under 100k traces per day, worse for native object store. &lt;strong>Gotcha:&lt;/strong> Tempo has no traditional indexing; searches like &amp;ldquo;traces that took more than 5s and touched tenant X&amp;rdquo; require &lt;strong>TraceQL&lt;/strong> + Grafana and are not as fast as Jaeger with Elasticsearch. For immediate ad-hoc diagnosis it is worth keeping a parallel Jaeger with aggressive sampling. Licences &lt;strong>AGPL 3.0&lt;/strong> (Tempo) and &lt;strong>Apache 2.0&lt;/strong> (Jaeger), maintained by &lt;strong>Grafana Labs&lt;/strong> and &lt;strong>CNCF&lt;/strong> respectively.&lt;/p>
&lt;h3 id="prometheus--grafana">Prometheus + Grafana&lt;/h3>
&lt;p>Prometheus is the time-series metrics foundation of the ecosystem. Pull model (it scrapes &lt;code>/metrics&lt;/code> endpoints), PromQL for queries, exporters for everything (Postgres, Kafka, NVIDIA GPU via &lt;code>dcgm-exporter&lt;/code>, vLLM natively). Grafana visualises Prometheus + Tempo + Loki on a single plane. For LLMOps, the critical metrics are &lt;code>gpu_utilization&lt;/code>, &lt;code>kv_cache_usage_pct&lt;/code>, &lt;code>tokens_per_second&lt;/code>, &lt;code>prefill_latency_p95&lt;/code>, &lt;code>decode_latency_p95&lt;/code>, &lt;code>queue_depth&lt;/code>, aggregated by tenant. &lt;strong>Gotcha:&lt;/strong> Prometheus is very good up to about 1M active series; above that it is worth moving to &lt;strong>Thanos&lt;/strong> or &lt;strong>Mimir&lt;/strong> for long retention and horizontal scalability. For the blog&amp;rsquo;s typical LLM cluster (4-8 H100), Prometheus alone is enough. Licences &lt;strong>Apache 2.0&lt;/strong> (Prometheus, &lt;strong>CNCF&lt;/strong>) and &lt;strong>AGPL 3.0&lt;/strong> (Grafana 10+, &lt;strong>Grafana Labs&lt;/strong>).&lt;/p>
&lt;h3 id="langfuse">Langfuse&lt;/h3>
&lt;p>Langfuse is the LLM-specific OSS observability + prompt management tool. It captures spans with LLM semantic conventions (input, output, model, tokens, latency, score, user_id, session_id), visualises them as &lt;strong>conversational traces&lt;/strong> (not just span trees), manages &lt;strong>prompts versioned with a &lt;code>production&lt;/code> label&lt;/strong> and allows &lt;strong>curated datasets + evals&lt;/strong> from the same UI. For serious LLMOps, Langfuse fills the gap that neither Tempo nor Jaeger covers: a tracing UI designed to be LLM-first. &lt;strong>Gotcha:&lt;/strong> Langfuse maintains its own store (Postgres + ClickHouse for high volume); on large clusters the operational side of ClickHouse deserves attention. To get started, Postgres alone copes. Licence &lt;strong>MIT&lt;/strong> for the OSS core, &lt;strong>EE Enterprise Edition&lt;/strong> with additional features (SSO, audit logs, advanced RBAC). Maintained by &lt;strong>Langfuse GmbH&lt;/strong> (Berlin, German). There is Langfuse Cloud (SaaS).&lt;/p>
&lt;h3 id="phoenix-arize-oss">Phoenix Arize OSS&lt;/h3>
&lt;p>Phoenix is Arize AI&amp;rsquo;s OSS for LLM observability + evals, an alternative to Langfuse with a different emphasis: more oriented to evaluation and visual debugging (embedding drift, cluster analysis), less to prompt management. A good pairing with Langfuse when you want a dual approach: Langfuse for &amp;ldquo;production conversational traces&amp;rdquo;, Phoenix for &amp;ldquo;exploratory investigation of model behaviour&amp;rdquo;. &lt;strong>Gotcha:&lt;/strong> Phoenix duplicates functionality with Langfuse and with MLflow; having all three in production multiplies the operational burden. Pick one as the main tool and the others as complements. Licence &lt;strong>Elastic License 2.0&lt;/strong> (not strictly OSI), maintained by &lt;strong>Arize AI&lt;/strong>.&lt;/p>
&lt;h3 id="cilium-tetragon--hubble">Cilium Tetragon + Hubble&lt;/h3>
&lt;p>Tetragon (eBPF runtime security observer) and Hubble (eBPF network observer) are the low-level pieces that give real runtime visibility into the cluster: which processes run in which pods, what syscalls they make, what network connections they open, in real time. For ENS/NIS2 environments that demand &amp;ldquo;prove what ran in production&amp;rdquo;, Tetragon is the irrefutable audit layer: every process execution with its parent, its capabilities, its K8s context. Hubble visualises network flows by pod, namespace, service. &lt;strong>Gotcha:&lt;/strong> the volume of events generated is high; without kernel-side filtering (which Tetragon supports with &lt;code>TracingPolicy&lt;/code>), it saturates the observability plane fast. Discipline in policies. Licence &lt;strong>Apache 2.0&lt;/strong> for both, maintained by &lt;strong>Cilium / CNCF / Isovalent&lt;/strong>.&lt;/p>
&lt;h3 id="evidently-ai">Evidently AI&lt;/h3>
&lt;p>Evidently is the OSS library for &lt;strong>drift detection&lt;/strong>: it compares input and output distributions between two time windows (training vs production, current week vs previous week), applies statistical tests (KS, PSI, Wasserstein, chi-square) and generates HTML reports. For LLMOps it detects when the distribution of prompts changes (new topics, new lengths, new languages) or when the model starts answering shorter / longer / differently. &lt;strong>Gotcha:&lt;/strong> Evidently is oriented to tabular data and embeddings; for raw text it is best combined with an encoder embedder that produces vectors before applying the tests. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Evidently AI&lt;/strong> (a company). Alternatives: &lt;strong>NannyML&lt;/strong> (Apache 2.0, &lt;strong>NannyML BV&lt;/strong>), &lt;strong>Alibi Detect&lt;/strong> (Apache 2.0, &lt;strong>Seldon&lt;/strong>).&lt;/p>
&lt;p>&lt;strong>More options for Observe:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Loki&lt;/strong> — Prometheus-style logs backend for Grafana. AGPL 3.0, Grafana Labs.&lt;/li>
&lt;li>&lt;strong>Pixie&lt;/strong> — auto-instrumented eBPF observability. Apache 2.0, CNCF.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Decision matrix — Observe:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If your case is&lt;/th>
&lt;th>Choose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Minimum viable stack&lt;/td>
&lt;td>&lt;strong>OTel Collector + Tempo + Prometheus + Grafana + Langfuse&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Traces with strong ad-hoc search&lt;/td>
&lt;td>Add &lt;strong>Jaeger&lt;/strong> with aggressive sampling&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ENS / NIS2 compliance runtime audit&lt;/td>
&lt;td>&lt;strong>Tetragon + Hubble&lt;/strong> + mandated retention&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Exploratory investigation of the model&lt;/td>
&lt;td>&lt;strong>Phoenix Arize OSS&lt;/strong> alongside Langfuse&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Statistical drift detection&lt;/td>
&lt;td>&lt;strong>Evidently&lt;/strong> over embeddings + inputs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cluster beyond 1M Prometheus series&lt;/td>
&lt;td>&lt;strong>Mimir&lt;/strong> (Grafana Labs) or &lt;strong>Thanos&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="stage-6--retrain--cross-cutting-pieces">Stage 6 — Retrain + cross-cutting pieces&lt;/h2>
&lt;p>Retrain closes the loop feedback → triage → enriched dataset → new adapter. Prompt versioning and data versioning stitch lineage across stages. Detailed in &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">prompt versioning&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">data versioning&lt;/a>.&lt;/p>
&lt;h3 id="apache-airflow">Apache Airflow&lt;/h3>
&lt;p>Airflow is the most established OSS DAG scheduler. You define workflows as Python code (DAGs), each DAG with tasks (operators) that run according to declared dependencies plus a cron schedule. For retraining: a weekly DAG that extracts feedback from Postgres, triages it with an LLM-as-classifier, enriches the enriched dataset in DVC, launches the fine-tuning job on Kubernetes, runs evals against the golden set, and promotes if it passes the gates. There is a huge ecosystem of operators for everything (S3, Postgres, Kafka, Slack, K8s, Spark…). &lt;strong>Gotcha:&lt;/strong> Airflow 2.x improved a great deal over the chaos of 1.x, but the scheduler is still a component that deserves operational attention (Postgres backend, executor pool, sidecar workers); for simple pipelines it is over-engineering. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by the &lt;strong>ASF&lt;/strong>.&lt;/p>
&lt;h3 id="argo-workflows">Argo Workflows&lt;/h3>
&lt;p>Argo Workflows is the K8s-native equivalent of Airflow: each step is a container, the DAGs are defined as K8s YAML, the executor is Kubernetes itself. For environments where &lt;strong>everything is K8s&lt;/strong>, Argo fits without an extra component to maintain. Long tasks (a six-hour fine-tuning run) execute as Pods that survive control plane failures. It integrates trivially with Kubeflow Pipelines (which is built on top). &lt;strong>Gotcha:&lt;/strong> Argo&amp;rsquo;s YAML syntax is verbose; for complex DAGs, Argo feels less productive than Airflow in Python. Solutions: &lt;strong>Hera&lt;/strong> (Python DSL for Argo, a &lt;strong>DataBricks contribution&lt;/strong>) or &lt;strong>Argo + custom CRDs&lt;/strong>. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>CNCF&lt;/strong>.&lt;/p>
&lt;h3 id="kubeflow-pipelines">Kubeflow Pipelines&lt;/h3>
&lt;p>Kubeflow Pipelines is the layer above Argo Workflows oriented specifically to ML: artefact tracking, experiment tracking, reusable pipeline templates, versioned components. Built on Argo, it adds the ML conceptual model (input artefact, output artefact, metrics) that raw Argo does not have. For cyclical retraining on a K8s cluster, it is the most &amp;ldquo;ML-ready&amp;rdquo; option in the OSS ecosystem. &lt;strong>Gotcha:&lt;/strong> Kubeflow as a full suite is heavy (10+ components); many organisations install only Pipelines + Training Operator + Katib and skip Notebook Server / legacy KFServing. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>CNCF / LF AI &amp;amp; Data&lt;/strong>.&lt;/p>
&lt;h3 id="feast">Feast&lt;/h3>
&lt;p>Feast is the most widely used OSS feature store. It defines &lt;strong>feature views&lt;/strong> over batch sources (BigQuery, Postgres, Parquet) and online sources (Redis, DynamoDB, Postgres with an extension), exposes a consistent API for read-during-training and read-during-inference (point-in-time correctness), and guarantees that the features of the model in production are the same ones it was trained with. For LLMOps where the model needs consistent user / session / context features (latest plan, tenure as a customer, recent tickets), Feast provides the discipline. &lt;strong>Gotcha:&lt;/strong> for many pure LLM systems (a RAG chatbot with no complex features), Feast is over-engineering, Postgres is enough. When there are real features (recommendation, scoring, ranking), Feast shines. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>LF AI &amp;amp; Data&lt;/strong>.&lt;/p>
&lt;h3 id="argilla">Argilla&lt;/h3>
&lt;p>Argilla is the OSS annotation + HiL (human-in-the-loop) platform most aligned with modern LLMOps. It creates annotation projects with templates (classification, ranking, span annotation, RLHF preference, free-form text), connects to HuggingFace datasets, and integrates with Langfuse to import traces from production as cases to annotate. It supports multiple annotators with reconciliation, kappa scoring and quality control. To enrich retrain datasets with cases from the &amp;ldquo;blunt tone&amp;rdquo; cluster of the &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain post&lt;/a>, Argilla is the front end. &lt;strong>Gotcha:&lt;/strong> Argilla requires Elasticsearch for production performance; for small experiments SQLite is enough. Licence &lt;strong>Apache 2.0&lt;/strong>, maintained by &lt;strong>Argilla, Inc.&lt;/strong> (acquired by &lt;strong>Hugging Face&lt;/strong> in 2024). Alternative: &lt;strong>Label Studio&lt;/strong> (Apache 2.0, &lt;strong>HumanSignal&lt;/strong>), more generalist, less LLM-first.&lt;/p>
&lt;h3 id="langfuse-prompts--mlflow-prompt-registry">Langfuse Prompts + MLflow Prompt Registry&lt;/h3>
&lt;p>Langfuse Prompts manages prompts as versioned entities with labels (production, staging, experiment). The client reads the active prompt from Langfuse on the request path (with a local cache of a few seconds) and propagates &lt;code>prompt_id, prompt_version&lt;/code> to the OTel span, exactly as the &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">forensic post&lt;/a> does. MLflow Prompt Registry does the same with a slightly different conceptual model (no labels-as-pointers; it uses stages, like the Models registry). Both are valid; the choice depends on which tracking tool is already in place. &lt;strong>Gotcha (Langfuse):&lt;/strong> labels are mutable, so changing &lt;code>production&lt;/code> points at another version with no explicit audit; prompts are better deployed via PR against the configs repo, not manually in the UI. Licences and governance covered above.&lt;/p>
&lt;p>&lt;strong>More options for Retrain + cross-cutting pieces:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Prefect&lt;/strong> — &amp;ldquo;modern&amp;rdquo; Python DAGs, an alternative to Airflow. Apache 2.0, &lt;strong>Prefect Tech&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Dagster&lt;/strong> — DAGs with a strong focus on data assets. Apache 2.0, &lt;strong>Dagster Labs&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Label Studio&lt;/strong> — generalist annotation. Apache 2.0, &lt;strong>HumanSignal&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>OpenLineage&lt;/strong> — cross-system lineage event standard. Apache 2.0, &lt;strong>LF AI &amp;amp; Data&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>DataHub / Apache Atlas / OpenMetadata&lt;/strong> — catalogue + lineage with a UI. Apache 2.0.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Decision matrix — Retrain + cross-cutting pieces:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If your case is&lt;/th>
&lt;th>Choose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Simple pipelines with a catalogue of operators&lt;/td>
&lt;td>&lt;strong>Airflow&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Everything is K8s, minimal components&lt;/td>
&lt;td>&lt;strong>Argo Workflows&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ML pipelines with artefact tracking&lt;/td>
&lt;td>&lt;strong>Kubeflow Pipelines&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HiL annotation for LLM retraining&lt;/td>
&lt;td>&lt;strong>Argilla&lt;/strong> + Langfuse integration&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Features shared between training and inference&lt;/td>
&lt;td>&lt;strong>Feast&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No complex features, only prompts + LLM&lt;/td>
&lt;td>Skip Feast&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Lightweight prompt registry&lt;/td>
&lt;td>&lt;strong>Langfuse Prompts&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MLflow is already centralised&lt;/td>
&lt;td>&lt;strong>MLflow Prompt Registry&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="master-table-licence-governance-and-enterprise-offering">Master table: licence, governance and enterprise offering&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Licence&lt;/th>
&lt;th>Governance / maintainer&lt;/th>
&lt;th>Commercial EE / SaaS&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>DVC&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Iterative.ai&lt;/td>
&lt;td>DVC Studio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>lakeFS&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Treeverse&lt;/td>
&lt;td>lakeFS Cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MinIO&lt;/strong>&lt;/td>
&lt;td>AGPL v3&lt;/td>
&lt;td>MinIO Inc.&lt;/td>
&lt;td>SUBNET / AIStor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Qdrant&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Qdrant GmbH&lt;/td>
&lt;td>Qdrant Cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>pgvector&lt;/strong>&lt;/td>
&lt;td>PostgreSQL License&lt;/td>
&lt;td>Andrew Kane + community&lt;/td>
&lt;td>— (built-in Postgres clouds)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>PostgreSQL&lt;/strong>&lt;/td>
&lt;td>PostgreSQL License&lt;/td>
&lt;td>PostgreSQL Global Dev Group&lt;/td>
&lt;td>several managed (Crunchy, Neon, Aiven, EDB)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Apache Kafka&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>ASF&lt;/td>
&lt;td>Confluent Cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Debezium&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Red Hat / ASF&lt;/td>
&lt;td>Debezium Server / Confluent Connectors&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Apache Flink&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>ASF&lt;/td>
&lt;td>Ververica Platform, Aiven&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>HF Transformers&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Hugging Face SAS&lt;/td>
&lt;td>HF Inference Endpoints / Enterprise Hub&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>PEFT&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Hugging Face SAS&lt;/td>
&lt;td>— (part of the HF offering)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>bitsandbytes&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>Tim Dettmers + community&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MLflow&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>LF AI &amp;amp; Data&lt;/td>
&lt;td>Databricks MLflow&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Axolotl&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>OpenAccess AI Collective&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Ray (Train)&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Anyscale + community&lt;/td>
&lt;td>Anyscale Platform&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>DeepSpeed&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>Microsoft&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>DeepEval&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Confident AI&lt;/td>
&lt;td>Confident AI SaaS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>RAGAS&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Exploding Gradients&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Promptfoo&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>Promptfoo, Inc.&lt;/td>
&lt;td>Promptfoo Cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NeMo Guardrails&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>NVIDIA&lt;/td>
&lt;td>NeMo Microservices&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Presidio&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>Microsoft&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Phoenix (Arize)&lt;/strong>&lt;/td>
&lt;td>Elastic v2&lt;/td>
&lt;td>Arize AI&lt;/td>
&lt;td>Arize Platform&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>vLLM&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>vLLM Project / LF AI &amp;amp; Data&lt;/td>
&lt;td>several (Red Hat, AWS, IBM, NVIDIA)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TGI&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Hugging Face SAS&lt;/td>
&lt;td>HF Inference Endpoints&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>LMSys + community&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TensorRT-LLM&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>NVIDIA&lt;/td>
&lt;td>NVIDIA AI Enterprise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>llama.cpp&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>Georgi Gerganov + community&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Triton Inference Server&lt;/strong>&lt;/td>
&lt;td>BSD-3&lt;/td>
&lt;td>NVIDIA&lt;/td>
&lt;td>NVIDIA AI Enterprise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>KServe&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>LF AI &amp;amp; Data (Kubeflow)&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Envoy AI Gateway&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>CNCF / Tetrate&lt;/td>
&lt;td>Tetrate Service Bridge&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LiteLLM&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>BerriAI&lt;/td>
&lt;td>LiteLLM Cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>OpenTelemetry&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>CNCF&lt;/td>
&lt;td>several vendors (Honeycomb, Datadog, Grafana)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tempo&lt;/strong>&lt;/td>
&lt;td>AGPL 3.0&lt;/td>
&lt;td>Grafana Labs&lt;/td>
&lt;td>Grafana Cloud Tempo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Jaeger&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>CNCF&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Prometheus&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>CNCF&lt;/td>
&lt;td>Grafana Cloud, AMP, GCP Managed Prom, Azure&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Grafana&lt;/strong>&lt;/td>
&lt;td>AGPL 3.0&lt;/td>
&lt;td>Grafana Labs&lt;/td>
&lt;td>Grafana Cloud, Grafana Enterprise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Loki&lt;/strong>&lt;/td>
&lt;td>AGPL 3.0&lt;/td>
&lt;td>Grafana Labs&lt;/td>
&lt;td>Grafana Cloud Loki&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Langfuse&lt;/strong>&lt;/td>
&lt;td>MIT (core) / EE&lt;/td>
&lt;td>Langfuse GmbH&lt;/td>
&lt;td>Langfuse Cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tetragon&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Cilium / CNCF / Isovalent&lt;/td>
&lt;td>Isovalent Enterprise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Hubble&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Cilium / CNCF&lt;/td>
&lt;td>Isovalent Enterprise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Evidently AI&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Evidently AI&lt;/td>
&lt;td>Evidently Cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Apache Airflow&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>ASF&lt;/td>
&lt;td>Astronomer, MWAA, Cloud Composer&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Argo Workflows&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>CNCF&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kubeflow Pipelines&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>CNCF / LF AI &amp;amp; Data&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Feast&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>LF AI &amp;amp; Data&lt;/td>
&lt;td>Tecton (commercial)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Argilla&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Hugging Face&lt;/td>
&lt;td>HF Hub features&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>OpenLineage&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>LF AI &amp;amp; Data&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>DataHub&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Acryl Data&lt;/td>
&lt;td>Acryl Cloud&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>The pattern to watch for when reading the table:&lt;/strong> &lt;strong>AGPL 3.0&lt;/strong> and &lt;strong>Elastic v2&lt;/strong> are the ones that add the most friction in companies with strict licensing policies (legal asks for a specific review). The &lt;strong>Apache 2.0&lt;/strong> ones pass compliance without argument. Those with an &amp;ldquo;EE Enterprise&amp;rdquo; tier or equivalent hide a decision: the OSS version is functionally complete for production, but team features (SSO, audit, advanced RBAC) live in the commercial version. For ENS customers under a HIGH declaration, the EE features (SSO with corporate SAML/OIDC, immutable audit logs) are usually mandatory, so it is worth knowing the price beforehand.&lt;/p>
&lt;h2 id="when-to-move-from-the-minimum-stack-to-the-full-stack">When to move from the &amp;ldquo;minimum stack&amp;rdquo; to the &amp;ldquo;full stack&amp;rdquo;&lt;/h2>
&lt;p>The whole catalogue can be intimidating. But you do not build all of it on day one. There is a reasonable order that this blog has been validating in posts throughout the series. The &lt;strong>minimum viable stack&lt;/strong> that serves an LLM API with acceptable discipline:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Serving&lt;/strong>: vLLM on Kubernetes plus an Envoy AI Gateway in front.&lt;/li>
&lt;li>&lt;strong>Data&lt;/strong>: Postgres + pgvector (no Qdrant), MinIO for object store, no Kafka.&lt;/li>
&lt;li>&lt;strong>Tune&lt;/strong>: Axolotl + MLflow, no Ray Train.&lt;/li>
&lt;li>&lt;strong>Eval&lt;/strong>: Promptfoo in CI, no RAGAS and no judge in production.&lt;/li>
&lt;li>&lt;strong>Observe&lt;/strong>: OTel Collector + Prometheus + Grafana + Langfuse, no Phoenix and no Tetragon.&lt;/li>
&lt;li>&lt;strong>Retrain&lt;/strong>: feedback in Postgres + crontab scripts, no Airflow.&lt;/li>
&lt;li>&lt;strong>Versioning&lt;/strong>: prompts in Langfuse + datasets in DVC over MinIO, no lakeFS.&lt;/li>
&lt;/ul>
&lt;p>That is &lt;strong>about 8-10 components&lt;/strong> and it serves a reasonable LLM system for a single tenant with moderate traffic. As the system grows, there are identifiable moments where adding each piece pays off:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Trigger&lt;/th>
&lt;th>Component to add&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Multi-tenant with isolated corpora&lt;/td>
&lt;td>&lt;strong>Qdrant&lt;/strong> (collections per tenant, ACL)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The corpus is refreshed often and periodically breaks&lt;/td>
&lt;td>&lt;strong>lakeFS&lt;/strong> (branches with pre-merge hooks)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The embedding pipeline needs streaming&lt;/td>
&lt;td>&lt;strong>Kafka + Debezium + Flink&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Retraining moves from monthly to weekly&lt;/td>
&lt;td>&lt;strong>Airflow&lt;/strong> or &lt;strong>Argo Workflows&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Shared features appear (customer profile, scoring)&lt;/td>
&lt;td>&lt;strong>Feast&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Annotation outgrows informal capacity&lt;/td>
&lt;td>&lt;strong>Argilla&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RAG eval needs specific metrics&lt;/td>
&lt;td>&lt;strong>RAGAS&lt;/strong> + Langfuse datasets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ENS compliance demands runtime audit&lt;/td>
&lt;td>&lt;strong>Tetragon + Hubble&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drift is invisible and shows up late&lt;/td>
&lt;td>&lt;strong>Evidently&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A single stack stops covering multi-model&lt;/td>
&lt;td>&lt;strong>Triton&lt;/strong> or &lt;strong>KServe&lt;/strong> with several predictors&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multiple simultaneous multi-tenant adapters&lt;/td>
&lt;td>&lt;strong>vLLM Production Stack&lt;/strong> + a dedicated Operator&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Each jump adds 1-2 components and is worth the cost only when the trigger is clear. Adding Kafka &amp;ldquo;just in case&amp;rdquo; when the corpus is updated once a month is net negative work.&lt;/p>
&lt;h2 id="what-we-have-not-covered-yet">What we have not covered (yet)&lt;/h2>
&lt;p>Some pieces still deserve a post of their own:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Schema Registry&lt;/strong> for LLM data and prompts (Confluent OSS, Karapace, JSON Schema Registry).&lt;/li>
&lt;li>&lt;strong>Catalogue + lineage&lt;/strong> in depth: DataHub vs Atlas vs OpenMetadata + OpenLineage taken seriously.&lt;/li>
&lt;li>&lt;strong>Federated learning&lt;/strong> over OSS (Flower, FedML) for scenarios where the data is not centralised.&lt;/li>
&lt;li>&lt;strong>OSS MCP Servers&lt;/strong> and their place in the stack as a tools / actions layer.&lt;/li>
&lt;li>&lt;strong>&amp;ldquo;Agentic&amp;rdquo; evals&lt;/strong> specific to multi-step systems with tool use.&lt;/li>
&lt;li>&lt;strong>Upgrade best practices&lt;/strong> for each component (vLLM every 6 weeks, Kafka major every 18 months, etc.).&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">Anatomy of an LLM request in production&lt;/a> — the forensic piece that follows one request through the six stages; this catalogue is the list of tools that appeared along that route.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">The parallel catalogue: OSS vs hyperscalers&lt;/a> — the horizontal cut that shows, for each stage, what each OSS tool does and what its equivalent is on AWS, GCP and Azure.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map of the pipeline to which this catalogue puts concrete OSS names.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">MLOps specific to LLMs in 2026&lt;/a> — general context on LLMOps.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">Data versioning with DVC and lakeFS&lt;/a> — the deep-dive on the two OSS protagonists of the Data stage + cross-cutting piece.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">Prompt versioning with Langfuse and MLflow&lt;/a> — the deep-dive on the Prompt cross-cutting piece.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a> — the Tune stage in real operations.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — the deep-dives on Eval + safety.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard: the sworn translator with a notebook of equivalences&lt;/a> — a specific zoom on the Protect AI tool with its Anonymize + Vault + Deanonymize pattern, the 36 composable scanners and the four deployment modes (lib, FastAPI API, OTel sidecar, AI Gateway plugin).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a> · &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention&lt;/a> · &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving&lt;/a> · &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on K8s&lt;/a> · &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM K8s operators&lt;/a> · &lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">Multi-tenant GPU cluster&lt;/a> — Deploy in all its layers.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight LLM tracing&lt;/a> · &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP observability with OTel&lt;/a> · &lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">eBPF + drift&lt;/a> — Observe from its three angles.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop&lt;/a> — the Retrain stage in detail.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/langfuse-inside-sorting-centre-bottleneck/">Langfuse from the inside: v3 architecture and the 10 backend knobs&lt;/a> — the operational deep-dive on the star observability tool of this catalogue: a six-service architecture, asynchronous ingestion and self-hosted tuning on the example cluster.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/">Isolating AI agents: overview&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">runbook&lt;/a> — the runtime security layer for agents that execute code: bubblewrap/ai-jail on the client and Tetragon (eBPF) on the cluster, with its Kata &lt;code>RuntimeClass&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/kserve-open-inference-protocol-control-plane/">Model chain of trust (1/4): KServe and the Open Inference Protocol&lt;/a> — the deep-dive on the control plane of the Deploy stage: InferenceService, ServingRuntime and the API contract that this catalogue only enumerates.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-where-the-bytes-live-registry-oci-artefacts/">Model chain of trust (2/4): registry, OCI artefacts and distribution&lt;/a> — the missing piece between Tune and Deploy: where the weights live (OCI artefacts with ORAS, Harbor, MLflow) and how they reach the GPU node.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.vllm.ai/">vLLM&lt;/a> · &lt;a href="https://github.com/vllm-project/production-stack">vLLM Production Stack&lt;/a> · &lt;a href="https://huggingface.co/docs/text-generation-inference/">TGI&lt;/a> · &lt;a href="https://docs.sglang.ai/">SGLang&lt;/a> · &lt;a href="https://nvidia.github.io/TensorRT-LLM/">TensorRT-LLM&lt;/a> · &lt;a href="https://github.com/ggerganov/llama.cpp">llama.cpp&lt;/a> — OSS inference engines.&lt;/li>
&lt;li>&lt;a href="https://docs.nvidia.com/deeplearning/triton-inference-server/">Triton Inference Server&lt;/a> · &lt;a href="https://kserve.github.io/website/">KServe&lt;/a> · &lt;a href="https://aigateway.envoyproxy.io/">Envoy AI Gateway&lt;/a> · &lt;a href="https://docs.litellm.ai/">LiteLLM&lt;/a> — orchestration and AI gateway.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/documentation/">Qdrant&lt;/a> · &lt;a href="https://github.com/pgvector/pgvector">pgvector&lt;/a> · &lt;a href="https://milvus.io/docs">Milvus&lt;/a> — vector databases.&lt;/li>
&lt;li>&lt;a href="https://dvc.org/doc">DVC&lt;/a> · &lt;a href="https://docs.lakefs.io/">lakeFS&lt;/a> · &lt;a href="https://min.io/docs/">MinIO&lt;/a> — versioning and object store.&lt;/li>
&lt;li>&lt;a href="https://kafka.apache.org/documentation/">Apache Kafka&lt;/a> · &lt;a href="https://debezium.io/documentation/">Debezium&lt;/a> · &lt;a href="https://flink.apache.org/">Apache Flink&lt;/a> — streams and CDC.&lt;/li>
&lt;li>&lt;a href="https://huggingface.co/docs/transformers">Hugging Face Transformers&lt;/a> · &lt;a href="https://huggingface.co/docs/peft">PEFT&lt;/a> · &lt;a href="https://huggingface.co/docs/bitsandbytes">bitsandbytes&lt;/a> · &lt;a href="https://docs.axolotl.ai/">Axolotl&lt;/a> — fine-tuning.&lt;/li>
&lt;li>&lt;a href="https://mlflow.org/docs/">MLflow&lt;/a> · &lt;a href="https://docs.ray.io/en/latest/train/">Ray Train&lt;/a> · &lt;a href="https://www.kubeflow.org/docs/components/trainer/">Kubeflow Training Operator&lt;/a> — training orchestration.&lt;/li>
&lt;li>&lt;a href="https://docs.confident-ai.com/">DeepEval&lt;/a> · &lt;a href="https://docs.ragas.io/">RAGAS&lt;/a> · &lt;a href="https://promptfoo.dev/docs/">Promptfoo&lt;/a> · &lt;a href="https://docs.nvidia.com/nemo/guardrails/">NeMo Guardrails&lt;/a> · &lt;a href="https://microsoft.github.io/presidio/">Presidio&lt;/a> — evals and guardrails.&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/">OpenTelemetry&lt;/a> · &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OTel GenAI semconv&lt;/a> · &lt;a href="https://grafana.com/docs/tempo/">Tempo&lt;/a> · &lt;a href="https://prometheus.io/docs/">Prometheus&lt;/a> · &lt;a href="https://grafana.com/docs/">Grafana&lt;/a> · &lt;a href="https://grafana.com/docs/loki/">Loki&lt;/a> — observability foundation.&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs">Langfuse&lt;/a> · &lt;a href="https://docs.arize.com/phoenix">Phoenix Arize&lt;/a> · &lt;a href="https://docs.evidentlyai.com/">Evidently AI&lt;/a> — LLM observability and drift.&lt;/li>
&lt;li>&lt;a href="https://docs.cilium.io/">Cilium&lt;/a> · &lt;a href="https://tetragon.io/">Tetragon&lt;/a> · &lt;a href="https://docs.cilium.io/en/stable/observability/hubble/">Hubble&lt;/a> — eBPF runtime.&lt;/li>
&lt;li>&lt;a href="https://airflow.apache.org/docs/">Apache Airflow&lt;/a> · &lt;a href="https://argo-workflows.readthedocs.io/">Argo Workflows&lt;/a> · &lt;a href="https://www.kubeflow.org/docs/components/pipelines/">Kubeflow Pipelines&lt;/a> · &lt;a href="https://docs.feast.dev/">Feast&lt;/a> · &lt;a href="https://docs.argilla.io/">Argilla&lt;/a> — orchestration + retrain + annotation.&lt;/li>
&lt;/ul></description></item><item><title>Data versioning for LLMOps: DVC, lakeFS and the reproducible golden dataset challenge</title><link>https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/</link><pubDate>Fri, 22 May 2026 11:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The Data stage of the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a> has a silent link on which everything else depends: &lt;strong>versioning the datasets&lt;/strong> with the same discipline the code is versioned with. It is not optional. An LLM system in production consumes at least four distinct types of dataset (training/fine-tuning, RAG corpus, golden eval set, enriched dataset from the Retrain loop) and each has its own demands. Git solves code but fails on data for two technical reasons (size and a useless binary diff) and one operational one (it does not propagate lineage as far as the weights bucket of the trained model). The two dominant OSS tools, &lt;strong>DVC&lt;/strong> and &lt;strong>lakeFS&lt;/strong>, &lt;strong>unified in November 2025&lt;/strong> under a single organisation with a roadmap oriented towards LLM training and RAG datalakes; they are still complementary projects (file-level vs whole-bucket branching) but now under common governance. The productive pattern the market has consolidated on: identify each artefact with an immutable &lt;code>(dataset_id, version)&lt;/code>, propagate the pair as far as the experiment tracking (MLflow / W&amp;amp;B), version the dataset&amp;rsquo;s schema too (not just its content), apply a strict holdout to the golden eval set so you are not measuring memorisation, and maintain bidirectional traceability &lt;code>dataset_version ↔ model_version ↔ deployment ↔ trace_id&lt;/code>. Without this, the promise of &amp;ldquo;we can audit which model answered what&amp;rdquo; collapses at the first serious incident.&lt;/p>
&lt;h2 id="you-are-here-data-with-a-cross-cutting-effect-on-tune-eval-and-retrain">You are here: Data (with a cross-cutting effect on Tune, Eval and Retrain)&lt;/h2>
&lt;p>This post goes into the detail of the versioning link inside stage &lt;strong>1 · Data&lt;/strong>. Versioning belongs operationally to Data, but the artefacts it produces travel to Tune (training set), Eval (golden set) and Retrain (enriched dataset). That is why the diagram marks Data as active and shows a cross-cutting band indicating end-to-end lineage.&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="you are here: Data with cross-cutting lineage">
&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">You are here: DATA · dataset versioning with lineage to the production trace&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">Dataset lineage: training set → Tune · golden set → Eval · enriched set → Retrain (back to Data)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-master-analogy-batch-traceability-in-a-serious-factory">The master analogy: batch traceability in a serious factory&lt;/h2>
&lt;p>A serious pharmaceutical factory does not produce without &lt;strong>batch traceability&lt;/strong>. Every box of pills carries a printed batch number; that batch is associated with manufacturing dates, with the specific batches of each raw material used, with the quality tests it passed, and with the technicians who signed off each step. If a patient reports an adverse effect, the factory can rewind within hours: this package → this batch → these raw materials → this shift → this production line → this quality control result. Without that chain, the incident is a permanent mystery.&lt;/p>
&lt;p>A serious LLM system works the same way. The &amp;ldquo;package&amp;rdquo; is the answer a user saw in production. The &amp;ldquo;batch&amp;rdquo; is the combination of model, adapter, prompt, context and configuration that generated it. And the &amp;ldquo;raw materials&amp;rdquo; are the datasets: the training set the base model was trained on, the adapter&amp;rsquo;s fine-tuning dataset, the RAG corpus that feeds retrieval, the golden eval set that validates promotion. If a customer says &lt;em>&amp;ldquo;what data was the model trained on that answered X to my question Y on 14 March?&amp;rdquo;&lt;/em>, without batch traceability the answer is &lt;em>&amp;ldquo;we do not know&amp;rdquo;&lt;/em>. And that, with a customer under compliance pressure, kills the contract.&lt;/p>
&lt;p>Git versions the recipe (the code). Data versioning versions the ingredients. Without both, there is no auditable factory.&lt;/p>
&lt;h2 id="the-four-artefacts-worth-versioning-with-differentiated-demands">The four artefacts worth versioning (with differentiated demands)&lt;/h2>
&lt;p>Not all datasets are versioned the same way or at the same frequency. The typical LLM system in production handles four artefacts worth governing separately.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Artefact&lt;/th>
&lt;th>What it is&lt;/th>
&lt;th>Typical size&lt;/th>
&lt;th>New-version frequency&lt;/th>
&lt;th>Who consumes it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Training / fine-tuning dataset&lt;/strong>&lt;/td>
&lt;td>Input/output pairs (or conversations) that train the adapter or the model.&lt;/td>
&lt;td>10⁴ – 10⁷ examples · 1 – 100 GB&lt;/td>
&lt;td>Per Tune experiment&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>Indexed documents that feed retrieval.&lt;/td>
&lt;td>10⁵ – 10⁹ chunks · 10 GB – 10 TB&lt;/td>
&lt;td>Almost continuous (streaming ingest)&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>Curated examples with an expected answer to measure quality.&lt;/td>
&lt;td>10² – 10⁴ examples · MB&lt;/td>
&lt;td>Per product release&lt;/td>
&lt;td>Eval gates in CI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Enriched retrain dataset&lt;/strong>&lt;/td>
&lt;td>Cases where the system failed + the human correction.&lt;/td>
&lt;td>Hundreds to thousands per quarter&lt;/td>
&lt;td>Per &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain&lt;/a> cycle&lt;/td>
&lt;td>Next Tune&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>All four have common requirements (immutable identity, lineage, schema) and relevant differences:&lt;/p>
&lt;ul>
&lt;li>The &lt;strong>training set&lt;/strong> tends to be large, stable per experiment, and the cost of an error is a lost experiment (expensive but bounded).&lt;/li>
&lt;li>The &lt;strong>RAG corpus&lt;/strong> is enormous, in continuous change, and its versioning is handled through periodic snapshots of the index (not of the raw text). Usually lakeFS or bucket branches; DVC is not the best fit.&lt;/li>
&lt;li>The golden eval set is small but critical: errors here contaminate the whole promotion chain. Here the rigidity of the versioning matters more than anywhere else.&lt;/li>
&lt;li>The &lt;strong>enriched retrain dataset&lt;/strong> is incremental by nature: each Retrain cycle contributes a delta over the previous one. The new version does not overwrite; it inherits and adds.&lt;/li>
&lt;/ul>
&lt;p>Confusing them, treating the RAG corpus as if it were the training set, or the golden eval as if it were just another dataset, is the source of half the operational problems in data versioning.&lt;/p>
&lt;h2 id="why-git-is-not-enough">Why Git is not enough&lt;/h2>
&lt;p>The obvious question: if Git already solves code, why does it not solve data too? Three reasons, two technical and one operational.&lt;/p>
&lt;p>&lt;strong>Reason 1: size.&lt;/strong> A Git repository with a 50 GB dataset becomes unmanageable. &lt;code>git clone&lt;/code> pulls the entire history; &lt;code>git status&lt;/code> walks every file; the pack file in &lt;code>.git/objects&lt;/code> bloats to up to twice the size of the dataset. Git LFS solves the first part (the binary leaves the pack) but introduces its own complexity without addressing the other two reasons.&lt;/p>
&lt;p>&lt;strong>Reason 2: useless binary diff.&lt;/strong> Git assumes text diffs are useful. When a column changes in an 8 GB parquet, the diff is opaque, because the file is binary, compressed and columnar. You cannot do code review on a dataset change the way you do on a function change. You need a semantic diff: how many rows changed, which columns changed, which distribution moved. No native Git gives you that.&lt;/p>
&lt;p>&lt;strong>Reason 3: lineage that crosses repository boundaries.&lt;/strong> This is the most important and the most subtle. The training dataset lives in a bucket. The trainer code lives in a Git repo. The trained model is published to a model registry. Inference in production generates traces in an observability system. Connecting &lt;code>dataset_v3 → adapter_v7 → deployment_d2 → trace t_x9&lt;/code> requires propagating identifiers &lt;strong>across four different systems&lt;/strong>, not inside one repo. Git has no opinion on this.&lt;/p>
&lt;p>Data versioning tools (DVC, lakeFS, Pachyderm, Quilt) exist because they solve the three problems at once: they hang the data outside the Git repo, they offer some form of semantic diff, and they expose stable identities that can be propagated towards experiment tracking and the model registry.&lt;/p>
&lt;h2 id="dvc-vs-lakefs-before-the-unification">DVC vs lakeFS before the unification&lt;/h2>
&lt;p>Until November 2025, the two dominant OSS tools coexisted as complementary approaches.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Axis&lt;/th>
&lt;th>DVC&lt;/th>
&lt;th>lakeFS&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Mental model&lt;/td>
&lt;td>&amp;ldquo;Git for data&amp;rdquo;&lt;/td>
&lt;td>&amp;ldquo;Branching for the data lake&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Granularity&lt;/td>
&lt;td>Individual file&lt;/td>
&lt;td>Whole bucket (with per-branch namespacing)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Storage&lt;/td>
&lt;td>Remote-agnostic (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 over the bucket&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Diff&lt;/td>
&lt;td>File hash + external metadata&lt;/td>
&lt;td>Object-level diff + commit log&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Strong cases&lt;/td>
&lt;td>Discrete training datasets, model files, reproducible pipelines&lt;/td>
&lt;td>Large RAG corpora, branching a shared data lake, parallel experiments without duplicating data&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Git integration&lt;/td>
&lt;td>Deep (the &lt;code>.dvc&lt;/code> files are committed to Git)&lt;/td>
&lt;td>Tangential (lakeFS lives alongside)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Who operates it&lt;/td>
&lt;td>MLE team&lt;/td>
&lt;td>Data engineering team&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In practice, many teams used them at the same time: DVC for the discrete datasets that fed an experiment (they fit in a Git repo thanks to the indirection of the &lt;code>.dvc&lt;/code> pointers), and lakeFS for the large RAG corpus bucket they wanted to branch without duplicating terabytes.&lt;/p>
&lt;h2 id="what-changed-with-the-november-2025-acquisition">What changed with the November 2025 acquisition&lt;/h2>
&lt;p>&lt;a href="https://medium.com/the-modern-scientist/reproducible-ai-versioning-models-prompts-and-data-96dd0337af65">lakeFS acquired DVC&lt;/a> in November 2025. The operational consequence as of May 2026 is modest but relevant:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>There is (as yet) no technical merger of the projects.&lt;/strong> DVC is still DVC and lakeFS is still lakeFS. The current CLIs, formats and workflows have not changed.&lt;/li>
&lt;li>&lt;strong>An explicit combined roadmap towards LLM training and RAG datalakes.&lt;/strong> The merged organisation has stated specific priorities: consistent branching between the dataset and the trained model, native integrations with MLflow / W&amp;amp;B / Langfuse, support for the typical LLM formats (jsonl, parquet with embedded tokenisation), and branch-aware vector indexing.&lt;/li>
&lt;li>&lt;strong>Convergence expected in 2026-2027.&lt;/strong> The market anticipates a single registry with two operating modes (file-level + bucket-branching) under a unified CLI. As of today, teams still combine both.&lt;/li>
&lt;/ul>
&lt;p>The practical reading for 2026: &lt;strong>adopt DVC for discrete training/eval datasets and lakeFS for the RAG corpus&lt;/strong>, but design the lineage so that a future unified registry can absorb both without re-versioning everything. Concretely: use stable identifiers (&lt;code>dataset_id&lt;/code>, &lt;code>version&lt;/code>, &lt;code>commit_hash&lt;/code>) that can be propagated regardless of the tool.&lt;/p>
&lt;h2 id="the-operational-pattern-four-hop-lineage">The operational pattern: four-hop lineage&lt;/h2>
&lt;p>Once you accept that datasets have to be versioned, the question is not &amp;ldquo;which tool&amp;rdquo; but &amp;ldquo;which chain of identifiers connects production to the source data&amp;rdquo;. The pattern the market has consolidated on has four hops:&lt;/p>
&lt;pre tabindex="0">&lt;code>(dataset_id, dataset_version)
│ versioned in DVC or lakeFS
▼
(model_id, model_version)
│ registered in MLflow / W&amp;amp;B with the dataset as input
▼
(deployment_id, prompt_version)
│ registered in the model registry + prompt registry
▼
(trace_id)
│ emitted by the inference engine with OTel
▼
answer visible to the user
&lt;/code>&lt;/pre>&lt;p>Each arrow is a metadata write that crosses the boundary between two systems. If a single arrow is missing, the lineage breaks and the promise of auditability evaporates.&lt;/p>
&lt;p>A concrete example of the flow, using 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"># Data stage: version the 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"># uploads the binary to the 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"># Tune stage: train while recording 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"># The run records: 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"># Eval stage: validate while recording 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"># Deploy stage: the deployment inherits the dataset + golden ids&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Every trace in Observe carries model_version + prompt_version&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># which rewind back to dataset_version&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The equivalent version with lakeFS over the 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 for the new corpus embeddings&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"># Index the corpus on that 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"># Validate before merging to 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"># If it passes, merge (changes the corpus that serves production)&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>The virtue of the second flow: while the new corpus is being validated, the production system keeps serving from &lt;code>main&lt;/code> with no interference. The parallel branch works as a real staging area over the whole bucket.&lt;/p>
&lt;h2 id="schema-contracts-data-versioning-without-them-is-an-illusion">Schema contracts: data versioning without them is an illusion&lt;/h2>
&lt;p>Versioning a dataset&amp;rsquo;s content without versioning its &lt;strong>schema&lt;/strong> is a frequent mistake. The problem: a versioned dataset with an implicit schema still breaks silently when a producer (the ingestion team, the annotation team, an ad-hoc script) changes a field.&lt;/p>
&lt;p>A concrete case: a customer support golden eval set, 1000 examples, an &lt;code>expected_output&lt;/code> field originally &lt;code>string&lt;/code>. Someone decides they need to capture several valid answers and changes the field to &lt;code>list[string]&lt;/code>. The eval loader accepts both formats by chance (Python is lax) but the downstream LLM judge receives a different object. The eval keeps passing but now it measures something else.&lt;/p>
&lt;p>Productive pattern: the dataset is versioned with DVC/lakeFS and its schema is versioned with a &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">Schema Registry&lt;/a> (Confluent or Apicurio) or, in less mature systems, with a JSON Schema embedded alongside the dataset. CI blocks any PR that breaks the contract without a version bump.&lt;/p>
&lt;p>A minimal schema for a golden eval entry (illustrative):&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>Operational rules:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Explicit forward/backward compatibility&lt;/strong>: adding an optional field is backward-compatible; removing a required one is breaking. The policy is enforced with a compatibility check in CI.&lt;/li>
&lt;li>&lt;strong>The schema version embedded&lt;/strong> in every row of the dataset (a &lt;code>_schema_version&lt;/code> field). The loader validates that the version matches what the consuming code expects.&lt;/li>
&lt;li>&lt;strong>The schema registry as the single source of truth&lt;/strong>, not as an optional copy of the JSON Schema in four repos.&lt;/li>
&lt;/ul>
&lt;p>Without this level of discipline, &amp;ldquo;we have data versioning&amp;rdquo; means &amp;ldquo;we store the bytes but we do not control what they mean&amp;rdquo;.&lt;/p>
&lt;h2 id="golden-eval-set-the-most-critical-version">Golden eval set: the most critical version&lt;/h2>
&lt;p>Of the four artefacts, the &lt;strong>golden eval set&lt;/strong> is the one that demands the most rigour. A failure here contaminates the whole promotion chain: if the eval lies, the gates approve models that should not pass.&lt;/p>
&lt;p>Three extra disciplines over the golden set:&lt;/p>
&lt;p>&lt;strong>Annotation with measured quality.&lt;/strong> Each example is labelled by a human, and a percentage (10-20 %) is annotated by two independent people. The &lt;strong>inter-annotator agreement&lt;/strong> (Cohen&amp;rsquo;s kappa or pairwise F1) is measured and published; a golden set with kappa &amp;lt; 0.7 is measuring human noise, not model behaviour. Argilla and Label Studio provide the mechanics; what matters is the discipline, not the tool.&lt;/p>
&lt;p>&lt;strong>A strict holdout against contamination.&lt;/strong> The golden set must &lt;strong>never&lt;/strong> enter the training set. The concrete mechanism: hash each golden set &lt;code>input&lt;/code> (sha256 normalised by lowercasing + stripping trivial punctuation) → check in CI against all the training set hashes. If there is an intersection, CI blocks until it is resolved. Without this check, the model passes the eval by memorisation, not by capability. The consequence in production is disastrous: the &amp;ldquo;validated&amp;rdquo; model fails on cases analogous to the golden set that it had not memorised.&lt;/p>
&lt;p>&lt;strong>Additive versioning, never destructive.&lt;/strong> When the golden set grows (each retrain cycle adds cases), &lt;code>golden_v3 = golden_v2 ∪ new_examples&lt;/code>. Never &lt;code>golden_v3 = a different new set&lt;/code>. Only that way can you compare two models trained months apart over the same base + the new delta. If you rewrite the golden set, you cannot tell whether the March model was worse than the May one or whether you were simply measuring different things.&lt;/p>
&lt;p>Summary table of the discipline per artefact:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Practice&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>Immutable versioning&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes (snapshots)&lt;/td>
&lt;td>&lt;strong>Yes, critical&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Schema with a contract&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Recommended&lt;/td>
&lt;td>&lt;strong>Yes, critical&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Double annotation&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Not applicable&lt;/td>
&lt;td>&lt;strong>Yes (10-20 %)&lt;/strong>&lt;/td>
&lt;td>Yes (10-20 %)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Holdout vs other datasets&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>&lt;strong>Yes, hash check&lt;/strong>&lt;/td>
&lt;td>Yes (vs golden)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drift check vs previous version&lt;/td>
&lt;td>Recommended&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Recommended&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Lineage to deployment&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="promotion-gates-the-dataset-is-promoted-like-the-model">Promotion gates: the dataset is promoted like the model&lt;/h2>
&lt;p>A candidate dataset (a freshly enriched &lt;code>golden_v13&lt;/code>, an &lt;code>enriched_retrain_2026_q2&lt;/code> resulting from the Retrain cycle) does not enter production just by being in the bucket. It goes through gates equivalent to the model&amp;rsquo;s or the prompt&amp;rsquo;s:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Schema validation&lt;/strong> — the contract is met. Blocked in CI if not.&lt;/li>
&lt;li>&lt;strong>Quality validation&lt;/strong> — a random 5-10 % sample reviewed by a human with a quality score ≥ 4/5. Blocked if the sample fails.&lt;/li>
&lt;li>&lt;strong>Holdout segregation check&lt;/strong> — for golden sets and enriched datasets, a hash check against all the other active datasets. Blocked if there is overlap.&lt;/li>
&lt;li>&lt;strong>Drift check vs previous version&lt;/strong> — a KS test over the distribution of the input embeddings, or simpler metrics (mean length, segment distribution, the ratio of each label). A warning if the drift is high with no documented cause; a block if it is very high.&lt;/li>
&lt;li>&lt;strong>Lineage check&lt;/strong> — the dataset explicitly declares which version it inherits from and what changed. Without that metadata, it does not go in.&lt;/li>
&lt;/ol>
&lt;p>Only when all five gates pass is the dataset tagged &lt;code>production-ready&lt;/code> and the downstream pipelines that depend on it unblocked (the next Tune, the next product release, the next eval cycle).&lt;/p>
&lt;h2 id="the-on-premise-stack-applied">The on-premise stack applied&lt;/h2>
&lt;p>On a generic infrastructure with an &lt;strong>RTX 4090&lt;/strong> (24 GB VRAM, a development / small-batch profile) and a &lt;strong>4×H100 SXM cluster&lt;/strong> (80 GB VRAM each, NVLink, training and production inference), data versioning fits without a dedicated GPU for the versioning itself, since versioning lives on CPU + storage, but it does touch the GPU for the drift checks that require embeddings.&lt;/p>
&lt;p>Typical topology:&lt;/p>
&lt;pre tabindex="0">&lt;code>┌────────────────────────────────────────────────────────────┐
│ Object store (MinIO or 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>Operational notes:&lt;/p>
&lt;ul>
&lt;li>The &lt;strong>object store&lt;/strong> (MinIO or Ceph) serves both as the DVC remote and as lakeFS storage. One storage plane, two views.&lt;/li>
&lt;li>The &lt;strong>schema checks&lt;/strong> and &lt;strong>holdout hashing&lt;/strong> are fast CPU-bound tasks; the CI runner executes them without a GPU.&lt;/li>
&lt;li>The &lt;strong>embedding drift check&lt;/strong> needs an encoder; the RTX 4090 handles this without touching the production cluster. A small encoder (BGE-small, E5-small, ~100M parameters) processes 10⁴ examples in a few minutes.&lt;/li>
&lt;li>The &lt;strong>H100 cluster&lt;/strong> stays free for training and serving, with no contamination from versioning jobs.&lt;/li>
&lt;/ul>
&lt;h3 id="when-do-you-not-need-dvclakefs">When do you NOT need DVC/lakeFS?&lt;/h3>
&lt;p>There is an opposite position defended with figures in the &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning post&lt;/a>: for small systems with a single team, datasets &amp;lt; 1 GB and a handful of adapters, &lt;strong>Postgres + pgvector + an S3 bucket + hashed filenames&lt;/strong> are enough. The operational complexity of DVC/lakeFS does not pay for itself.&lt;/p>
&lt;p>The dividing line is reasonable:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You do not need DVC/lakeFS&lt;/strong>: a single team, small datasets, few adapters, no multiple products sharing data.&lt;/li>
&lt;li>&lt;strong>You do need it&lt;/strong>: multiple teams, datasets &amp;gt; 10 GB, several products sharing a golden eval set, external compliance demanding batch traceability, or an institutionalised quarterly retrain cycle.&lt;/li>
&lt;/ul>
&lt;p>Adopting DVC + lakeFS before you need them is overhead. Adopting them six months late is losing six months of lineage irrecoverably.&lt;/p>
&lt;h2 id="seven-pitfalls-that-turn-data-versioning-into-theatre">Seven pitfalls that turn data versioning into theatre&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Versioning the data but not the schemas.&lt;/strong> The content is versioned, the contract changes silently, the system breaks without the versioning capturing it. A Schema Registry is not optional; it is half the problem.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>The same S3 path overwritten.&lt;/strong> &amp;ldquo;Upload &lt;code>training.jsonl&lt;/code> to the bucket&amp;rdquo; and the next experiment rewrites the file. S3 versioning (if it is enabled) saves your skin, but without an immutable identifier propagated to MLflow you cannot rewind. The correct pattern: &lt;code>training_v3.jsonl&lt;/code> or &lt;code>training/2026q2/&amp;lt;sha&amp;gt;.jsonl&lt;/code>, never the same name.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>A golden eval set without a strict holdout.&lt;/strong> Without a hash check against training, the model memorises the eval and passes without having learned. It is the LLM equivalent of an exam the teacher announces in advance: everyone passes, nothing has been measured.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Not recording dataset → model lineage.&lt;/strong> When an incident requires knowing what data a certain model was trained on, the correct answer is a query to MLflow / W&amp;amp;B. If the answer is &amp;ldquo;let&amp;rsquo;s ask whoever trained it&amp;rdquo; (assuming they are still on the team), the lineage does not exist.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>DVC added six months late.&lt;/strong> Adopting versioning in month 1 = a nuisance. Adopting it in month 6 = an irrecoverable loss of six months of datasets that can no longer be reconstructed. The curse of &amp;ldquo;we&amp;rsquo;ll add it later&amp;rdquo;.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>lakeFS with branches that never get merged.&lt;/strong> Parallel branches over the corpus are useful for experimenting; kept indefinitely without merging, operations turn into a graveyard of half-updated branches. Explicit policy: merge or destroy within N weeks.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Schema validation only in production.&lt;/strong> The contract is validated when the dataset is already in production and the model already trained. By then, the incident has already happened. The validation has to be &lt;strong>in CI&lt;/strong>, before the merge, over the delta the PR introduces.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="a-datasets-cycle-on-one-screen">A dataset&amp;rsquo;s cycle on one screen&lt;/h2>
&lt;pre tabindex="0">&lt;code>┌─────────────────────────────────────────────────────────────┐
│ Producer (ingest / annotation / retrain loop) │
└────────────────┬────────────────────────────────────────────┘
│
▼ (commit a candidate version)
┌─────────────────────────┐
│ CI gates │
│ - Schema validation │
│ - Quality sampled │
│ - Holdout hash check │ ── fails → PR blocked
│ - Drift vs previous │
│ - Lineage declared │
└────────────┬────────────┘
│ passes
▼
┌─────────────────────────┐
│ DVC tag or lakeFS commit│
│ + MLflow registry │ ← immutable version
│ + Schema Registry │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Downstream pipeline │
│ Tune / Eval / Deploy │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Production trace │
│ → rewinds to dataset │
└─────────────────────────┘
&lt;/code>&lt;/pre>&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;p>At top level, this post leaves out:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Vector store versioning&lt;/strong> proper: an embedding index is not versioned like a raw dataset because it depends on the embedding model. Changing the embedder rewrites the whole index. It is a different animal and deserves separate treatment (recall, ANN parameters, index branching vs full re-embedding).&lt;/li>
&lt;li>&lt;strong>Standardised lineage tooling&lt;/strong> (OpenLineage, Marquez): how to emit and consume lineage events interoperably between systems.&lt;/li>
&lt;li>&lt;strong>Data quality frameworks&lt;/strong> (Great Expectations, Soda, Deequ): how to write suites of &amp;ldquo;expectations&amp;rdquo; over a dataset and enforce them on every version.&lt;/li>
&lt;li>&lt;strong>Privacy-preserving versioning&lt;/strong>: federated learning without centralising the dataset, differential privacy applied to the version that gets distributed.&lt;/li>
&lt;li>&lt;strong>Contamination between third-party golden sets&lt;/strong> (HumanEval, MMLU, etc.) and the training datasets of open models: the problem of &amp;ldquo;the model passes HumanEval because HumanEval is in its pretraining&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>Each one is worth a post of its own when the field justifies it.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map where this piece fits, the Data section.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop&lt;/a> — how the enriched dataset produced by Retrain returns to Data; this post details how to version it properly.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">Prompt versioning with Langfuse and MLflow&lt;/a> — the other cross-cutting piece of the lineage; the &lt;code>prompt_version&lt;/code> travels alongside the &lt;code>dataset_version&lt;/code> in every trace.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a> — argues for a minimalist stack (Postgres + pgvector + S3) without DVC/lakeFS for small systems; this post explains when you cross the line to the other side.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs: the layer after tracing&lt;/a> — the main consumer of the golden eval set.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">LLM-specific MLOps in 2026&lt;/a> — market context for the complete LLMOps stack.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">PostgreSQL + Qdrant for ingestion&lt;/a> — how the ingestion that precedes versioning is materialised.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/">ISO/IEC 42001: the AI system&amp;rsquo;s operations manual&lt;/a> — the versioning discipline described here materialises the five controls of Annex A.7 (data quality, acquisition, provenance, preparation) directly, with no additional work.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/eu-ai-act-technical-dossier-on-premise-llm-architecture/">EU AI Act: the technical file article by article&lt;/a> — Art. 10 (data and data governance) requires the four datasets versioned + bias analysis + lineage + representativeness. This post covers the technical mechanics; that one covers the legal obligations and the auditable checklist.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://dvc.org/doc">DVC documentation&lt;/a> — versioning workflows, pipelines and remotes.&lt;/li>
&lt;li>&lt;a href="https://docs.lakefs.io/">lakeFS documentation&lt;/a> — branching, merging and commits over the bucket.&lt;/li>
&lt;li>&lt;a href="https://medium.com/the-modern-scientist/reproducible-ai-versioning-models-prompts-and-data-96dd0337af65">lakeFS acquires DVC, November 2025&lt;/a> — announcement and combined roadmap.&lt;/li>
&lt;li>&lt;a href="https://docs.confluent.io/platform/current/schema-registry/index.html">Confluent Schema Registry&lt;/a> and &lt;a href="https://www.apicur.io/registry/">Apicurio&lt;/a> — schema contracts for streaming data.&lt;/li>
&lt;li>&lt;a href="https://openlineage.io/">OpenLineage&lt;/a> and &lt;a href="https://marquezproject.ai/">Marquez&lt;/a> — open standard for lineage events.&lt;/li>
&lt;li>&lt;a href="https://greatexpectations.io/">Great Expectations&lt;/a> — data quality expectations in CI.&lt;/li>
&lt;li>&lt;a href="https://mlflow.org/docs/latest/tracking.html">MLflow Tracking&lt;/a> — input datasets as first-class artefacts since MLflow 2.4.&lt;/li>
&lt;li>&lt;a href="https://www.pachyderm.com/">Pachyderm&lt;/a> and &lt;a href="https://quiltdata.com/">Quilt&lt;/a> — historical alternatives to DVC/lakeFS.&lt;/li>
&lt;li>On eval set contamination: &lt;em>&amp;ldquo;Stop Uploading Test Data in Plain Text&amp;rdquo;&lt;/em> (Magar &amp;amp; Schwartz, 2022) and later work on detecting contamination in pretraining corpora.&lt;/li>
&lt;/ul></description></item></channel></rss>