<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Pipeline on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/pipeline/</link><description>Recent content in Pipeline on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 23 May 2026 07:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/pipeline/index.xml" rel="self" type="application/rss+xml"/><item><title>Anatomy of an LLM request in production, May 2026: a tour of the six stages following a single request</title><link>https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/</link><pubDate>Fri, 22 May 2026 16:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Across several series the blog has laid out the pieces that hold up an LLM system in production: the &lt;strong>Data stage&lt;/strong> (&lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">dataset versioning&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">ingestion and vector stores&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka&lt;/a>), the &lt;strong>Tune stage&lt;/strong> (&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning&lt;/a>), the &lt;strong>Eval stage&lt;/strong> (&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">evals as the layer after tracing&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails and safety&lt;/a>), the &lt;strong>Deploy stage&lt;/strong> (&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/gpu-cluster-as-multi-tenant-platform/">multi-tenant GPU cluster&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM operators on K8s&lt;/a>), the &lt;strong>Observe stage&lt;/strong> (&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&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">eBPF + drift&lt;/a>), the &lt;strong>Retrain stage&lt;/strong> (&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">closing the feedback → dataset → adapter loop&lt;/a>), and the cross-cutting components (&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>). What is missing is &lt;strong>joining them up&lt;/strong>: seeing a single request cross every piece in order, in one coherent story. That is what this post does. We take a specific request from a multi-tenant support chatbot, rewind it back to the data that trained the adapter serving it today, follow it forward through serving, watch it reach the feedback store when the user marks a thumbs-down, and leave it as the seed of the next quarterly retrain cycle. The route works as a mental map and as an integrator&amp;rsquo;s guide: the system does not hold up if a single one of the seven pieces (six stages + two cross-cutting) is broken or absent. The practical lesson of the tour is not a new one. It is that &lt;strong>everything is connected&lt;/strong>, that local measurements lie when taken in isolation, and that the real cost of not operating one stage well is paid by another stage further down the line.&lt;/p>
&lt;h2 id="you-are-here-every-stage-at-once">You are here: every stage at once&lt;/h2>
&lt;p>Unlike previous posts, where the mini-map marked a single active box, this one covers &lt;strong>the whole&lt;/strong> pipeline. It is the only post on the blog that activates the six stages and the two cross-cutting components simultaneously, because we follow a real request that crosses all of them.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 180" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="tour of every stage and cross-cutting component of the LLMOps pipeline">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:2.4}.cross{fill:#ffe9d6;stroke-width:1.6;stroke:#c66;rx:6}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#444}.tiny{font:600 10px sans-serif;fill:#333}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#trm)}.cyc{stroke:#c66;stroke-width:2;fill:none;stroke-dasharray:4 2;marker-end:url(#trm)}.trace{stroke:#1a73e8;stroke-width:2.4;fill:none}&lt;/style>
&lt;defs>&lt;marker id="trm" 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">Full tour: one request crosses the 6 stages and the 2 cross-cutting components&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 active"/>&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 active"/>&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 active"/>&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 active"/>&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 active"/>&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="357" height="25" class="cross"/>
&lt;text x="208" y="115" text-anchor="middle" class="sm">Prompt versioning (Langfuse / MLflow Prompts)&lt;/text>
&lt;rect x="405" y="98" width="360" height="25" class="cross"/>
&lt;text x="585" y="115" text-anchor="middle" class="sm">Data versioning (DVC / lakeFS) · Schema Registry&lt;/text>
&lt;text x="50" y="148" class="tiny">trace_id · prompt_id · prompt_version · dataset_id · dataset_version · model_id · model_version · deployment_id&lt;/text>
&lt;path class="trace" d="M30,165 Q200,140 400,165 T760,160"/>
&lt;text x="755" y="172" text-anchor="end" class="tiny" fill="#1a73e8">trace running through the whole system&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-forensic-analysis-of-a-request">The analogy: forensic analysis of a request&lt;/h2>
&lt;p>When an air accident happens, the forensic analysis does not limit itself to the last seconds of the flight. The investigation team rewinds to the maintenance of the previous six months, the manufacturer&amp;rsquo;s protocols, the pilot&amp;rsquo;s record, the weather briefing, the controller&amp;rsquo;s decisions, the incident history on the same model. The conclusion is rarely &lt;em>&amp;ldquo;the wing broke&amp;rdquo;&lt;/em>; it is &lt;em>&amp;ldquo;the wing broke because an inspection protocol written a certain way did not detect microcracks that the 2014 calculation model did not consider critical and that were critical beyond a certain fatigue cycle&amp;rdquo;&lt;/em>.&lt;/p>
&lt;p>When an LLM request in production &lt;strong>fails&lt;/strong> or &lt;strong>succeeds&lt;/strong>, there is also a long causal chain behind it. The answer the user sees is the last frame; what determined it starts months earlier and branches across six operational stages. If you only look at the last frame, you attribute the result to the model. If you look at the whole chain, you see that the model is one of twelve factors and rarely the most important.&lt;/p>
&lt;p>This post does that forensic analysis, but the other way round: instead of starting from a failure and rewinding, we start from a &lt;strong>specific request that works&lt;/strong> and break down what had to happen for it to work, and what will happen to it afterwards. It is a guided tour, not an incident diagnosis. But the mental discipline is the same: no stage is autonomous, and understanding the system means understanding the bridges between stages, not just the boxes.&lt;/p>
&lt;h2 id="the-scenario-a-multi-tenant-support-chatbot-for-regulated-customers">The scenario: a multi-tenant support chatbot for regulated customers&lt;/h2>
&lt;p>For the tour we use a concrete realistic scenario, representative enough that the observations carry over to most serious deployments in May 2026. It is an &lt;strong>LLM customer support SaaS product&lt;/strong>, sold to several corporate customers (multi-tenant) in regulated sectors (banking, insurance, health). The product:&lt;/p>
&lt;ul>
&lt;li>Accepts natural-language questions through a chat embedded in the customer&amp;rsquo;s website.&lt;/li>
&lt;li>Retrieves relevant fragments from the customer&amp;rsquo;s internal knowledge base (product documents, contractual terms, FAQs).&lt;/li>
&lt;li>Answers citing the fragment used, in the user&amp;rsquo;s language, with a tone adapted to the brand.&lt;/li>
&lt;li>If the question requires an action (cancel, change plan, open a case), it invokes a &lt;strong>tool&lt;/strong> from the catalogue exposed via MCP, with user confirmation.&lt;/li>
&lt;li>It is contractually obliged not to invent customer data, to respect the tenant&amp;rsquo;s style guide, and to meet ENS / NIS2 compliance on data in the EU.&lt;/li>
&lt;/ul>
&lt;p>The infrastructure is &lt;strong>on-premise&lt;/strong> over Kubernetes (RKE2 with the Cilium CNI, in the pattern covered in &lt;a href="https://blog.lo0.es/en/posts/kubernetes-cilium-bgp-services-without-ingress/">Cilium BGP&lt;/a> and CNI networking). The GPU plane is a &lt;strong>generic 4×H100 SXM cluster&lt;/strong> (80 GB each, NVLink) for serving and production training, plus an &lt;strong>RTX 4090&lt;/strong> (24 GB) for development, offline evals and ad-hoc re-embeddings.&lt;/p>
&lt;p>The request we are going to follow:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>User&lt;/strong> (end customer, Spanish, premium plan with an insurer):
&lt;em>&amp;ldquo;How do I cancel my premium subscription? I have been paying for three months and I have not used half of the cover.&amp;rdquo;&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>At first sight, a simple question. Let us see what is behind it.&lt;/p>
&lt;h2 id="step-0--the-frame-the-answer-the-user-sees">Step 0 — The frame: the answer the user sees&lt;/h2>
&lt;p>At 11:47:32 on a Tuesday, the user sends the message. 4.2 seconds later, this appears in the UI:&lt;/p>
&lt;blockquote>
&lt;p>To cancel your premium subscription, follow these steps: (1) Sign in to the customer portal with your ID number and password. (2) Go to &lt;em>My policies&lt;/em> → &lt;em>Premium Life 2024&lt;/em>. (3) Press &lt;em>Request cancellation&lt;/em>. (4) Select the reason (in your case, &amp;ldquo;not using the product&amp;rdquo;) and confirm. You will receive a confirmation email within 24-48 h.&lt;/p>
&lt;p>Before you cancel, let me remind you that your plan includes a &lt;strong>free annual medical check-up&lt;/strong> and &lt;strong>24/7 travel assistance&lt;/strong>, cover that is lost on cancellation. If you like, I can help you review your use of the cover over the past three months to see whether switching to a more tailored plan suits you better than cancelling.&lt;/p>
&lt;p>&lt;em>Source: [Cancellation policy, section 4.2 of the 2024 General Terms]&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>After reading it, the user marks a &lt;strong>thumbs-down&lt;/strong> and writes in the form: &lt;em>&amp;ldquo;the answer is correct but the tone is too salesy; I only wanted to know how to cancel&amp;rdquo;&lt;/em>.&lt;/p>
&lt;p>That sequence, question, answer, thumbs-down with structured feedback, is the last visible frame. Let us go backwards to understand what had to happen for the answer to come out that way.&lt;/p>
&lt;h2 id="rewinding-backwards-what-was-already-in-place-before-the-request">Rewinding backwards: what was already in place before the request&lt;/h2>
&lt;p>Before the user typed, &lt;strong>the system already had&lt;/strong> a model loaded in serving, an active prompt labelled &lt;code>production&lt;/code>, an up-to-date vector index, a versioned dataset from the last fine-tuning, and a golden eval set that validated the promotion. Each of those artefacts got there through a process. We go through four jumps backwards.&lt;/p>
&lt;h3 id="t--90-days--the-previous-retrain-stage-closes-the-earlier-cycle">t = −90 days — The previous Retrain stage closes the earlier cycle&lt;/h3>
&lt;p>Three months ago, during a quarterly Retrain cycle, two things happened. The first: the support team reviewed the feedback accumulated over the previous six months and saw a pattern, the model answering premium users in an excessively formal tone, with users reporting that it &amp;ldquo;feels robotic&amp;rdquo;. The second: a one-off incident (a customer cancelling over an answer perceived as curt) triggered an incident-driven mini-cycle.&lt;/p>
&lt;p>The process, covered in detail in the &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain post&lt;/a>, followed five sub-processes:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Feedback capture&lt;/strong> — explicit thumbs-down plus implicit feedback (abandonments, retries) accumulated in a &lt;code>feedback_signals&lt;/code> table in Postgres, all of them with a &lt;code>trace_id&lt;/code> that allows rewinding to the exact context.&lt;/li>
&lt;li>&lt;strong>Triage by root cause&lt;/strong> — the &amp;ldquo;curt tone&amp;rdquo; incident cluster was categorised as a &lt;code>prompt issue&lt;/code> (it was not the model answering badly, it was the system prompt asking for an overly formal register). One sub-cluster was a &lt;code>model issue&lt;/code> (in some cases the model dug its heels in even with a warmer prompt).&lt;/li>
&lt;li>&lt;strong>Dataset enrichment&lt;/strong> — the team manually annotated 280 cases where the model was too curt, labelled with the reference answer (&amp;ldquo;how it should have answered&amp;rdquo;). Double annotation on the critical 20%; cases with a quality score below 4 were left out.&lt;/li>
&lt;li>&lt;strong>Cadence decision&lt;/strong> — the incident was treated as incident-driven; the rest of the quarterly Retrain followed the calendar.&lt;/li>
&lt;li>&lt;strong>Promotion&lt;/strong> — the new &lt;code>customer_support_v7&lt;/code> adapter went through eval gates against &lt;code>customer_support_v6&lt;/code>, a 5% canary for a week, and was promoted when the golden set metrics showed a stable improvement in the &amp;ldquo;tone / clarity&amp;rdquo; segment with no regressions elsewhere.&lt;/li>
&lt;/ol>
&lt;p>The result: the adapter active in production when the user sent the Step 0 request is &lt;code>customer_support_v7&lt;/code>, trained on the enriched dataset &lt;code>enriched_retrain_2026_q1&lt;/code> version 3, with double lineage back to the original incident.&lt;/p>
&lt;h3 id="t--60-days--the-data-stage-the-enriched-dataset-is-versioned-and-enters-circulation">t = −60 days — The Data stage: the enriched dataset is versioned and enters circulation&lt;/h3>
&lt;p>Immediately after Retrain, the Data stage of the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">LLMOps pipeline&lt;/a> does its work. Three critical operations, covered in detail in the &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">data versioning post&lt;/a>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Immutable versioning of the enriched dataset&lt;/strong> with DVC, sha256 hash propagated to the registry. The identifier &lt;code>(enriched_retrain_2026_q1, v3, sha256:9af...)&lt;/code> becomes the luggage tag that will travel through the coming stages.&lt;/li>
&lt;li>&lt;strong>Schema contract&lt;/strong> validated by CI: every row meets the JSON Schema of the entry the trainer expects (&lt;code>example_id&lt;/code>, &lt;code>input.user_query&lt;/code>, &lt;code>input.retrieved_context&lt;/code>, &lt;code>expected_output&lt;/code>, &lt;code>rubric&lt;/code>, &lt;code>segment&lt;/code>, &lt;code>difficulty&lt;/code>). A validation fails in CI if any row breaks the contract.&lt;/li>
&lt;li>&lt;strong>Holdout segregation check&lt;/strong>: the normalised sha256 hash of each &lt;code>input&lt;/code> is compared against every hash in the active golden eval set (&lt;code>customer_support_golden_v12&lt;/code>). Zero overlaps = the dataset does not contaminate the eval. Had there been a single one, CI would have blocked the merge.&lt;/li>
&lt;/ul>
&lt;p>In parallel, the &lt;strong>RAG corpus&lt;/strong> (product manuals, FAQs, general terms of the insurer tenant) is kept alive. The &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">ingestion pipeline&lt;/a> keeps capturing changes from the customer&amp;rsquo;s CMS: a new section of the cancellation policy was modified in February and reindexed in Qdrant. As the &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">post about RAG over Kafka&lt;/a> explains, the corpus is not retrained with every change: only the delta is re-embedded, and &lt;code>lakeFS&lt;/code> keeps a branch of the embeddings bucket with the new version. The branch is merged into &lt;code>main&lt;/code> when &lt;code>recall@10&lt;/code> over a representative set of queries stays above the threshold (0.78 in this system).&lt;/p>
&lt;h3 id="t--45-days--the-tune-stage-the-customer_support_v7-adapter-is-trained">t = −45 days — The Tune stage: the customer_support_v7 adapter is trained&lt;/h3>
&lt;p>Three weeks after closing the dataset, training of the new LoRA adapter starts. As the &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning post&lt;/a> details, the production pattern in 2026 avoids retraining the base model, which is expensive, slow and irreversible, and favours a &lt;strong>LoRA adapter over a stable base model&lt;/strong> (in this system, Llama 3 70B-instruct quantised to INT8 for serving). The training:&lt;/p>
&lt;ul>
&lt;li>Runs over 4 of the H100s (NVLink, tensor parallel) for ~6 hours.&lt;/li>
&lt;li>Uses &lt;code>transformers + PEFT + bitsandbytes&lt;/code>, monitored by MLflow.&lt;/li>
&lt;li>Every step records the &lt;code>dataset_id&lt;/code>, &lt;code>dataset_version&lt;/code> and &lt;code>dataset_hash&lt;/code> as an input artifact in MLflow.&lt;/li>
&lt;li>The output, a &lt;code>customer_support_v7.safetensors&lt;/code> file of ~280 MB with the LoRA weights, is uploaded to MinIO with its own hash, and MLflow records &lt;code>model_id, model_version, parent_dataset&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>At this point the lineage chain is closed for this leg:&lt;/p>
&lt;pre tabindex="0">&lt;code>enriched_retrain_2026_q1, v3, sha256:9af...
│
▼
mlflow run train, run_id: 0xa721...
│
▼
customer_support_v7, sha256:5c1...
&lt;/code>&lt;/pre>&lt;h3 id="t--38-days--the-eval-stage-the-v7-adapter-goes-through-eval-gates">t = −38 days — The Eval stage: the v7 adapter goes through eval gates&lt;/h3>
&lt;p>The freshly trained adapter is not promoted. It goes through an &lt;strong>eval suite&lt;/strong> covered in detail in the &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">post about evals&lt;/a>. The golden eval set, &lt;code>customer_support_golden_v12&lt;/code>, 850 human-curated examples with an inter-annotator kappa of 0.81, is run against two models: the candidate v7 adapter and the v6 currently in production. The metrics:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>v6 (prod)&lt;/th>
&lt;th>v7 (cand.)&lt;/th>
&lt;th>Threshold&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Faithfulness to the RAG fragment&lt;/td>
&lt;td>0.87&lt;/td>
&lt;td>0.89&lt;/td>
&lt;td>≥ 0.82&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Toxicity (low is good)&lt;/td>
&lt;td>0.012&lt;/td>
&lt;td>0.011&lt;/td>
&lt;td>≤ 0.02&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;Warm but professional&amp;rdquo; tone (judge LLM)&lt;/td>
&lt;td>0.71&lt;/td>
&lt;td>0.84&lt;/td>
&lt;td>≥ 0.78&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Format compliance (structured markdown)&lt;/td>
&lt;td>0.94&lt;/td>
&lt;td>0.93&lt;/td>
&lt;td>≥ 0.90&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Helpful-but-not-pushy (judge LLM)&lt;/td>
&lt;td>0.66&lt;/td>
&lt;td>0.79&lt;/td>
&lt;td>≥ 0.75&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Latency p95 (ms)&lt;/td>
&lt;td>2,840&lt;/td>
&lt;td>2,910&lt;/td>
&lt;td>≤ 3,500&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>To this is added the &lt;strong>guardrails and safety suite&lt;/strong> covered in the &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails post&lt;/a>: jailbreak resistance, PII leakage detection, prompt injection over MCP tools. The v7 improves on safety in two metrics and ties on the rest.&lt;/p>
&lt;p>The v7 enters a 5% traffic canary for 7 days, under close monitoring. At the end of the canary, the &lt;strong>online metrics&lt;/strong> confirm what the offline ones anticipated: an improvement in tone and helpfulness, equivalent latency, no new failure modes. Promotion approved. The v7 moves to the &lt;code>production&lt;/code> label.&lt;/p>
&lt;h3 id="t--31-days--the-deploy-stage-the-v7-adapter-enters-serving">t = −31 days — The Deploy stage: the v7 adapter enters serving&lt;/h3>
&lt;p>The &lt;code>customer_support_v7&lt;/code> adapter is promoted to the serving cluster. Three pieces covered in separate posts come into play.&lt;/p>
&lt;p>&lt;strong>vLLM as the inference engine.&lt;/strong> The engine lives on Kubernetes, deployed via a dedicated Operator, as the &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">post about LLM operators&lt;/a> and the &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">post about vLLM on K8s&lt;/a> explain. The operator is responsible for detecting the new adapter in the registry, hot-loading it without restarting the engine (a native vLLM capability with &lt;code>--enable-lora&lt;/code>), and directing traffic based on the label.&lt;/p>
&lt;p>&lt;strong>Disaggregated serving.&lt;/strong> As the &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">post about disaggregated serving&lt;/a> details, the system separates &lt;strong>prefill&lt;/strong> (compute-intensive, throughput-bound) and &lt;strong>decode&lt;/strong> (memory-intensive, latency-bound) into different GPU pools. The user&amp;rsquo;s request, when it arrives, prefills in a specialised pod and decodes in another, communicating over NVLink plus a shared KV cache fabric.&lt;/p>
&lt;p>&lt;strong>Multi-tenant GPU cluster.&lt;/strong> The H100 cluster serves several tenants, not just the insurer from Step 0. As the &lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">post about the multi-tenant cluster&lt;/a> explains, isolation is realised on four planes: the Kubernetes namespace, ACLs over adapters (only the tenant&amp;rsquo;s namespace loads its LoRAs), partitioning of the &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a> per tenant (one tenant cannot read another&amp;rsquo;s cached prefixes), and a tokens-per-minute quota enforced at the gateway.&lt;/p>
&lt;p>&lt;strong>Synchronised prompt registry.&lt;/strong> The product&amp;rsquo;s &lt;code>system_prompt&lt;/code> lives in Langfuse with the &lt;code>production&lt;/code> label. The active version is &lt;code>customer_support_system_prompt&lt;/code>, version 12. The gateway reads the prompt from Langfuse on the request path (with a few seconds of cache so it does not hammer the registry). Detailed in the &lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">prompt versioning post&lt;/a>.&lt;/p>
&lt;p>The result at t = −31 days: the combination &lt;code>(adapter v7, prompt v12, golden v12)&lt;/code> is active and served. The system is ready for the request that will arrive 31 days later.&lt;/p>
&lt;h2 id="moving-forward-the-users-request-crosses-the-system">Moving forward: the user&amp;rsquo;s request crosses the system&lt;/h2>
&lt;p>Back to Step 0: 11:47:32 on a Tuesday. The user presses Enter. We go in real time, in milliseconds.&lt;/p>
&lt;h3 id="t--0-ms--entry-through-the-gateway">t = 0 ms — Entry through the gateway&lt;/h3>
&lt;p>The user&amp;rsquo;s browser POSTs to &lt;code>chat.aseguradora-ejemplo.com/api/chat&lt;/code>. The traffic crosses the edge load balancer and enters the SaaS product&amp;rsquo;s API gateway. The gateway:&lt;/p>
&lt;ul>
&lt;li>Authenticates the user&amp;rsquo;s JWT (an end customer of the insurer tenant).&lt;/li>
&lt;li>Extracts the &lt;code>tenant_id&lt;/code> and checks that its tokens-per-minute quota is not exhausted.&lt;/li>
&lt;li>Resolves which &lt;code>model_id&lt;/code>, &lt;code>adapter_id&lt;/code> and &lt;code>prompt_id&lt;/code> correspond to this tenant and product. In this case: &lt;code>llama-3-70b-int8&lt;/code> + &lt;code>customer_support_v7&lt;/code> + prompt label &lt;code>production&lt;/code>.&lt;/li>
&lt;li>Builds a unique &lt;code>trace_id&lt;/code> (W3C TraceContext, propagable to OTel) and starts a root span.&lt;/li>
&lt;/ul>
&lt;p>At 8 ms, the gateway passes the request to the prefill pool.&lt;/p>
&lt;h3 id="t--8-ms--pulling-the-versioned-prompt">t = 8 ms — Pulling the versioned prompt&lt;/h3>
&lt;p>Before serving, the OpenAI-compatible client the engine uses internally pulls the active system prompt. As the &lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">post about prompt versioning&lt;/a> details, the pattern is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">prompt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">prompt_registry&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pull&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;customer_support_system_prompt&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">label&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;production&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># now pointing at v12&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># A local 30 s cache reduces the round-trip to 0.1 % of requests&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The OTel span of the prompt pull carries the attributes &lt;code>gen_ai.prompt.id = customer_support_system_prompt&lt;/code>, &lt;code>gen_ai.prompt.version = 12&lt;/code>, &lt;code>gen_ai.prompt.label = production&lt;/code>. They are propagated to all children.&lt;/p>
&lt;h3 id="t--12-ms--rag-retrieval">t = 12 ms — RAG retrieval&lt;/h3>
&lt;p>The system needs context from the tenant&amp;rsquo;s knowledge base. It runs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">query_embedding&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">encoder&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">encode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_query&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">chunks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">qdrant&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">collection&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;tenant_&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">tenant_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">_kb_v3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">vector&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">query_embedding&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">score_threshold&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.72&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">reranked&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">reranker&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rerank&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_query&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">chunks&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">top_k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>At 38 ms, the reranker returns two fragments: one from the &lt;em>Cancellation policy, section 4.2&lt;/em> and another from &lt;em>Premium plan benefits, section 2.1&lt;/em>. As the &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">post about PostgreSQL + Qdrant&lt;/a> details, the tenant&amp;rsquo;s corpus is kept isolated by collection and ACL: no tenant can read another&amp;rsquo;s chunks.&lt;/p>
&lt;h3 id="t--40-ms--building-the-final-payload">t = 40 ms — Building the final payload&lt;/h3>
&lt;p>The engine composes:&lt;/p>
&lt;pre tabindex="0">&lt;code>[system_prompt v12]
+ [retrieved context: 2 chunks]
+ [short session history: 1 previous turn]
+ [user query]
&lt;/code>&lt;/pre>&lt;p>Total: ~1,850 tokens of context. The OTel span records &lt;code>gen_ai.request.input_tokens = 1850&lt;/code>, &lt;code>gen_ai.request.model = llama-3-70b-int8&lt;/code>, &lt;code>gen_ai.request.adapter = customer_support_v7&lt;/code>.&lt;/p>
&lt;h3 id="t--45-ms--prefill">t = 45 ms — Prefill&lt;/h3>
&lt;p>The payload enters the prefill pool. The GPU processes the 1,850 tokens in a single parallel pass, computing for each token its K and V vectors (attention key and value). Those vectors are materialised as the &lt;strong>KV cache&lt;/strong>, covered in detail in the &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache fundamentals post&lt;/a>. The resulting cache takes up ~120 MB of VRAM in INT8.&lt;/p>
&lt;p>Here a key optimisation appears: the v12 system prompt is cached in the prefill pool (prefix caching, covered in the &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">post about PagedAttention&lt;/a>). Since the system prompt is the same for this tenant, the first ~500 tokens of the context are not recomputed: they are read from the prefix cache. That reduces the effective prefill from 1,850 tokens to ~1,350 tokens, saving ~270 ms of compute.&lt;/p>
&lt;p>At 580 ms (effective prefill), the TTFT (time to first token) is ready. The first token leaves for the decode pool.&lt;/p>
&lt;h3 id="t--580-ms--decode-streaming">t = 580 ms — Decode (streaming)&lt;/h3>
&lt;p>The decode pool receives the prefilled KV cache and starts generation token by token. As the &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">post about disaggregated serving&lt;/a> details, the prefill/decode separation is what lets a multi-tenant system keep TPS stable: the decode pool is sized to sustain thousands of sessions decoding in parallel at a low cost per token, while the prefill pool is sized for bursts of short TTFT.&lt;/p>
&lt;p>Generation at ~80 tokens per second. The answer will be ~290 tokens. Total decode time: ~3.6 s. Streaming: the user starts seeing words from t = 580 ms.&lt;/p>
&lt;p>While decode advances, the engine emits child spans on every iteration with &lt;code>gen_ai.response.tokens_generated&lt;/code>, &lt;code>gen_ai.response.cache_hit_ratio&lt;/code>, &lt;code>gen_ai.response.cumulative_latency&lt;/code>. The &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">post about AgentSight&lt;/a> and the &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">post about MCP observability with OTel&lt;/a> cover the detailed instrumentation of this layer.&lt;/p>
&lt;h3 id="t--4200-ms--complete-answer-root-span-closed">t = 4,200 ms — Complete answer, root span closed&lt;/h3>
&lt;p>Generation finishes. The engine closes the root span with &lt;code>gen_ai.response.completion_tokens = 290&lt;/code>, &lt;code>gen_ai.response.finish_reason = stop&lt;/code>, &lt;code>gen_ai.response.total_latency_ms = 4200&lt;/code>. The user sees the final answer. The session is ready for a next turn or for the user to click thumbs-up/thumbs-down.&lt;/p>
&lt;p>By this point, every active stage has taken part:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Data&lt;/strong> (pre-existing): the indexed RAG corpus, the dataset that trained the adapter, the golden set that validated it.&lt;/li>
&lt;li>&lt;strong>Tune&lt;/strong> (pre-existing): the v7 adapter trained 45 days ago.&lt;/li>
&lt;li>&lt;strong>Eval&lt;/strong> (pre-existing): the gates that approved the promotion.&lt;/li>
&lt;li>&lt;strong>Deploy&lt;/strong> (at this very moment): vLLM + disaggregated + KV cache + multi-tenant.&lt;/li>
&lt;li>&lt;strong>Observe&lt;/strong> (at this very moment): the OTel spans emitted to Langfuse + Tempo, the metrics to Prometheus.&lt;/li>
&lt;li>&lt;strong>Retrain&lt;/strong> (about to activate): the feedback the user will leave in 15 seconds.&lt;/li>
&lt;/ul>
&lt;h2 id="in-parallel-observe-is-watching">In parallel: Observe is watching&lt;/h2>
&lt;p>While the request happens, several pieces of Observe run in parallel and leave a structured trail.&lt;/p>
&lt;p>&lt;strong>OTel tracing.&lt;/strong> Every span (gateway, prompt pull, retrieval, prefill, decode) travels to Langfuse and to an OTel collector that forwards them to a backend (Tempo / Jaeger). The unique &lt;code>trace_id&lt;/code> links all the spans. As the &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">post about tracing with AgentSight&lt;/a> details, end-to-end propagation is the main enabler of post-incident debugging: without it, you cannot reconstruct what happened three weeks later.&lt;/p>
&lt;p>&lt;strong>Runtime metrics.&lt;/strong> The engine emits Prometheus metrics per interval: &lt;code>gpu_utilization&lt;/code>, &lt;code>kv_cache_usage&lt;/code>, &lt;code>tokens_per_second&lt;/code>, &lt;code>queue_depth&lt;/code>, &lt;code>prefill_latency_p95&lt;/code>, &lt;code>decode_latency_p95&lt;/code>. The metrics are not tied to a trace; they are aggregated by tenant and service.&lt;/p>
&lt;p>&lt;strong>Online LLM-as-judge.&lt;/strong> A configurable percentage of answers (2% in this system) is also run through a judge LLM in the background, which scores the answer against a simple rubric (correct / partial / incorrect + a tone score). The judge does not block the answer to the user; it feeds the dashboard.&lt;/p>
&lt;p>&lt;strong>Statistical drift.&lt;/strong> In parallel, a slower pipeline computes drift over the distribution of inputs and outputs. As the &lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">post about eBPF + drift&lt;/a> explains, low-level monitoring (latency, error rate per endpoint) is complemented with statistical drift detection (KS test, embedding distance) that spots when &amp;ldquo;something is wrong&amp;rdquo; before a thumbs-down confirms it.&lt;/p>
&lt;p>&lt;strong>Safety and guardrails monitor.&lt;/strong> The &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails post&lt;/a> describes the layer that watches for jailbreak attempts, PII leakage and prompt injection via MCP tools. In this case, none of them fires.&lt;/p>
&lt;p>All these pieces operate &lt;strong>continuously&lt;/strong>, not per request. But this particular request left its trail in every one of them.&lt;/p>
&lt;h2 id="the-feedback-the-loop-closes">The feedback: the loop closes&lt;/h2>
&lt;p>Fifteen seconds after reading the answer, the user marks a thumbs-down and writes in the form: &lt;em>&amp;ldquo;the answer is correct but the tone is too salesy; I only wanted to know how to cancel&amp;rdquo;&lt;/em>. That apparently trivial gesture triggers an important sequence.&lt;/p>
&lt;h3 id="insertion-into-feedback_signals">Insertion into feedback_signals&lt;/h3>
&lt;p>As the &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">post about Retrain&lt;/a> details, the thumbs-down is persisted as a structured row in a Postgres table:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">feedback_signals&lt;/span>&lt;span class="w"> &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="n">signal_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">trace_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">request_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">signal_type&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">signal_value&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="n">prompt_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">prompt_version&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_segment&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">occurred_at&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="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &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="n">gen_random_uuid&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="s1">&amp;#39;4f5...&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- the trace_id from Step 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;r-22a...&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- request_id
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;thumbs&amp;#39;&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="s1">&amp;#39;{&amp;#34;vote&amp;#34;:&amp;#34;down&amp;#34;,&amp;#34;reason&amp;#34;:&amp;#34;too pushy&amp;#34;,&amp;#34;text&amp;#34;:&amp;#34;I only wanted to know how to cancel&amp;#34;}&amp;#39;&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="s1">&amp;#39;customer_support_system_prompt&amp;#39;&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="mi">12&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="s1">&amp;#39;llama-3-70b-int8+customer_support_v7&amp;#39;&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="s1">&amp;#39;premium-es&amp;#39;&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="s1">&amp;#39;2026-05-19T11:47:51+02:00&amp;#39;&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="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With this, the row is linked by &lt;code>trace_id&lt;/code> to everything that happened: prompt v12, retrieved context, complete output, latency metrics, judge score (0.82 in this case, considered good by the judge, though the human disagrees).&lt;/p>
&lt;h3 id="triage-by-root-cause">Triage by root cause&lt;/h3>
&lt;p>The MLE team runs triage the next morning. Combining heuristic rules, LLM-as-classifier and human review:&lt;/p>
&lt;ul>
&lt;li>The signal is not a &lt;code>model issue&lt;/code>: the model answered correctly to the prompt it received.&lt;/li>
&lt;li>It is not a &lt;code>retrieval issue&lt;/code>: the retrieved chunks were the right ones.&lt;/li>
&lt;li>It is not an &lt;code>infra issue&lt;/code>: latency was normal.&lt;/li>
&lt;li>It is a &lt;strong>&lt;code>prompt issue&lt;/code>&lt;/strong>: the v12 system prompt instructs the model to &amp;ldquo;offer alternatives before processing destructive actions&amp;rdquo;. That instruction generates the &amp;ldquo;salesy tone&amp;rdquo; in some contexts.&lt;/li>
&lt;/ul>
&lt;p>The incident accumulates with others from the month in the &amp;ldquo;salesy tone&amp;rdquo; cluster. When the cluster crosses a threshold (typically 30-50 incidents of the same type or a percentage of the total), it will enter an incident-driven mini-cycle or wait for the quarterly Retrain, depending on the size.&lt;/p>
&lt;h3 id="the-next-cycle-picks-it-up">The next cycle picks it up&lt;/h3>
&lt;p>Three months later, in the next quarterly Retrain, this feedback is one of many that will motivate two changes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>A new prompt version v13&lt;/strong> with an adjusted instruction: &amp;ldquo;offer alternatives only if the user does not express a clear intention to cancel&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>A possible reinforcement of the adapter&lt;/strong> with cases of a more direct tone for premium-es. If the cluster justifies it.&lt;/li>
&lt;/ul>
&lt;p>The v13 will go into its own eval gate. The golden set will grow with cases where the correct tone is &amp;ldquo;direct, not salesy&amp;rdquo;. The v8 of the adapter (if it arrives) will retrain over the enriched dataset &lt;code>enriched_retrain_2026_q2&lt;/code> that already contains this annotated case.&lt;/p>
&lt;p>The loop closes. The Step 0 request has contributed to the version of the system that will serve another user three months later.&lt;/p>
&lt;h2 id="what-goes-in-each-trace-identity-and-traceability">What goes in each trace: identity and traceability&lt;/h2>
&lt;p>If the reader looks at the seven identifiers that are everywhere in this route, they see the network of identities that makes all of the above possible. It is the &lt;strong>identity infrastructure&lt;/strong> of an LLM system in production:&lt;/p>
&lt;pre tabindex="0">&lt;code>trace_id 4f5... (unique per request)
request_id r-22a... (idem)
prompt_id customer_support_system_prompt
prompt_version 12
prompt_label production
dataset_id enriched_retrain_2026_q1
dataset_version v3 (sha256:9af...)
model_id llama-3-70b-int8
adapter_id customer_support_v7 (sha256:5c1...)
deployment_id d-prod-7b
schema_version 3.2
tenant_id aseguradora-ejemplo
user_segment premium-es
golden_set_id customer_support_golden_v12
&lt;/code>&lt;/pre>&lt;p>If a single piece of that set is missing or does not propagate, &lt;strong>the chain breaks&lt;/strong>. The next incident investigated will land in &amp;ldquo;we cannot rewind to the origin because the system did not record it&amp;rdquo;. That is why the cross-cutting components, prompt versioning and data versioning, are not luxuries: they are the connection without which the other six stages operate blind.&lt;/p>
&lt;h2 id="synthesis-diagram-how-the-pieces-fit">Synthesis diagram: how the pieces fit&lt;/h2>
&lt;pre tabindex="0">&lt;code> ┌─────────────────────────────────────────┐
│ User (end customer, B2C) │
└─────────────────┬───────────────────────┘
│ chat msg + JWT
▼
┌─────────────────────────────────────────┐
│ Edge LB + WAF + Cilium CNI │
└─────────────────┬───────────────────────┘
│ HTTPS, internal mTLS
▼
┌─────────────────────────────────────────────────┐
│ API Gateway (auth, quota, model routing) │
│ - Resolves tenant → model + adapter + prompt │
│ - Starts trace_id (W3C) │
└──────┬─────────────────────┬────────────────────┘
│ │
(pull prompt) │ │ (pull config)
▼ ▼
┌────────────────────┐ ┌──────────────────────┐
│ Langfuse Prompt │ │ Model registry │
│ Registry (v12) │ │ (adapter v7) │
└─────────┬──────────┘ └──────────┬───────────┘
│ │
└──────────┬───────────────┘
│ payload ready
▼
┌──────────────────────────────────────────┐
│ vLLM engine (K8s Operator) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Pool prefill │ → │ Pool decode │ │
│ │ (H100×N) │ │ (H100×M) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ KV cache fabric │ │
│ └──────────────────┘ │
│ - prefix caching of the system prompt │
│ - PagedAttention │
└──────┬───────────────────────────────────┘
│ tokens stream
▼
┌─────────────────────────────────────────┐
│ User sees the answer + thumbs/UX UI │
└─────────────────┬───────────────────────┘
│ feedback (15 s later)
▼
┌─────────────────────────────────────────┐
│ feedback_signals (Postgres) │
│ + Langfuse scores │
└─────────────────┬───────────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
triage quarterly Retrain cycle dataset_id
root cause or incident-driven enriched (DVC)
│
▼
Tune of v8
(next cycle)
In parallel throughout the request, OTel instrumentation:
spans → Tempo / Jaeger ; events → Langfuse ; metrics → Prometheus
&lt;/code>&lt;/pre>&lt;h2 id="the-on-premise-stack-applied">The on-premise stack applied&lt;/h2>
&lt;p>Taking the above to a generic on-premise consultant-profile infrastructure (RTX 4090 + 4×H100 SXM cluster):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Typical resources&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Network plane&lt;/td>
&lt;td>Edge LB (HAProxy / nginx ingress) + Cilium CNI with BGP, covered in &lt;a href="https://blog.lo0.es/en/posts/kubernetes-cilium-bgp-services-without-ingress/">Cilium BGP&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>K8s compute plane&lt;/td>
&lt;td>RKE2 with two manager nodes + a GPU node pool&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Production GPU plane&lt;/td>
&lt;td>4× H100 SXM (NVLink, 80 GB each), partitioned via MIG into prefill/decode pools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Development GPU plane&lt;/td>
&lt;td>1× RTX 4090 (24 GB) for offline evals, drift-check embeddings, smoke tests&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Storage plane&lt;/td>
&lt;td>MinIO or Ceph object store; DVC remote + lakeFS backend&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OLTP data plane&lt;/td>
&lt;td>Postgres 18 with replication; pgvector 0.8 for small cases&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vector plane&lt;/td>
&lt;td>Qdrant or Milvus for large RAG corpora&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stream plane&lt;/td>
&lt;td>Kafka (Redpanda / pure Apache) + Schema Registry; CDC with Debezium or Flink CDC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Observability plane&lt;/td>
&lt;td>OTel Collector + Tempo (traces) + Prometheus (metrics) + Loki (logs); Langfuse for the LLM-specific part&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Runtime security plane&lt;/td>
&lt;td>Tetragon, covered in the &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">runtime security post&lt;/a>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The real density is not the sum of the boxes: it is the &lt;strong>operations&lt;/strong> that tie the boxes together. A cluster with every piece but no versioning discipline, no end-to-end &lt;code>trace_id&lt;/code> propagation, no schema contracts and no cadenced retraining is a cluster that serves LLM &lt;strong>once&lt;/strong> and then ages. The difference between a project and a platform is exactly that.&lt;/p>
&lt;h2 id="ten-bridges-between-stages-where-the-system-breaks">Ten bridges between stages where the system breaks&lt;/h2>
&lt;p>The route reveals something important: failures are rarely &lt;strong>inside&lt;/strong> a stage; they are on the &lt;strong>bridges&lt;/strong> between stages. Ten common bridges:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Data → Tune&lt;/strong>: the dataset does not propagate its &lt;code>(dataset_id, dataset_version)&lt;/code> to the trainer. The same dataset trained twice produces two &lt;code>model_id&lt;/code> values that cannot be told apart.&lt;/li>
&lt;li>&lt;strong>Tune → Eval&lt;/strong>: the trained model does not propagate its lineage to the eval run. The eval passes, but there is no record of which dataset it was trained on. Three months later, irreproducible.&lt;/li>
&lt;li>&lt;strong>Eval → Deploy&lt;/strong>: promotion happens without the serving system recording which version of the adapter it is serving at each moment. The day the model gives a dangerous answer, nobody knows which adapter answered.&lt;/li>
&lt;li>&lt;strong>Deploy → Observe&lt;/strong>: the engine does not emit &lt;code>gen_ai.request.adapter&lt;/code>, &lt;code>gen_ai.prompt.version&lt;/code>, &lt;code>gen_ai.dataset.version&lt;/code> as span attributes. The traces exist but cannot be crossed with the lineage.&lt;/li>
&lt;li>&lt;strong>Observe → Retrain&lt;/strong>: the feedback is captured in a tool (Langfuse, Phoenix) but nobody reads it. The Retrain stage &amp;ldquo;exists&amp;rdquo;, but feedback piles up untriaged.&lt;/li>
&lt;li>&lt;strong>Retrain → Data&lt;/strong>: the enriched dataset goes into the next Tune without passing through versioning discipline, schema contract and holdout check. Silent contamination of the golden set.&lt;/li>
&lt;li>&lt;strong>Prompt versioning ↔ everything&lt;/strong>: the &lt;code>prompt_id, prompt_version&lt;/code> does not propagate to the spans. The day the team discovers that a prompt change regressed the system, it cannot isolate which one or when.&lt;/li>
&lt;li>&lt;strong>Data versioning ↔ everything&lt;/strong>: the &lt;code>dataset_id, dataset_version&lt;/code> does not show up in experiment tracking. &amp;ldquo;v8 is retrained&amp;rdquo; but nobody can prove it was over the enriched dataset and not the old one.&lt;/li>
&lt;li>&lt;strong>MCP ↔ tools&lt;/strong>: the system invokes tools (cancellation, policy changes) but does not record a &lt;code>gen_ai.tool.invocation_id&lt;/code> linked to the trace. The actions end up dissociated from the answer that generated them.&lt;/li>
&lt;li>&lt;strong>Schema Registry ↔ data&lt;/strong>: datasets version content but not schema. A breaking change in &lt;code>expected_output&lt;/code> breaks the eval silently; nobody notices anything until a human reviews the results.&lt;/li>
&lt;/ol>
&lt;p>The bridges are covered throughout the blog. Operations enforce them. The team&amp;rsquo;s culture keeps them.&lt;/p>
&lt;h2 id="how-to-walk-through-the-blog">How to walk through the blog&lt;/h2>
&lt;p>If you arrive at this post from outside and want a reading route:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>The map&lt;/strong>: &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 everything else.&lt;/li>
&lt;li>&lt;strong>The context&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">MLOps specific to LLMs in 2026&lt;/a> — the landscape and why LLMOps is not classic MLOps.&lt;/li>
&lt;li>&lt;strong>Inference from the inside out&lt;/strong>: &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 deep dive&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/gpu-cluster-as-multi-tenant-platform/">Multi-tenant GPU cluster&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 operators on K8s&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Data&lt;/strong>: &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 ingestion&lt;/a> → &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Tune&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Eval&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a> → &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Observe&lt;/strong>: &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/">On-device eBPF + drift&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Retrain&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Closing the feedback → dataset → adapter loop&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Cross-cutting&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">Prompt versioning with Langfuse and MLflow&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Supporting infrastructure&lt;/strong> (the base everything is built on): &lt;a href="https://blog.lo0.es/en/posts/kubernetes-cilium-bgp-services-without-ingress/">RKE2 with Cilium BGP&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble + eBPF observability&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon runtime security&lt;/a>.&lt;/li>
&lt;/ol>
&lt;h2 id="what-we-have-not-covered-yet">What we have not covered (yet)&lt;/h2>
&lt;p>At the first level the main things are there. The following posts on the blog, when the topics justify it, could go deeper into:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Schema Registry for LLM data and prompts&lt;/strong>: the other half of the data contract.&lt;/li>
&lt;li>&lt;strong>A dedicated AI Gateway&lt;/strong>: LiteLLM, Portkey, Kong AI Gateway as a control plane.&lt;/li>
&lt;li>&lt;strong>OTel gen_ai semantic conventions&lt;/strong>: the emerging standard that ties the seven identifiers of the &amp;ldquo;identity&amp;rdquo; block into well-formed spans.&lt;/li>
&lt;li>&lt;strong>Federated learning over regulated customer data&lt;/strong>: how to train without centralising the corpus.&lt;/li>
&lt;li>&lt;strong>Capacity planning&lt;/strong> for shared multi-tenant clusters.&lt;/li>
&lt;li>&lt;strong>Disaster recovery&lt;/strong> of an LLM service: how to reproduce the state of the system 30 days back.&lt;/li>
&lt;li>&lt;strong>Cost accounting per tenant&lt;/strong>: tokens × weights × adapter × infrastructure → invoice.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">The parallel catalogue: the six LLMOps stages in open source and in the hyperscalers&lt;/a> — the vertical cut that complements this post: the same six stages + two cross-cutting components, but crossed with their equivalents in AWS, GCP and Azure, and with the insurer&amp;rsquo;s chatbot ported to an AWS stack.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages: entry by entry&lt;/a> — the zoom in on the open source side of the parallel catalogue: a ~150-word entry per core tool (vLLM, Langfuse, DVC, Qdrant, Airflow, NeMo Guardrails, Presidio…), licence and governance, a decision matrix per stage and a diagram of the connected OSS stack. It works as the consultant&amp;rsquo;s reference toolbox.&lt;/li>
&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>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">MLOps specific to LLMs in 2026&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">Data versioning for LLMOps&lt;/a>&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>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka: technical architecture&lt;/a>&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>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory of LLM inference&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention from the inside&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving: prefill and decode&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding: the secretary who anticipates what the boss is going to say&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/flashattention-librarian-io-awareness-async-blackwell-asymmetry/">FlashAttention v1/v2/v3/v4: the librarian who never clears the desk&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">The GPU cluster as a multi-tenant platform&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM operators on Kubernetes&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight: end-to-end LLM tracing&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP from the inside and observability with OTel&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">eBPF in local inference and drift detection&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the feedback → dataset → adapter loop&lt;/a>&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>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kubernetes-cilium-bgp-services-without-ingress/">RKE2 with Cilium BGP&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble + eBPF observability&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon runtime security&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.w3.org/TR/trace-context/">W3C Trace Context&lt;/a> — end-to-end propagation of &lt;code>traceparent&lt;/code> and &lt;code>tracestate&lt;/code>.&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry GenAI Semantic Conventions&lt;/a> — &lt;code>gen_ai.*&lt;/code> attributes for LLM spans.&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs">Langfuse documentation&lt;/a> — observability and prompt registry.&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/">vLLM documentation&lt;/a> — production inference engine with PagedAttention and LoRA hot-swap.&lt;/li>
&lt;li>&lt;a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/">Kubernetes Operators&lt;/a> — the declarative management pattern.&lt;/li>
&lt;li>&lt;a href="https://mlflow.org/docs/latest/">MLflow Tracking and Model Registry&lt;/a> — lineage of runs and input artifacts.&lt;/li>
&lt;li>&lt;a href="https://dvc.org/">DVC&lt;/a> and &lt;a href="https://lakefs.io/">lakeFS&lt;/a> — dataset versioning, unified in Nov 2025.&lt;/li>
&lt;li>&lt;a href="https://openlineage.io/">OpenLineage&lt;/a> — an open standard for lineage events between systems.&lt;/li>
&lt;li>ENS / NIS2: compliance frameworks that apply to operators in the EU; recommended reading for the context in which the scenario operates.&lt;/li>
&lt;/ul></description></item><item><title>The parallel catalogue: the six LLMOps stages in open source and on the hyperscalers (AWS, GCP, Azure)</title><link>https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/</link><pubDate>Sat, 23 May 2026 07:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">previous forensic post&lt;/a> used a single request to walk the six stages of the LLMOps pipeline and the two cross-cutting components. This post walks the same stages but crosses them with three extra columns: how each stage is built in &lt;strong>open source on-premise&lt;/strong>, and what the equivalent services are on &lt;strong>AWS&lt;/strong>, &lt;strong>GCP&lt;/strong> and &lt;strong>Azure&lt;/strong>. It is not a migration guide nor a cost benchmark: it is a &lt;strong>catalogue of equivalences&lt;/strong> with its gaps. The general pattern you will see: OSS gives you control, sovereignty and free composition in exchange for expensive operations; the hyperscalers give you integration and time-to-market in exchange for lock-in on margins, data contracts and political dependency. For scenarios subject to ENS / NIS2 with critical customer data, OSS wins by default; for discovery projects where time-to-market is the deciding metric, the hyperscaler wins by default. The interesting part is in the middle. As a concrete thread, at the end we take the multi-tenant chatbot from the previous post and port it to AWS piece by piece to show what disappears, what appears, and where the lock-in materialises.&lt;/p>
&lt;h2 id="you-are-here-the-same-six-stages-but-by-column">You are here: the same six stages, but by column&lt;/h2>
&lt;p>This post shares a map with the previous one, the six stages and the two cross-cutting pieces are all active, but it changes the cut: instead of following a request horizontally, it makes the vertical cut and shows which tools live in each stage depending on the deployment model.&lt;/p>
&lt;div class="diagram" style="max-width:840px;margin:1rem auto;">
&lt;svg viewBox="0 0 840 310" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="the six LLMOps stages with their OSS and hyperscaler equivalents">
&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:10px sans-serif;fill:#333}.tiny{font:9px sans-serif;fill:#222}.row{font:600 10px sans-serif;fill:#333}.oss{fill:#dfe9f5;stroke:#356}.aws{fill:#fde6c8;stroke:#a65}.gcp{fill:#dceaf8;stroke:#369}.azu{fill:#dde2f8;stroke:#447}.foot{font:9px sans-serif;fill:#666}&lt;/style>
&lt;text x="420" y="20" text-anchor="middle" class="lbl">Parallel catalogue: open source on-premise vs managed hyperscalers&lt;/text>
&lt;rect x="60" y="35" width="125" height="30" class="box active"/>&lt;text x="122.5" y="54" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="190" y="35" width="125" height="30" class="box active"/>&lt;text x="252.5" y="54" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="320" y="35" width="125" height="30" class="box active"/>&lt;text x="382.5" y="54" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="450" y="35" width="125" height="30" class="box active"/>&lt;text x="512.5" y="54" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="580" y="35" width="125" height="30" class="box active"/>&lt;text x="642.5" y="54" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="710" y="35" width="125" height="30" class="box active"/>&lt;text x="772.5" y="54" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;text x="52" y="98" text-anchor="end" class="row">OSS&lt;/text>
&lt;rect x="60" y="80" width="125" height="30" class="box oss"/>&lt;text x="122.5" y="99" text-anchor="middle" class="tiny">DVC · lakeFS · Qdrant&lt;/text>
&lt;rect x="190" y="80" width="125" height="30" class="box oss"/>&lt;text x="252.5" y="99" text-anchor="middle" class="tiny">PEFT · MLflow · Ray&lt;/text>
&lt;rect x="320" y="80" width="125" height="30" class="box oss"/>&lt;text x="382.5" y="99" text-anchor="middle" class="tiny">DeepEval · Promptfoo&lt;/text>
&lt;rect x="450" y="80" width="125" height="30" class="box oss"/>&lt;text x="512.5" y="99" text-anchor="middle" class="tiny">vLLM · KServe · Operators&lt;/text>
&lt;rect x="580" y="80" width="125" height="30" class="box oss"/>&lt;text x="642.5" y="99" text-anchor="middle" class="tiny">OTel · Tempo · Langfuse&lt;/text>
&lt;rect x="710" y="80" width="125" height="30" class="box oss"/>&lt;text x="772.5" y="99" text-anchor="middle" class="tiny">Airflow · Argo · Kubeflow&lt;/text>
&lt;text x="52" y="138" text-anchor="end" class="row">AWS&lt;/text>
&lt;rect x="60" y="120" width="125" height="30" class="box aws"/>&lt;text x="122.5" y="139" text-anchor="middle" class="tiny">S3 · OpenSearch · MSK&lt;/text>
&lt;rect x="190" y="120" width="125" height="30" class="box aws"/>&lt;text x="252.5" y="139" text-anchor="middle" class="tiny">SageMaker · Bedrock&lt;/text>
&lt;rect x="320" y="120" width="125" height="30" class="box aws"/>&lt;text x="382.5" y="139" text-anchor="middle" class="tiny">Bedrock Eval · Guardrails&lt;/text>
&lt;rect x="450" y="120" width="125" height="30" class="box aws"/>&lt;text x="512.5" y="139" text-anchor="middle" class="tiny">Bedrock · SM Endpoints&lt;/text>
&lt;rect x="580" y="120" width="125" height="30" class="box aws"/>&lt;text x="642.5" y="139" text-anchor="middle" class="tiny">CloudWatch · X-Ray · ADOT&lt;/text>
&lt;rect x="710" y="120" width="125" height="30" class="box aws"/>&lt;text x="772.5" y="139" text-anchor="middle" class="tiny">SM Pipelines · GT&lt;/text>
&lt;text x="52" y="178" text-anchor="end" class="row">GCP&lt;/text>
&lt;rect x="60" y="160" width="125" height="30" class="box gcp"/>&lt;text x="122.5" y="179" text-anchor="middle" class="tiny">GCS · BQ · Vertex VS&lt;/text>
&lt;rect x="190" y="160" width="125" height="30" class="box gcp"/>&lt;text x="252.5" y="179" text-anchor="middle" class="tiny">Vertex Training · Tuning&lt;/text>
&lt;rect x="320" y="160" width="125" height="30" class="box gcp"/>&lt;text x="382.5" y="179" text-anchor="middle" class="tiny">Vertex Eval · Model Armor&lt;/text>
&lt;rect x="450" y="160" width="125" height="30" class="box gcp"/>&lt;text x="512.5" y="179" text-anchor="middle" class="tiny">Vertex Pred · Gemini API&lt;/text>
&lt;rect x="580" y="160" width="125" height="30" class="box gcp"/>&lt;text x="642.5" y="179" text-anchor="middle" class="tiny">Cloud Trace · Monitoring&lt;/text>
&lt;rect x="710" y="160" width="125" height="30" class="box gcp"/>&lt;text x="772.5" y="179" text-anchor="middle" class="tiny">Vertex Pipelines&lt;/text>
&lt;text x="52" y="218" text-anchor="end" class="row">Azure&lt;/text>
&lt;rect x="60" y="200" width="125" height="30" class="box azu"/>&lt;text x="122.5" y="219" text-anchor="middle" class="tiny">ADLS · AI Search · ADF&lt;/text>
&lt;rect x="190" y="200" width="125" height="30" class="box azu"/>&lt;text x="252.5" y="219" text-anchor="middle" class="tiny">Azure ML · AOAI tuning&lt;/text>
&lt;rect x="320" y="200" width="125" height="30" class="box azu"/>&lt;text x="382.5" y="219" text-anchor="middle" class="tiny">AI Eval · Content Safety&lt;/text>
&lt;rect x="450" y="200" width="125" height="30" class="box azu"/>&lt;text x="512.5" y="219" text-anchor="middle" class="tiny">AOAI · ML Endpoints&lt;/text>
&lt;rect x="580" y="200" width="125" height="30" class="box azu"/>&lt;text x="642.5" y="219" text-anchor="middle" class="tiny">App Insights · Monitor&lt;/text>
&lt;rect x="710" y="200" width="125" height="30" class="box azu"/>&lt;text x="772.5" y="219" text-anchor="middle" class="tiny">Azure ML Pipelines&lt;/text>
&lt;rect x="60" y="245" width="385" height="26" class="cross"/>
&lt;text x="252.5" y="262" text-anchor="middle" class="sm">Prompt versioning: Langfuse · MLflow ↔ Bedrock · Vertex · Foundry&lt;/text>
&lt;rect x="450" y="245" width="385" height="26" class="cross"/>
&lt;text x="642.5" y="262" text-anchor="middle" class="sm">Data versioning: DVC · lakeFS · OpenLineage ↔ S3 · Dataplex · Purview&lt;/text>
&lt;text x="420" y="295" text-anchor="middle" class="foot">The OSS stack runs on-premise; AWS / GCP / Azure show the managed equivalents per stage.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-your-own-bakery-and-the-franchise">The analogy: your own bakery and the franchise&lt;/h2>
&lt;p>A baker opens a business. There are two possible models.&lt;/p>
&lt;p>He can open &lt;strong>his own bakery&lt;/strong>: rent the premises, buy the oven, choose the flour suppliers, hire his master baker, write his recipes, set the prices, dress the shop window. The day he wants to launch an organic rye sourdough loaf, he asks nobody for permission. The day the price of flour goes up, he finds another supplier. But he pays for all of it: the initial investment, the risk, the daily operations, the months when he misreads the neighbourhood. The bakery is his.&lt;/p>
&lt;p>Or he can join a &lt;strong>franchise&lt;/strong>: the franchisor hands over the turnkey premises, the oven with a maintenance contract, the suppliers already negotiated, the operating manuals, the written recipes, the centralised marketing, the loyalty app, the POS system. The learning curve is weeks, not years. But the recipes belong to the franchisor, so do the suppliers, the price of bread is in the catalogue, and the day the croissant formula changes he gets an informational email, not a business decision.&lt;/p>
&lt;p>Both bakeries turn out bread. Both meet health regulations and generate income. The operational difference is enormous and it is not about technology: it is about &lt;strong>ownership, control and timescale&lt;/strong>.&lt;/p>
&lt;p>The parallel with LLMOps is direct. The on-premise OSS stack is the bakery you own. The managed stack on the hyperscalers is the franchise. The &lt;strong>pieces&lt;/strong> that appear in each stage are functionally equivalent, at the end of the day both solve the same technical problem, but the governance model, the operational cost, the lock-in and the compliance guarantees are different. This post builds the parallel catalogue so that the choice is not made by default.&lt;/p>
&lt;h2 id="quick-recap-of-the-previous-post">Quick recap of the previous post&lt;/h2>
&lt;p>In the &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">forensic post&lt;/a> we followed one specific request: a premium-es user of an insurance company asking &lt;em>&amp;ldquo;How do I cancel my premium subscription?&amp;rdquo;&lt;/em> to the multi-tenant support chatbot of the SaaS provider that hosts it. The route crossed the &lt;strong>six stages of the LLMOps pipeline&lt;/strong>, Data, Tune, Eval, Deploy, Observe, Retrain, plus the &lt;strong>two cross-cutting components&lt;/strong>, prompt versioning and data versioning, over an on-premise infrastructure: RKE2 with Cilium BGP, a 4×H100 SXM cluster, an RTX 4090 for development, vLLM on Kubernetes, Langfuse + OTel + Prometheus + Tempo, Postgres + Qdrant, DVC + lakeFS + MinIO, Kafka and MLflow. The system meets ENS / NIS2 and keeps &lt;code>trace_id&lt;/code> propagated end to end.&lt;/p>
&lt;p>What comes now is that same system, piece by piece, showing for each box which tool does the job if you are on public cloud, because the integrator&amp;rsquo;s question is rarely &amp;ldquo;OSS yes or no?&amp;rdquo;: it is &amp;ldquo;what do I lose and what do I gain if I take this box managed?&amp;rdquo;. And the answer differs box by box.&lt;/p>
&lt;h2 id="stage-1--data">Stage 1 — Data&lt;/h2>
&lt;p>&lt;strong>The problem.&lt;/strong> There are three sub-problems the Data stage solves, frequently confused with one another. First, &lt;strong>versioning and identity&lt;/strong> of the corpus and of the training datasets (making sure a &lt;code>dataset_id, dataset_version&lt;/code> exists and propagates). Second, &lt;strong>storage and serving&lt;/strong> of the operational corpus (object store + vector index + structured text). Third, &lt;strong>streams and ingestion&lt;/strong> from source systems with CDC, transformation and stable schemas (Schema Registry).&lt;/p>
&lt;p>&lt;strong>Reference OSS stack.&lt;/strong> Versioning lives in &lt;strong>DVC&lt;/strong> (pointers in git, content in an object store) combined with &lt;strong>lakeFS&lt;/strong> for branch/merge semantics over data. The &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">post on data versioning&lt;/a> goes into the functional difference. The object store is &lt;strong>MinIO&lt;/strong> or &lt;strong>Ceph&lt;/strong>. The vector index is &lt;strong>Qdrant&lt;/strong> or &lt;strong>Milvus&lt;/strong> for large corpora (millions of chunks) and &lt;strong>pgvector on Postgres 18&lt;/strong> for small cases where having one component fewer pays off operationally. The stream layer is &lt;strong>Kafka&lt;/strong> (pure Apache or &lt;strong>Redpanda&lt;/strong>) with a &lt;strong>Schema Registry&lt;/strong> (Confluent or &lt;strong>Karapace&lt;/strong> OSS), CDC with &lt;strong>Debezium&lt;/strong> or &lt;strong>Flink CDC&lt;/strong>, transformation with &lt;strong>Flink&lt;/strong> or &lt;strong>Spark Structured Streaming&lt;/strong>. The catalogue / lineage is &lt;strong>DataHub&lt;/strong>, &lt;strong>Apache Atlas&lt;/strong> or &lt;strong>OpenMetadata&lt;/strong> with &lt;strong>OpenLineage&lt;/strong> events between systems. The &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">post on PostgreSQL + Qdrant ingestion&lt;/a> and the &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">post on RAG over Kafka&lt;/a> cover the detailed operations.&lt;/p>
&lt;p>&lt;strong>Hyperscaler equivalents.&lt;/strong> On &lt;strong>AWS&lt;/strong>, the corpus lives in &lt;strong>S3&lt;/strong> (with versioning enabled, which is the cheap substitute for serious data versioning), tabular queries in &lt;strong>Athena&lt;/strong> or &lt;strong>Redshift&lt;/strong>, the vector index in &lt;strong>Amazon OpenSearch&lt;/strong> with the vector plug-in or in &lt;strong>Amazon Aurora pgvector&lt;/strong>. The stream layer is &lt;strong>MSK&lt;/strong> (managed Kafka) or &lt;strong>Kinesis Data Streams&lt;/strong>, CDC with &lt;strong>AWS DMS&lt;/strong>, transformation with &lt;strong>Glue Streaming&lt;/strong> or &lt;strong>MSK Connect&lt;/strong>. The catalogue is &lt;strong>AWS Glue Data Catalog&lt;/strong> + &lt;strong>AWS Lake Formation&lt;/strong> for data governance. And for the RAG case there is also &lt;strong>Amazon Bedrock Knowledge Bases&lt;/strong>, the managed shortcut: you give it S3, it indexes into OpenSearch or Aurora pgvector, exposes a retrieval API and the operations end there, in exchange for paying per indexed chunk and per query.&lt;/p>
&lt;p>On &lt;strong>GCP&lt;/strong>, the corpus lives in &lt;strong>Cloud Storage&lt;/strong> (with object versioning), the analytical warehouse is &lt;strong>BigQuery&lt;/strong> (with &lt;strong>BigQuery Vector Search&lt;/strong> already integrated), the dedicated vector store is &lt;strong>Vertex AI Vector Search&lt;/strong> (formerly Matching Engine). The stream layer is &lt;strong>Pub/Sub&lt;/strong> + &lt;strong>Dataflow&lt;/strong>, CDC with &lt;strong>Datastream&lt;/strong>. Catalogue and lineage are &lt;strong>Dataplex&lt;/strong> (which in 2024-2025 absorbed Data Catalog and added automatic lineage). The managed equivalent of Knowledge Bases is &lt;strong>Vertex AI Search&lt;/strong> (formerly Discovery Engine).&lt;/p>
&lt;p>On &lt;strong>Azure&lt;/strong>, the corpus lives in &lt;strong>ADLS Gen2&lt;/strong>, tabular queries in &lt;strong>Microsoft Fabric&lt;/strong> / &lt;strong>Azure Synapse&lt;/strong>, the vector index in &lt;strong>Azure AI Search&lt;/strong> (vector mode) or &lt;strong>Azure Cosmos DB for PostgreSQL&lt;/strong> with pgvector. The stream layer is &lt;strong>Event Hubs&lt;/strong> + &lt;strong>Stream Analytics&lt;/strong> or &lt;strong>Microsoft Fabric Real-Time Intelligence&lt;/strong>, CDC with &lt;strong>Azure Data Factory&lt;/strong>. The catalogue is &lt;strong>Microsoft Purview&lt;/strong>, which covers catalogue, lineage and data governance integrated with Entra ID.&lt;/p>
&lt;p>&lt;strong>Summary table — Data stage.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Functional piece&lt;/th>
&lt;th>OSS on-premise&lt;/th>
&lt;th>AWS&lt;/th>
&lt;th>GCP&lt;/th>
&lt;th>Azure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Object store&lt;/td>
&lt;td>MinIO, Ceph&lt;/td>
&lt;td>S3&lt;/td>
&lt;td>Cloud Storage&lt;/td>
&lt;td>ADLS Gen2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dataset versioning&lt;/td>
&lt;td>DVC, lakeFS&lt;/td>
&lt;td>S3 Versioning (limited), Lake Formation&lt;/td>
&lt;td>GCS Versioning, Dataplex&lt;/td>
&lt;td>ADLS versioning, Purview&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vector index&lt;/td>
&lt;td>Qdrant, Milvus, pgvector&lt;/td>
&lt;td>OpenSearch, Aurora pgvector, Bedrock KB&lt;/td>
&lt;td>Vertex Vector Search, BigQuery VS&lt;/td>
&lt;td>Azure AI Search, Cosmos pgvector&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stream + CDC&lt;/td>
&lt;td>Kafka + Debezium + Flink&lt;/td>
&lt;td>MSK / Kinesis + DMS + Glue&lt;/td>
&lt;td>Pub/Sub + Datastream + Dataflow&lt;/td>
&lt;td>Event Hubs + ADF&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Schema Registry&lt;/td>
&lt;td>Karapace, Confluent OSS&lt;/td>
&lt;td>Glue Schema Registry&lt;/td>
&lt;td>Pub/Sub schemas&lt;/td>
&lt;td>Schema Registry (Event Hubs)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Catalogue + lineage&lt;/td>
&lt;td>DataHub, Atlas, OpenLineage&lt;/td>
&lt;td>Glue Catalog + Lake Formation&lt;/td>
&lt;td>Dataplex&lt;/td>
&lt;td>Purview&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Managed end-to-end RAG&lt;/td>
&lt;td>— (you build it)&lt;/td>
&lt;td>Bedrock Knowledge Bases&lt;/td>
&lt;td>Vertex AI Search&lt;/td>
&lt;td>Azure AI Studio Knowledge&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Where the names mislead.&lt;/strong> &lt;em>S3 Versioning&lt;/em> is not DVC. It keeps object versions but has no notion of a &lt;strong>dataset&lt;/strong> (which objects together make up version 3 of the enriched set?), does not propagate &lt;code>dataset_hash&lt;/code> to the trainer, does not integrate with experiment tracking, and does not fail a CI run if a dataset breaks schema. Covering that properly on AWS requires combining it with Lake Formation, Glue Data Catalog and your own records in SageMaker Experiments. The same applies on GCP with Dataplex and on Azure with Purview. The gap is real and is paid for in operations or in broken lineage.&lt;/p>
&lt;h2 id="stage-2--tune">Stage 2 — Tune&lt;/h2>
&lt;p>&lt;strong>The problem.&lt;/strong> Producing a new &lt;code>model_id, model_version&lt;/code>, typically a LoRA adapter over a stable base, as 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> explains, with lineage back to the dataset that trained it and experiment tracking that lets you reproduce it six months later.&lt;/p>
&lt;p>&lt;strong>OSS stack.&lt;/strong> Technical core: &lt;strong>HuggingFace Transformers + PEFT&lt;/strong> (LoRA, QLoRA), &lt;strong>bitsandbytes&lt;/strong> for quantisation, &lt;strong>DeepSpeed&lt;/strong> or &lt;strong>FSDP&lt;/strong> for parallelism. Experiment tracking: &lt;strong>MLflow&lt;/strong> (authoritative) or &lt;strong>Weights &amp;amp; Biases self-hosted&lt;/strong>. Convenience frameworks: &lt;strong>Axolotl&lt;/strong> and &lt;strong>Llama Factory&lt;/strong> wrap the machinery above in declarative configuration. Distributed orchestration: &lt;strong>Kubeflow Training Operator&lt;/strong> or &lt;strong>Ray Train&lt;/strong>. On small infrastructures, direct scripts with &lt;strong>Slurm&lt;/strong> or &lt;strong>K8s Jobs&lt;/strong> over GPU pools. The lineage chain &lt;code>dataset → run → model&lt;/code> is closed by registering the dataset as an MLflow input artefact.&lt;/p>
&lt;p>&lt;strong>Hyperscaler equivalents.&lt;/strong> On &lt;strong>AWS&lt;/strong>, &lt;strong>SageMaker Training Jobs&lt;/strong> covers most workloads, &lt;strong>SageMaker HyperPod&lt;/strong> covers large training runs with resilience to node failures, and &lt;strong>SageMaker JumpStart&lt;/strong> offers click-to-train fine-tuning over a catalogue of pre-curated models. For fine-tuning Bedrock models (hosted Claude, Llama, Mistral) there is &lt;strong>Bedrock Custom Models&lt;/strong>: you upload the dataset to S3, Bedrock trains, and it returns a private endpoint with provisioned throughput. The equivalent experiment tracking is &lt;strong>SageMaker Experiments&lt;/strong> or &lt;strong>managed MLflow in SageMaker&lt;/strong> (yes, AWS has officially hosted MLflow since 2024).&lt;/p>
&lt;p>On &lt;strong>GCP&lt;/strong>, &lt;strong>Vertex AI Custom Training&lt;/strong> runs any container with GPUs or TPUs; &lt;strong>Vertex AI Tuning&lt;/strong> is the managed API for fine-tuning Gemini and Model Garden models. Experiment tracking in &lt;strong>Vertex AI Experiments&lt;/strong> (with MLflow compatibility).&lt;/p>
&lt;p>On &lt;strong>Azure&lt;/strong>, &lt;strong>Azure ML Training Jobs&lt;/strong> on your own clusters or managed compute; &lt;strong>Azure OpenAI fine-tuning&lt;/strong> for fine-tuning GPT and the o-series; &lt;strong>Azure ML Experiments&lt;/strong> with MLflow natively integrated since 2022.&lt;/p>
&lt;p>&lt;strong>Summary table — Tune stage.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Functional piece&lt;/th>
&lt;th>OSS on-premise&lt;/th>
&lt;th>AWS&lt;/th>
&lt;th>GCP&lt;/th>
&lt;th>Azure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Training framework&lt;/td>
&lt;td>HF Transformers + PEFT&lt;/td>
&lt;td>SageMaker SDK&lt;/td>
&lt;td>Vertex AI SDK&lt;/td>
&lt;td>Azure ML SDK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quantisation / parallelism&lt;/td>
&lt;td>bitsandbytes, DeepSpeed, FSDP&lt;/td>
&lt;td>SageMaker libs + HF support&lt;/td>
&lt;td>Vertex + HF support&lt;/td>
&lt;td>Azure ML + HF support&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Managed fine-tuning (black box)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Bedrock Custom Models, JumpStart&lt;/td>
&lt;td>Vertex Tuning (Gemini)&lt;/td>
&lt;td>Azure OpenAI fine-tuning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Distributed on a cluster&lt;/td>
&lt;td>Kubeflow, Ray Train, Slurm&lt;/td>
&lt;td>SageMaker HyperPod&lt;/td>
&lt;td>Vertex AI Training (multi-node)&lt;/td>
&lt;td>Azure ML compute clusters&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Experiment tracking&lt;/td>
&lt;td>MLflow, W&amp;amp;B self-hosted&lt;/td>
&lt;td>SageMaker Experiments, managed MLflow&lt;/td>
&lt;td>Vertex Experiments&lt;/td>
&lt;td>Azure ML + MLflow&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Access to the base model&lt;/td>
&lt;td>Whatever you download (Llama, Mistral, Qwen)&lt;/td>
&lt;td>Bedrock catalog + HF Hub&lt;/td>
&lt;td>Vertex Model Garden + HF Hub&lt;/td>
&lt;td>Azure ML model catalog + HF Hub&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Where the names mislead.&lt;/strong> The &lt;em>managed&lt;/em> fine-tunings (Bedrock Custom, Vertex Tuning, AOAI fine-tuning) are a &lt;strong>black box&lt;/strong>: you do not choose hyperparameters beyond a handful, you do not see the trainer&amp;rsquo;s detailed logs, and you cannot inspect the dataset once it is inside their pipeline. The experiment tracking they offer is not comparable to MLflow sitting next to the trainer, where you can capture any metric and artefact. For scenarios where &lt;em>operationally&lt;/em> you do not need inspection this is liberating; for ENS / NIS2 scenarios where you have to prove what trained what, the black box fails by construction.&lt;/p>
&lt;h2 id="stage-3--eval">Stage 3 — Eval&lt;/h2>
&lt;p>&lt;strong>The problem.&lt;/strong> Validating candidates before and after promotion against a golden set, with operational metrics (faithfulness to the RAG, tone, format compliance, toxicity, jailbreak resistance, PII leakage) run as gates in CI and as online sampling. Covered in the &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">post on evals&lt;/a> and the one on &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a>.&lt;/p>
&lt;p>&lt;strong>OSS stack.&lt;/strong> Eval suites: &lt;strong>DeepEval&lt;/strong>, &lt;strong>RAGAS&lt;/strong> (specialised in RAG), &lt;strong>Promptfoo&lt;/strong> (declarative, ideal for CI), &lt;strong>lm-eval-harness&lt;/strong> (academic), &lt;strong>HELM&lt;/strong>. Evals integrated with tracing: &lt;strong>Langfuse Evals&lt;/strong>, &lt;strong>Phoenix Arize OSS&lt;/strong>. LLM-as-judge judges: any local OSS model; in serious systems, two different judges to reduce bias. Safety and guardrails: &lt;strong>NeMo Guardrails&lt;/strong> (NVIDIA), &lt;strong>Guardrails AI&lt;/strong>, &lt;strong>LlamaGuard&lt;/strong> + &lt;strong>PromptGuard&lt;/strong> (Meta), &lt;strong>ShieldGemma&lt;/strong> (Google, open weights), &lt;strong>PII detectors&lt;/strong> such as &lt;strong>Presidio&lt;/strong> (Microsoft) on-prem.&lt;/p>
&lt;p>&lt;strong>Hyperscaler equivalents.&lt;/strong> On &lt;strong>AWS&lt;/strong>, &lt;strong>Bedrock Model Evaluation&lt;/strong> offers automatic evals (toxicity, accuracy, robustness) and human-in-the-loop, &lt;strong>Bedrock Guardrails&lt;/strong> covers the safety layer (denied topics, PII, prompt injection, contextual grounding check), and &lt;strong>SageMaker Clarify&lt;/strong> adds bias and explainability over general models.&lt;/p>
&lt;p>On &lt;strong>GCP&lt;/strong>, &lt;strong>Vertex AI Evaluation Service&lt;/strong> runs evals with automatic metrics and an LLM judge, &lt;strong>Vertex AI Model Armor&lt;/strong> and the &lt;strong>safety filters&lt;/strong> built into the Gemini API cover the guardrails layer. &lt;strong>Vertex AI Studio&lt;/strong> exposes interactive Eval for prompt iteration.&lt;/p>
&lt;p>On &lt;strong>Azure&lt;/strong>, the &lt;strong>Azure AI Evaluation SDK&lt;/strong> runs offline evals against datasets, &lt;strong>Azure AI Content Safety&lt;/strong> covers safety (Prompt Shields against jailbreak, &lt;strong>Groundedness detection&lt;/strong>, content categories, &lt;strong>PII detection&lt;/strong>). All accessible from &lt;strong>Azure AI Foundry&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Summary table — Eval stage.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Functional piece&lt;/th>
&lt;th>OSS on-premise&lt;/th>
&lt;th>AWS&lt;/th>
&lt;th>GCP&lt;/th>
&lt;th>Azure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Automatic eval suite&lt;/td>
&lt;td>DeepEval, RAGAS, Promptfoo&lt;/td>
&lt;td>Bedrock Model Evaluation&lt;/td>
&lt;td>Vertex AI Evaluation Service&lt;/td>
&lt;td>Azure AI Evaluation SDK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLM-as-judge&lt;/td>
&lt;td>Any OSS model&lt;/td>
&lt;td>Bedrock judge models&lt;/td>
&lt;td>Vertex judge (Gemini)&lt;/td>
&lt;td>Azure OpenAI judges&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Golden set management&lt;/td>
&lt;td>Langfuse datasets, manual&lt;/td>
&lt;td>SageMaker Ground Truth datasets&lt;/td>
&lt;td>Vertex Datasets&lt;/td>
&lt;td>Azure ML Datasets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Guardrails (jailbreak, PII, prompt injection)&lt;/td>
&lt;td>NeMo Guardrails, LlamaGuard, Presidio&lt;/td>
&lt;td>Bedrock Guardrails&lt;/td>
&lt;td>Vertex Model Armor + Gemini safety&lt;/td>
&lt;td>Azure AI Content Safety (Prompt Shields, Groundedness)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Eval in CI&lt;/td>
&lt;td>Promptfoo + GitHub Actions&lt;/td>
&lt;td>Bedrock Eval API + CodeBuild&lt;/td>
&lt;td>Vertex Eval API + Cloud Build&lt;/td>
&lt;td>Azure AI Eval + Azure Pipelines&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Where the names mislead.&lt;/strong> Managed guardrails are convenient but &lt;strong>opaque&lt;/strong>: the Bedrock Guardrails rules are configurable but the detection implementation cannot be inspected; the same goes for Azure AI Content Safety. In OSS, NeMo Guardrails shows you the Colang graph and Presidio shows you the recognisers, auditable and modifiable. For regulated systems where an auditor asks &lt;em>&amp;ldquo;how exactly does it detect PII?&amp;rdquo;&lt;/em>, OSS answers with code; the cloud answers with documentation.&lt;/p>
&lt;h2 id="stage-4--deploy">Stage 4 — Deploy&lt;/h2>
&lt;p>&lt;strong>The problem.&lt;/strong> Serving tokens to the end user with predictable latency and throughput, a decent cost / token ratio, hot-swap adapter support, and multi-tenancy if the business demands it. Covered in the posts on &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 operators&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">multi-tenant cluster&lt;/a>.&lt;/p>
&lt;p>&lt;strong>OSS stack.&lt;/strong> Inference engine: &lt;strong>vLLM&lt;/strong> (PagedAttention, prefix caching, LoRA hot-swap, OpenAI-compatible API) as the reference, &lt;strong>TensorRT-LLM&lt;/strong> for maximum optimisation on Hopper / Ada, &lt;strong>SGLang&lt;/strong> for workloads with heavy prompt restructuring, &lt;strong>TGI&lt;/strong> (Hugging Face) as a mature alternative, &lt;strong>llama.cpp&lt;/strong> for edge and CPUs, &lt;strong>NVIDIA Dynamo&lt;/strong> for multi-node disaggregated serving on large clusters. Orchestration on Kubernetes: &lt;strong>KServe&lt;/strong>, &lt;strong>KubeRay&lt;/strong>, dedicated operators such as &lt;strong>llm-d&lt;/strong>, &lt;strong>vLLM Production Stack&lt;/strong> and &lt;strong>KAITO&lt;/strong>. Gateway / control plane: &lt;strong>Envoy AI Gateway&lt;/strong>, &lt;strong>LiteLLM Proxy&lt;/strong>, &lt;strong>Portkey AI Gateway&lt;/strong>, &lt;strong>Kong AI Gateway&lt;/strong>. Triton Inference Server covers mixed workloads (LLM + traditional) where a single backend matters.&lt;/p>
&lt;p>&lt;strong>Hyperscaler equivalents.&lt;/strong> On &lt;strong>AWS&lt;/strong>, two different routes. The &lt;em>managed-by-model&lt;/em> route is &lt;strong>Amazon Bedrock&lt;/strong>: a catalogue of hosted models (Claude, Llama, Mistral, Cohere, Titan), pay per token or &lt;strong>Provisioned Throughput&lt;/strong> with an SLA, &lt;strong>Bedrock Prompt Caching&lt;/strong> as the conceptual equivalent of vLLM&amp;rsquo;s prefix caching, and integrated &lt;strong>Bedrock Agents&lt;/strong> and &lt;strong>Bedrock Knowledge Bases&lt;/strong>. The &lt;em>managed-by-infrastructure&lt;/em> route is &lt;strong>SageMaker Endpoints&lt;/strong> (real-time, async, serverless, batch) with &lt;strong>Inference Components&lt;/strong> to densify multiple models on one instance. Own hardware: &lt;strong>AWS Inferentia&lt;/strong> and &lt;strong>Trainium&lt;/strong> via the &lt;strong>Neuron&lt;/strong> chip, an alternative to NVIDIA with a better cost / token on stable workloads if you compile your model.&lt;/p>
&lt;p>On &lt;strong>GCP&lt;/strong>, &lt;strong>Vertex AI Prediction Endpoints&lt;/strong> runs your containers or models from the &lt;strong>Model Garden&lt;/strong>, the &lt;strong>Gemini API&lt;/strong> via Vertex AI offers the managed Gemini models, and &lt;strong>Cloud TPU v5e / v5p / Trillium (v6)&lt;/strong> is the in-house hardware competing with the H100 for training and inference. For sovereignty there is &lt;strong>Google Distributed Cloud air-gapped&lt;/strong>, which brings Vertex AI to a certifiable on-premise rack.&lt;/p>
&lt;p>On &lt;strong>Azure&lt;/strong>, &lt;strong>Azure OpenAI Service&lt;/strong> serves OpenAI models (GPT-4.1, o-series, GPT-image), &lt;strong>Azure ML Managed Online Endpoints&lt;/strong> runs any model (OSS included, via container), and &lt;strong>Azure AI Foundry models&lt;/strong> absorbed in 2025 the catalogue of open models served as-a-service. Hardware: &lt;strong>Azure ND H100 v5&lt;/strong>, &lt;strong>ND H200 v5&lt;/strong>, &lt;strong>ND GB200 v6&lt;/strong> and the in-house bet &lt;strong>Microsoft Maia 100&lt;/strong> for internal inference.&lt;/p>
&lt;p>&lt;strong>Summary table — Deploy stage.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Functional piece&lt;/th>
&lt;th>OSS on-premise&lt;/th>
&lt;th>AWS&lt;/th>
&lt;th>GCP&lt;/th>
&lt;th>Azure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Inference engine&lt;/td>
&lt;td>vLLM, TensorRT-LLM, SGLang, TGI&lt;/td>
&lt;td>Bedrock (managed model), SM Endpoints (your container)&lt;/td>
&lt;td>Vertex Prediction, Gemini API&lt;/td>
&lt;td>Azure OpenAI, Azure ML Endpoints&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prefix / prompt caching&lt;/td>
&lt;td>vLLM native&lt;/td>
&lt;td>Bedrock Prompt Caching&lt;/td>
&lt;td>Vertex AI context caching&lt;/td>
&lt;td>Azure OpenAI prompt caching&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Adapter hot-swap (LoRA)&lt;/td>
&lt;td>vLLM &lt;code>--enable-lora&lt;/code>, S-LoRA&lt;/td>
&lt;td>Bedrock Custom Models endpoints&lt;/td>
&lt;td>Vertex Tuning endpoints&lt;/td>
&lt;td>Azure OpenAI fine-tuned deployments&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Disaggregated serving&lt;/td>
&lt;td>NVIDIA Dynamo, vLLM PD-disagg&lt;/td>
&lt;td>— (managed internally, not exposed)&lt;/td>
&lt;td>— (managed internally, not exposed)&lt;/td>
&lt;td>— (managed internally, not exposed)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Accelerator hardware&lt;/td>
&lt;td>NVIDIA H100/H200/B200, AMD MI300&lt;/td>
&lt;td>Inferentia, Trainium, NVIDIA&lt;/td>
&lt;td>TPU v5/v6, NVIDIA&lt;/td>
&lt;td>Maia, NVIDIA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AI Gateway / proxy&lt;/td>
&lt;td>Envoy AI Gateway, LiteLLM, Portkey, Kong&lt;/td>
&lt;td>API Gateway + Bedrock&lt;/td>
&lt;td>Vertex AI + Apigee&lt;/td>
&lt;td>Azure API Management + AOAI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>K8s orchestration&lt;/td>
&lt;td>KServe, KubeRay, llm-d, KAITO&lt;/td>
&lt;td>EKS + SageMaker Operators&lt;/td>
&lt;td>GKE + Vertex AI&lt;/td>
&lt;td>AKS + KAITO&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Where the names mislead.&lt;/strong> Bedrock Prompt Caching and Vertex context caching &lt;strong>sound&lt;/strong> equivalent to vLLM&amp;rsquo;s prefix caching, but operationally they are different: the cache lives on the hyperscaler&amp;rsquo;s plane, its eviction policy is opaque, its cost is billed separately, and you cannot easily see the hit ratio per tenant. In vLLM you see the hit ratio in Prometheus metrics and you decide the policy. The same applies to disaggregated serving: the hyperscalers implement it internally to cut their own cost of serving, but they &lt;strong>do not expose&lt;/strong> prefill/decode control to the user. If you need your workload&amp;rsquo;s TTFT to be controlled separately from TPS, that lever is not available.&lt;/p>
&lt;h2 id="stage-5--observe">Stage 5 — Observe&lt;/h2>
&lt;p>&lt;strong>The problem.&lt;/strong> End-to-end LLM traces with &lt;code>trace_id&lt;/code> propagated through every component, runtime metrics per tenant, online scoring (LLM judge over sampling), statistical drift, and safety / guardrails monitoring. Covered in the posts on &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight tracing&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;p>&lt;strong>OSS stack.&lt;/strong> Base standard: &lt;strong>OpenTelemetry&lt;/strong> (specification + collector + SDKs) with the &lt;strong>gen_ai semantic conventions&lt;/strong> that stabilised in 2025. Backends: &lt;strong>Tempo&lt;/strong> or &lt;strong>Jaeger&lt;/strong> for traces, &lt;strong>Prometheus&lt;/strong> for metrics, &lt;strong>Loki&lt;/strong> for logs, &lt;strong>Grafana&lt;/strong> as the common UI. LLM-specific layer: &lt;strong>Langfuse&lt;/strong> (self-hosted with an optional EE licence) and &lt;strong>Phoenix Arize OSS&lt;/strong>. eBPF layer for low-level observability: &lt;strong>Pixie&lt;/strong>, &lt;strong>Hubble&lt;/strong>, and &lt;strong>Cilium Tetragon&lt;/strong> for runtime security. Drift: &lt;strong>Evidently AI&lt;/strong>, &lt;strong>NannyML&lt;/strong>, &lt;strong>Alibi Detect&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Hyperscaler equivalents.&lt;/strong> On &lt;strong>AWS&lt;/strong>, &lt;strong>CloudWatch&lt;/strong> (metrics + logs) + &lt;strong>AWS X-Ray&lt;/strong> (traces) form the base, &lt;strong>CloudWatch Application Signals&lt;/strong> adds OTel-compatible APM, and &lt;strong>Amazon Managed Prometheus&lt;/strong> and &lt;strong>Amazon Managed Grafana&lt;/strong> serve the plane if you want to keep Prom + Grafana without operating them. &lt;strong>Bedrock logging&lt;/strong> integrates with CloudWatch and S3. &lt;strong>ADOT&lt;/strong> (AWS Distro for OpenTelemetry) is the official collector.&lt;/p>
&lt;p>On &lt;strong>GCP&lt;/strong>, &lt;strong>Cloud Monitoring&lt;/strong> + &lt;strong>Cloud Logging&lt;/strong> + &lt;strong>Cloud Trace&lt;/strong> + &lt;strong>Cloud Profiler&lt;/strong> form the quintet, all OTel compatible. &lt;strong>Vertex AI Model Monitoring&lt;/strong> offers drift detection (feature skew, prediction drift) integrated with runs.&lt;/p>
&lt;p>On &lt;strong>Azure&lt;/strong>, &lt;strong>Azure Monitor&lt;/strong> + &lt;strong>Application Insights&lt;/strong> + &lt;strong>Log Analytics&lt;/strong> cover the APM stack with native OTel, &lt;strong>Azure ML Model Monitor&lt;/strong> adds drift and data quality, and &lt;strong>Azure OpenAI diagnostic logs&lt;/strong> enrich the traces with token and model metadata.&lt;/p>
&lt;p>&lt;strong>Summary table — Observe stage.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Functional piece&lt;/th>
&lt;th>OSS on-premise&lt;/th>
&lt;th>AWS&lt;/th>
&lt;th>GCP&lt;/th>
&lt;th>Azure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Traces (OTel)&lt;/td>
&lt;td>OTel + Tempo / Jaeger&lt;/td>
&lt;td>X-Ray + ADOT, App Signals&lt;/td>
&lt;td>Cloud Trace&lt;/td>
&lt;td>App Insights + Azure Monitor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Metrics&lt;/td>
&lt;td>Prometheus + Grafana&lt;/td>
&lt;td>CloudWatch + AMP / AMG&lt;/td>
&lt;td>Cloud Monitoring&lt;/td>
&lt;td>Azure Monitor Metrics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Logs&lt;/td>
&lt;td>Loki, ELK&lt;/td>
&lt;td>CloudWatch Logs&lt;/td>
&lt;td>Cloud Logging&lt;/td>
&lt;td>Log Analytics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLM-specific (prompt, scores, sessions)&lt;/td>
&lt;td>Langfuse, Phoenix Arize OSS&lt;/td>
&lt;td>Bedrock logging + CW + custom&lt;/td>
&lt;td>Vertex AI tracing + custom&lt;/td>
&lt;td>App Insights + AOAI logs + custom&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drift detection&lt;/td>
&lt;td>Evidently, NannyML, Alibi Detect&lt;/td>
&lt;td>SageMaker Model Monitor&lt;/td>
&lt;td>Vertex AI Model Monitoring&lt;/td>
&lt;td>Azure ML Model Monitor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>eBPF / runtime&lt;/td>
&lt;td>Pixie, Hubble, Tetragon&lt;/td>
&lt;td>— (no direct equivalent)&lt;/td>
&lt;td>GKE Dataplane v2 / Cloud Service Mesh&lt;/td>
&lt;td>Azure CNI + Defender for Cloud&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Where the names mislead.&lt;/strong> The classic cloud APM tools (X-Ray, Cloud Trace, App Insights) do not understand &lt;strong>prompt versioning&lt;/strong> or &lt;strong>adapter id&lt;/strong> as native concepts. They accept the &lt;code>gen_ai.*&lt;/code> attributes as dimensions, but the UIs do not prioritise those views. Langfuse and Phoenix do, because they are designed for LLM. In cloud, the usual pattern is to send dual: APM to the managed service for infrastructure + self-hosted Langfuse / Phoenix for the LLM plane. That pays off.&lt;/p>
&lt;h2 id="stage-6--retrain--cross-cutting-pieces">Stage 6 — Retrain + cross-cutting pieces&lt;/h2>
&lt;p>&lt;strong>The problem (Retrain).&lt;/strong> Closing the loop feedback → triage → enriched dataset → new adapter, with a mixed cadence (quarterly + incident-driven). Covered in the &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain post&lt;/a>.&lt;/p>
&lt;p>&lt;strong>OSS Retrain stack.&lt;/strong> Orchestration: &lt;strong>Apache Airflow&lt;/strong>, &lt;strong>Prefect&lt;/strong>, &lt;strong>Dagster&lt;/strong> or &lt;strong>Argo Workflows&lt;/strong> and &lt;strong>Kubeflow Pipelines&lt;/strong> for K8s-native. Feature store where applicable: &lt;strong>Feast&lt;/strong>. Annotation and human-in-the-loop: &lt;strong>Argilla&lt;/strong>, &lt;strong>Label Studio&lt;/strong>, &lt;strong>Trubrics&lt;/strong>. Structured feedback capture: your own &lt;strong>Postgres&lt;/strong> table + &lt;strong>Langfuse scores&lt;/strong> + &lt;strong>Phoenix annotations&lt;/strong>. Closed-loop lineage: &lt;strong>OpenLineage&lt;/strong> tying dataset → run → model → deployment → feedback → next dataset.&lt;/p>
&lt;p>&lt;strong>Hyperscaler Retrain equivalents.&lt;/strong> On &lt;strong>AWS&lt;/strong>, &lt;strong>SageMaker Pipelines&lt;/strong> orchestrates the cycle, &lt;strong>SageMaker Ground Truth&lt;/strong> and &lt;strong>A2I&lt;/strong> (Augmented AI) manage annotation and HiL, and &lt;strong>SageMaker Model Monitor&lt;/strong> fires alerts that can invoke retrain pipelines. &lt;strong>AWS Step Functions&lt;/strong> serves as a more general alternative orchestrator.&lt;/p>
&lt;p>On &lt;strong>GCP&lt;/strong>, &lt;strong>Vertex AI Pipelines&lt;/strong> (based on Kubeflow Pipelines, compatible) orchestrates, &lt;strong>Vertex AI Data Labeling Service&lt;/strong> annotates, &lt;strong>Vertex AI Feature Store&lt;/strong> manages features, and &lt;strong>Workflows&lt;/strong> or &lt;strong>Cloud Composer&lt;/strong> (managed Airflow) act as orchestration alternatives.&lt;/p>
&lt;p>On &lt;strong>Azure&lt;/strong>, &lt;strong>Azure ML Pipelines&lt;/strong> orchestrates, &lt;strong>Azure ML Data Labeling&lt;/strong> annotates, &lt;strong>Azure ML Feature Store&lt;/strong> manages features.&lt;/p>
&lt;p>&lt;strong>The problem (cross-cutting: prompt + data versioning).&lt;/strong> Making sure &lt;code>prompt_id, prompt_version&lt;/code> and &lt;code>dataset_id, dataset_version&lt;/code> propagate through the whole system and show up in spans, runs and metrics. Covered in the posts on &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;p>&lt;strong>Prompt versioning equivalents.&lt;/strong> OSS: &lt;strong>Langfuse Prompts&lt;/strong>, &lt;strong>MLflow Prompt Registry&lt;/strong>. AWS: &lt;strong>Bedrock Prompt Management&lt;/strong> (catalogue, versions, labels, integrated A/B testing) and &lt;strong>SageMaker Prompt Hub&lt;/strong>. GCP: &lt;strong>Vertex AI Prompt Management&lt;/strong> inside Vertex AI Studio. Azure: &lt;strong>Azure AI Foundry Prompt flow&lt;/strong> and prompt versioning in &lt;strong>Azure OpenAI deployments&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Data versioning equivalents.&lt;/strong> OSS: DVC + lakeFS (already covered under Data). AWS: S3 Versioning + Lake Formation + Glue Catalog (they are not DVC but together they cover part of it). GCP: Cloud Storage versioning + Dataplex (same). Azure: ADLS Gen2 versioning + Purview (same). The &lt;strong>real gap&lt;/strong> here is that no hyperscaler offers DVC natively, so dataset-as-first-class-citizen operations still require a layer of your own.&lt;/p>
&lt;p>&lt;strong>Summary table — Retrain stage + cross-cutting pieces.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Functional piece&lt;/th>
&lt;th>OSS on-premise&lt;/th>
&lt;th>AWS&lt;/th>
&lt;th>GCP&lt;/th>
&lt;th>Azure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>ML pipeline orchestration&lt;/td>
&lt;td>Airflow, Dagster, Argo, Kubeflow&lt;/td>
&lt;td>SageMaker Pipelines, Step Functions&lt;/td>
&lt;td>Vertex AI Pipelines, Cloud Composer&lt;/td>
&lt;td>Azure ML Pipelines&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Feature store&lt;/td>
&lt;td>Feast&lt;/td>
&lt;td>SageMaker Feature Store&lt;/td>
&lt;td>Vertex AI Feature Store&lt;/td>
&lt;td>Azure ML Feature Store&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Annotation / HiL&lt;/td>
&lt;td>Argilla, Label Studio&lt;/td>
&lt;td>SageMaker Ground Truth, A2I&lt;/td>
&lt;td>Vertex Data Labeling&lt;/td>
&lt;td>Azure ML Data Labeling&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Feedback capture&lt;/td>
&lt;td>Postgres + Langfuse scores&lt;/td>
&lt;td>Bedrock + custom + Ground Truth&lt;/td>
&lt;td>Vertex + custom&lt;/td>
&lt;td>App Insights + custom&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prompt versioning&lt;/td>
&lt;td>Langfuse Prompts, MLflow Prompts&lt;/td>
&lt;td>Bedrock Prompt Management&lt;/td>
&lt;td>Vertex Prompt Management&lt;/td>
&lt;td>Azure AI Foundry Prompt flow&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Data versioning&lt;/td>
&lt;td>DVC + lakeFS + OpenLineage&lt;/td>
&lt;td>S3 Versioning + Lake Formation&lt;/td>
&lt;td>GCS + Dataplex&lt;/td>
&lt;td>ADLS + Purview&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cross-system lineage&lt;/td>
&lt;td>OpenLineage + DataHub&lt;/td>
&lt;td>SageMaker Lineage Tracking&lt;/td>
&lt;td>Dataplex lineage&lt;/td>
&lt;td>Purview&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="the-chatbot-from-the-previous-post-ported-to-aws">The chatbot from the previous post ported to AWS&lt;/h2>
&lt;p>To stop the catalogue being abstract, we take the full scenario from the &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">previous post&lt;/a>, the multi-tenant support chatbot for insurers on an on-premise OSS stack, and describe it component by component with an AWS stack. It is not an executable migration; it is the &lt;strong>map of what disappears, what appears and where the lock-in shows up&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>The network plane.&lt;/strong> Edge LB and WAF: &lt;strong>AWS WAF + CloudFront&lt;/strong>. Ingress to the cluster: &lt;strong>AWS Load Balancer Controller&lt;/strong> over &lt;strong>EKS&lt;/strong>. What was Cilium BGP + RKE2 is replaced by EKS with &lt;strong>VPC CNI&lt;/strong> (or Cilium on EKS, which is possible). The conceptual equivalent of Tetragon is &lt;strong>Amazon GuardDuty for EKS&lt;/strong> + optional &lt;strong>Falco&lt;/strong>. Moderate lock-in: network control couples to VPC.&lt;/p>
&lt;p>&lt;strong>The chat gateway and auth.&lt;/strong> What was a bespoke API gateway with JWT verification materialises as &lt;strong>Amazon API Gateway&lt;/strong> + &lt;strong>Amazon Cognito&lt;/strong> (or IAM Identity Center if it is B2B). The gateway&amp;rsquo;s AI-aware routing is covered with &lt;strong>Bedrock&lt;/strong> + per-customer tags, or with &lt;strong>AWS API Gateway custom authorizers&lt;/strong> invoking a Lambda for tenant resolution. High lock-in on the identity layer if Cognito is chosen.&lt;/p>
&lt;p>&lt;strong>The inference engine.&lt;/strong> Three different options, with a clear trade-off.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Bedrock with a managed model&lt;/strong> (Claude / Llama / Mistral): all the operations of vLLM, K8s Operators, KV cache and disaggregated serving disappear. You move to &lt;strong>Provisioned Throughput&lt;/strong> for a latency guarantee. You gain time-to-market; you lose control over prefill/decode, over custom LoRA adapters (Bedrock accepts Bedrock-managed fine-tunes but not arbitrary LoRAs), and you enter model lock-in (moving from Claude to Llama means changing API).&lt;/li>
&lt;li>&lt;strong>SageMaker Endpoints with your own vLLM container&lt;/strong>: you keep vLLM and its optimisations, but K8s disappears and SageMaker replaces it as the orchestration plane. Inference Components allow you to densify multiple adapters. KV cache, prefix caching and LoRA hot-swap work the same. Moderate lock-in on the SageMaker SDK and on the Inference Components format.&lt;/li>
&lt;li>&lt;strong>EKS with vLLM&lt;/strong> (the minimalist option): basically the OSS stack but with EKS instead of RKE2 and EBS/EFS instead of Ceph. Low lock-in, limited benefit from the cloud.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Data layer.&lt;/strong> The corpus moves to &lt;strong>S3&lt;/strong> with versioning, the embeddings to &lt;strong>Amazon OpenSearch Service&lt;/strong> or to &lt;strong>Aurora pgvector&lt;/strong>. The radical managed option is &lt;strong>Bedrock Knowledge Bases&lt;/strong>: you upload documents to S3, it indexes them, it exposes a retrieval API. We remove Qdrant, we remove manual embedding pipelines, we remove part of Kafka + Flink. But control over custom reranking, fine-grained per-chunk ACL and the possibility of re-embedding with a new encoder of your own disappears. Bedrock KB uses the Titan or Cohere embedders available in Bedrock, and changing them means changing the whole index. ENS compliance: you have to validate that the buckets and the index live in EU regions and that the embedding model does too.&lt;/p>
&lt;p>&lt;strong>Stream + CDC.&lt;/strong> Kafka + Debezium is replaced by &lt;strong>MSK&lt;/strong> + &lt;strong>MSK Connect&lt;/strong> or by &lt;strong>Kinesis + DMS&lt;/strong>. Schema Registry: &lt;strong>Glue Schema Registry&lt;/strong>. The events remain functionally equivalent. Moderate lock-in if you go to Kinesis (Kinesis is not Kafka), low if you go to MSK (Kafka compatibility).&lt;/p>
&lt;p>&lt;strong>Data versioning.&lt;/strong> Here the gap is clear. S3 Versioning + Lake Formation + Glue Catalog &lt;strong>is not DVC&lt;/strong>. To preserve the discipline of the previous post, &lt;code>(dataset_id, dataset_version, sha256_hash)&lt;/code> propagated as an input artefact to the trainer, you can keep DVC over S3 (DVC works perfectly with S3 as a remote) or accept the limitation and register lineage manually in SageMaker Lineage Tracking. The first option preserves the practice; the second accepts degradation.&lt;/p>
&lt;p>&lt;strong>Tune stage.&lt;/strong> The LoRA adapter &lt;code>customer_support_v7&lt;/code> is trained with &lt;strong>SageMaker Training Jobs&lt;/strong> on &lt;strong>ml.p5.48xlarge&lt;/strong> instances (8× H100), using a standard HuggingFace + PEFT container. MLflow managed by SageMaker or your own MLflow on EC2 covers tracking. Alternative: if the black box is acceptable, &lt;strong>Bedrock Custom Models&lt;/strong> with a dataset in S3 produces a fine-tuned Bedrock model without instantiating a GPU manually, at the cost of not being able to inspect the run.&lt;/p>
&lt;p>&lt;strong>Eval stage.&lt;/strong> Promptfoo + RAGAS in CI runs the same over &lt;strong>CodeBuild&lt;/strong>. &lt;strong>Bedrock Model Evaluation&lt;/strong> replaces a good part of the automatic eval suite. &lt;strong>Bedrock Guardrails&lt;/strong> replaces NeMo Guardrails + Presidio + LlamaGuard, with the loss of transparency noted earlier.&lt;/p>
&lt;p>&lt;strong>Deploy stage.&lt;/strong> If Bedrock was chosen as the engine, this stage vanishes: Bedrock serves. If SageMaker Endpoints + vLLM was chosen, KServe is replaced by SageMaker Operators (or KServe is kept over EKS). The AI Gateway that in OSS was Envoy AI Gateway or LiteLLM becomes &lt;strong>API Gateway&lt;/strong> + &lt;strong>Bedrock&lt;/strong> or &lt;strong>API Gateway&lt;/strong> + &lt;strong>Lambda&lt;/strong> + &lt;strong>SageMaker&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Observe stage.&lt;/strong> The OTel Collector remains the standard. Traces to &lt;strong>AWS X-Ray&lt;/strong> + &lt;strong>CloudWatch Application Signals&lt;/strong>. Metrics to &lt;strong>Amazon Managed Prometheus&lt;/strong>. Logs to &lt;strong>CloudWatch Logs&lt;/strong> + optionally &lt;strong>OpenSearch&lt;/strong> for search. &lt;strong>Langfuse&lt;/strong> is hosted on &lt;strong>ECS Fargate&lt;/strong> or &lt;strong>EKS&lt;/strong> because the cloud has no native equivalent of integrated prompt + traces + scores. Drift: &lt;strong>SageMaker Model Monitor&lt;/strong> replaces Evidently / NannyML. eBPF (Pixie / Hubble / Tetragon) has &lt;strong>no direct equivalent&lt;/strong> in managed AWS, so Falco or installing Tetragon on EKS is still the route.&lt;/p>
&lt;p>&lt;strong>Retrain stage.&lt;/strong> &lt;strong>SageMaker Pipelines&lt;/strong> orchestrates the quarterly cycle. &lt;strong>SageMaker Ground Truth&lt;/strong> + &lt;strong>A2I&lt;/strong> replace Argilla. The &lt;code>feedback_signals&lt;/code> table in Postgres stays as it is (RDS Postgres) or moves to DynamoDB for large scales.&lt;/p>
&lt;p>&lt;strong>How much the lock-in weighs.&lt;/strong> The component with the highest lock-in is Bedrock + Bedrock Knowledge Bases + Bedrock Guardrails: leaving that requires rewriting the inference plane and reindexing the whole RAG. Next is the SageMaker SDK (Pipelines, Endpoints, Training), where leaving costs but means rewriting scripts, not data. Data in S3 is portable (S3 → MinIO with &lt;code>rclone&lt;/code> works). OTel observability is portable at almost no cost if the collector is kept as the abstraction. The auth gateway is the other high lock-in point if it uses Cognito.&lt;/p>
&lt;p>&lt;strong>What you gain.&lt;/strong> A dramatic reduction in GPU infrastructure operations, K8s patching, CUDA driver management, prefill/decode sizing, Ceph / MinIO management. A very short start-up curve: one request served in less than a sprint versus several weeks of bring-up for the OSS stack. Explicit provider SLAs.&lt;/p>
&lt;p>&lt;strong>What you lose.&lt;/strong> Contractual data sovereignty (the data stays in EU regions if configured that way, but the operator is a US third party under the Cloud Act). Visibility of the whole stack (Bedrock is a black box from the model downwards). Roadmap independence (the decision to discontinue a model, raise prices or change guardrails is not controlled by the customer). Fine-grained cost-per-token optimisation (the levers are the ones the provider exposes). For ENS customers under a HIGH declaration or NIS2 essential category, several of these points are &lt;strong>non-compliance&lt;/strong>, not preference.&lt;/p>
&lt;h2 id="master-table-the-entire-parallel-catalogue">Master table: the entire parallel catalogue&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Stage / component&lt;/th>
&lt;th>OSS on-premise (the blog&amp;rsquo;s reference)&lt;/th>
&lt;th>AWS&lt;/th>
&lt;th>GCP&lt;/th>
&lt;th>Azure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Data&lt;/strong>&lt;/td>
&lt;td>DVC + lakeFS + MinIO + Qdrant + Kafka + Debezium&lt;/td>
&lt;td>S3 + Lake Formation + OpenSearch / Aurora pgvector + MSK + DMS&lt;/td>
&lt;td>GCS + Dataplex + Vertex Vector Search + Pub/Sub + Datastream&lt;/td>
&lt;td>ADLS Gen2 + Purview + Azure AI Search + Event Hubs + ADF&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Data versioning (cross-cutting)&lt;/strong>&lt;/td>
&lt;td>DVC + lakeFS + OpenLineage&lt;/td>
&lt;td>S3 Versioning + Lake Formation + Glue Catalog&lt;/td>
&lt;td>GCS Versioning + Dataplex lineage&lt;/td>
&lt;td>ADLS versioning + Purview&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tune&lt;/strong>&lt;/td>
&lt;td>HF Transformers + PEFT + bitsandbytes + MLflow + Ray/Kubeflow&lt;/td>
&lt;td>SageMaker Training + HyperPod + Bedrock Custom + SM Experiments&lt;/td>
&lt;td>Vertex AI Training + Vertex Tuning + Vertex Experiments&lt;/td>
&lt;td>Azure ML Training + Azure OpenAI fine-tuning + Azure ML + MLflow&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Eval&lt;/strong>&lt;/td>
&lt;td>DeepEval + RAGAS + Promptfoo + Langfuse Evals + NeMo Guardrails&lt;/td>
&lt;td>Bedrock Model Evaluation + Bedrock Guardrails + SageMaker Clarify&lt;/td>
&lt;td>Vertex AI Evaluation Service + Model Armor + Gemini safety&lt;/td>
&lt;td>Azure AI Evaluation SDK + Content Safety (Prompt Shields, Groundedness)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Deploy&lt;/strong>&lt;/td>
&lt;td>vLLM + KServe + LLM Operators + Envoy AI Gateway&lt;/td>
&lt;td>Bedrock + SageMaker Endpoints (+ Inferentia / Trainium)&lt;/td>
&lt;td>Vertex AI Prediction + Gemini API (+ TPU)&lt;/td>
&lt;td>Azure OpenAI + Azure ML Endpoints (+ Maia)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Observe&lt;/strong>&lt;/td>
&lt;td>OTel + Tempo + Prometheus + Loki + Langfuse + Phoenix + Hubble&lt;/td>
&lt;td>CloudWatch + X-Ray + ADOT + AMP/AMG + SM Model Monitor&lt;/td>
&lt;td>Cloud Monitoring + Cloud Trace + Vertex Model Monitoring&lt;/td>
&lt;td>Azure Monitor + App Insights + Azure ML Model Monitor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Retrain&lt;/strong>&lt;/td>
&lt;td>Airflow / Argo / Kubeflow Pipelines + Argilla + Feast&lt;/td>
&lt;td>SageMaker Pipelines + Ground Truth + A2I + SM Feature Store&lt;/td>
&lt;td>Vertex AI Pipelines + Data Labeling + Vertex Feature Store&lt;/td>
&lt;td>Azure ML Pipelines + Data Labeling + Azure ML Feature Store&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Prompt versioning (cross-cutting)&lt;/strong>&lt;/td>
&lt;td>Langfuse Prompts + MLflow Prompt Registry&lt;/td>
&lt;td>Bedrock Prompt Management + SM Prompt Hub&lt;/td>
&lt;td>Vertex AI Prompt Management&lt;/td>
&lt;td>Azure AI Foundry Prompt flow&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="when-to-choose-each-side--the-real-decision">When to choose each side — the real decision&lt;/h2>
&lt;p>The right question is not &lt;em>&amp;ldquo;OSS or cloud?&amp;rdquo;&lt;/em>. It is stage by stage.&lt;/p>
&lt;p>The &lt;strong>OSS side wins by default&lt;/strong> when there is:&lt;/p>
&lt;ul>
&lt;li>Data subject to ENS HIGH category, NIS2 essential sectors or equivalents (identifiable health data, regulated banking, critical infrastructure). Here provider traceability and the processing contract are not negotiable; using a service whose operator is subject to the Cloud Act, FISA 702 or equivalent compromises the legal basis.&lt;/li>
&lt;li>A requirement for auditable inspection of the model, the guardrails and the whole pipeline. If a regulator asks &lt;em>&amp;ldquo;how exactly does it detect PII?&amp;rdquo;&lt;/em> and an answer that ends in open code is mandatory.&lt;/li>
&lt;li>Large volumes with stable workloads. Above a certain threshold of tokens per month, the cost of Bedrock / AOAI / Vertex diverges from the amortised cost of your own GPU cluster. The threshold depends on the workload, but it typically sits between 5 and 50 billion tokens per month for models in the Llama 70B range.&lt;/li>
&lt;li>Roadmap independence as a priority. The day the provider discontinues a model or raises the price by 40 %, the organisation has to be able to ignore it.&lt;/li>
&lt;/ul>
&lt;p>The &lt;strong>hyperscaler side wins by default&lt;/strong> when there is:&lt;/p>
&lt;ul>
&lt;li>Critical time-to-market, an MVP in weeks. The operational weight of the OSS stack is too much for a project that has not yet proved product-market fit.&lt;/li>
&lt;li>A small team with no SREs / MLEs specialised in GPU inference. Operating KServe + vLLM + KV cache + multi-tenancy is not trivial; if the team cannot sustain it, hosting is the way.&lt;/li>
&lt;li>Variable workloads / unpredictable spikes. Bedrock on-demand and SageMaker serverless charge for what you use; your own cluster pays for the GPU whether it is busy or not.&lt;/li>
&lt;li>A need for specific proprietary models (Claude, GPT-4.1, Gemini Pro) with no acceptable OSS equivalent for the case.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Mixed stages&lt;/strong> are frequent and reasonable. In practice, a common pattern in 2026 is: data, observe and retrain in self-hosted OSS (lineage and sovereignty), tune in OSS on your own cluster, eval in OSS + managed guardrails depending on the safety profile, deploy managed for proprietary models and self-hosted for open models. The question to ask for each stage is: &lt;em>&amp;ldquo;if the provider raises prices by 50 % or discontinues a component tomorrow, how much does it cost to move it?&amp;rdquo;&lt;/em>. The parallel catalogue in this post gives the answer for each box.&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>OpenAI / Anthropic API directly&lt;/strong> (not through Bedrock or AOAI): another level of managed, another contract.&lt;/li>
&lt;li>&lt;strong>Serious hybrids&lt;/strong>: AWS Outposts, GCP Distributed Cloud, Azure Stack HCI / Azure Local, the hyperscaler in your own room.&lt;/li>
&lt;li>&lt;strong>Per-tenant cost accounting&lt;/strong> compared OSS vs cloud: how the bill is built and where attribution breaks.&lt;/li>
&lt;li>&lt;strong>A real migration&lt;/strong> OSS → cloud or cloud → OSS: steps, scripts, gotchas.&lt;/li>
&lt;li>&lt;strong>Concrete European sovereignty&lt;/strong>: GAIA-X, EuroHPC, the European cloud offering (OVHcloud, Scaleway, IONOS, Aruba), compared with the big three for ENS / NIS2 cases.&lt;/li>
&lt;li>&lt;strong>AWS Inferentia / Trainium, GCP TPU v6 Trillium, Azure Maia&lt;/strong>: in-house chips and how they change the cost / token calculation.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&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 walk of a request through the six stages, the thread from which this post makes the vertical cut.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages: tool by tool&lt;/a> — the zoom into the open source side of this post&amp;rsquo;s master table: a card of about 150 words per core OSS tool (vLLM, Langfuse, DVC, Qdrant, Airflow, NeMo Guardrails, Presidio…), licence and governance, a decision matrix per stage and a diagram of the connected OSS stack.&lt;/li>
&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 that this catalogue complements.&lt;/li>
&lt;li>&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 why LLMOps is not classic MLOps.&lt;/li>
&lt;li>&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 Data cross-cutting piece.&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 deep-dive on the Prompt cross-cutting piece.&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> — Tune in detail.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a> — Eval in detail.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — the safety layer in detail.&lt;/li>
&lt;li>&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> — Deploy from the inside.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a> · &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM operators on K8s&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 operation.&lt;/li>
&lt;li>&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 in its layers.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop feedback → dataset → adapter&lt;/a> — Retrain in detail.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/">ISO/IEC 42001: the operations manual of the AI system&lt;/a> — the lock-in and contractual sovereignty analysis in this post feeds directly into Annex A.10 (third parties and customer relationships) of the AIMS; the AI supplier register demands documenting exactly what is compared here column by column.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.vllm.ai/">vLLM documentation&lt;/a> and &lt;a href="https://github.com/vllm-project/production-stack">vLLM Production Stack&lt;/a> — the reference OSS inference engine.&lt;/li>
&lt;li>&lt;a href="https://docs.aws.amazon.com/bedrock/">Amazon Bedrock documentation&lt;/a> — AWS managed model catalogue, Knowledge Bases, Guardrails and Prompt Management.&lt;/li>
&lt;li>&lt;a href="https://docs.aws.amazon.com/sagemaker/">Amazon SageMaker AI&lt;/a> — training, endpoints, pipelines, model monitoring.&lt;/li>
&lt;li>&lt;a href="https://cloud.google.com/vertex-ai/docs">Google Vertex AI documentation&lt;/a> — training, prediction, evaluation, model monitoring.&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/azure/ai-foundry/">Azure AI Foundry documentation&lt;/a> — Microsoft&amp;rsquo;s unified plane for AI applications.&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/azure/ai-services/openai/">Azure OpenAI Service documentation&lt;/a> — OpenAI models hosted on Azure.&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry GenAI semantic conventions&lt;/a> — the standard that stitches observability across the OSS / cloud boundary.&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs">Langfuse documentation&lt;/a> and &lt;a href="https://docs.arize.com/phoenix">Arize Phoenix&lt;/a> — the reference OSS LLM observability tools.&lt;/li>
&lt;li>&lt;a href="https://dvc.org/">DVC&lt;/a> and &lt;a href="https://lakefs.io/">lakeFS&lt;/a> — OSS data versioning.&lt;/li>
&lt;li>&lt;a href="https://docs.nvidia.com/nemo/guardrails/">NeMo Guardrails&lt;/a> — OSS safety + dialog policy.&lt;/li>
&lt;li>ENS (Esquema Nacional de Seguridad) and NIS2 (Network and Information Security Directive 2) — the compliance frameworks that have the last word in the OSS vs cloud choice for regulated EU customers.&lt;/li>
&lt;/ul></description></item><item><title>The six-stage LLMOps pipeline: global architecture and a deep dive into every component</title><link>https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/</link><pubDate>Thu, 21 May 2026 06:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The first two posts in the series established the &lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">LLMOps landscape&lt;/a> and went down into the detail of the &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">data pipeline with Kafka&lt;/a>. This post does the intermediate zoom: it draws &lt;strong>the complete map of the system&lt;/strong>, a global architecture of a modern LLMOps setup with all the pieces the field has settled on in 2026, and goes into depth on each of the &lt;strong>six canonical stages of the pipeline&lt;/strong>: &lt;strong>Data&lt;/strong>, &lt;strong>Tune&lt;/strong>, &lt;strong>Eval&lt;/strong>, &lt;strong>Deploy&lt;/strong>, &lt;strong>Observe&lt;/strong>, &lt;strong>Retrain&lt;/strong>. For each stage we give the &lt;strong>operational sub-tasks&lt;/strong>, the &lt;strong>dominant tools&lt;/strong>, the &lt;strong>design decisions&lt;/strong> that always show up, and the &lt;strong>specific traps&lt;/strong> that are seen repeatedly in production. And, most important operationally: every stage carries a &lt;strong>&amp;ldquo;you are here&amp;rdquo; mini-map&lt;/strong> over the cycle, which will be reused in any later post in the series to place the reader. The idea: that anyone reading a post about fine-tuning, about prompt versioning, about eval gates or about drift detection can look at the mini-map and know immediately which piece of the larger system they are thinking about that day.&lt;/p>
&lt;blockquote>
&lt;p>This is the &lt;strong>third post in the MLOps series specific to LLMs&lt;/strong>. Previous ones: &lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">Landscape 2026&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka&lt;/a>. Here we move from &amp;ldquo;the what&amp;rdquo; and &amp;ldquo;one piece&amp;rdquo; to &lt;strong>the whole map&lt;/strong>, with detail per stage.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-global-architecture-the-master-map">The global architecture: the master map&lt;/h2>
&lt;p>Before going down into each stage, let us fix the whole map. What follows is the reference drawing of a production LLMOps system in 2026, with every component the field has settled on in its place:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 580" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Global LLMOps architecture 2026">
&lt;style>.title{font:700 14px sans-serif;fill:#222}.stage-title{font:700 13px sans-serif;fill:#222}.lbl{font:11px sans-serif;fill:#333}.sm{font:10px sans-serif;fill:#555}.tiny{font:9px sans-serif;fill:#666}.stage{stroke:#444;stroke-width:1.5;rx:8}.data{fill:#ffe9d6}.tune{fill:#ffd6d6}.eval{fill:#d6eaff}.deploy{fill:#d9f5d6}.obs{fill:#e9d6f5}.retrain{fill:#fff5b0}.cross{fill:#f0f0f0;stroke:#888;stroke-dasharray:4 2;rx:6}.arr{stroke:#444;stroke-width:1.6;fill:none;marker-end:url(#ar)}.cycle{stroke:#888;stroke-width:1.4;fill:none;marker-end:url(#ar);stroke-dasharray:6 3}&lt;/style>
&lt;defs>&lt;marker id="ar" 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="#444"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="22" text-anchor="middle" class="title">Global LLMOps architecture 2026 — the six stages and the cross-cutting components&lt;/text>
&lt;rect x="20" y="50" width="240" height="170" class="stage data"/>
&lt;text x="140" y="72" text-anchor="middle" class="stage-title">1 · DATA&lt;/text>
&lt;text x="35" y="92" class="sm">• Sources: OLTP, APIs, logs, scraping&lt;/text>
&lt;text x="35" y="108" class="sm">• CDC: Debezium, Flink CDC&lt;/text>
&lt;text x="35" y="124" class="sm">• Transport: Kafka + Schema Registry&lt;/text>
&lt;text x="35" y="140" class="sm">• Stream proc: Flink SQL, RisingWave&lt;/text>
&lt;text x="35" y="156" class="sm">• Versioning: DVC + lakeFS&lt;/text>
&lt;text x="35" y="172" class="sm">• Tableflow → Iceberg/Delta&lt;/text>
&lt;text x="35" y="188" class="sm">• Vector stores: Milvus, Qdrant,&lt;/text>
&lt;text x="35" y="202" class="sm"> Weaviate, pgvector, LanceDB&lt;/text>
&lt;rect x="280" y="50" width="240" height="170" class="stage tune"/>
&lt;text x="400" y="72" text-anchor="middle" class="stage-title">2 · TUNE&lt;/text>
&lt;text x="295" y="92" class="sm">• Modes: fine-tune / RAG /&lt;/text>
&lt;text x="295" y="106" class="sm"> agent training&lt;/text>
&lt;text x="295" y="124" class="sm">• Frameworks: PEFT, Axolotl, TRL,&lt;/text>
&lt;text x="295" y="138" class="sm"> Unsloth, llama-factory&lt;/text>
&lt;text x="295" y="156" class="sm">• Techniques: LoRA, QLoRA, DPO, RLHF&lt;/text>
&lt;text x="295" y="172" class="sm">• Clusters: H100/B200 + NVLink&lt;/text>
&lt;text x="295" y="188" class="sm">• Experiment tracking: MLflow, W&amp;amp;B&lt;/text>
&lt;text x="295" y="202" class="sm">• Adapter registry: private HF Hub&lt;/text>
&lt;rect x="540" y="50" width="220" height="170" class="stage eval"/>
&lt;text x="650" y="72" text-anchor="middle" class="stage-title">3 · EVAL&lt;/text>
&lt;text x="555" y="92" class="sm">• CI frameworks: DeepEval,&lt;/text>
&lt;text x="555" y="106" class="sm"> Promptfoo, Ragas, OpenAI Evals&lt;/text>
&lt;text x="555" y="124" class="sm">• Platforms: Langfuse, LangSmith,&lt;/text>
&lt;text x="555" y="138" class="sm"> Phoenix, Braintrust&lt;/text>
&lt;text x="555" y="156" class="sm">• Judge LLM (G-Eval, Prometheus)&lt;/text>
&lt;text x="555" y="172" class="sm">• Versioned golden dataset&lt;/text>
&lt;text x="555" y="188" class="sm">• Eval gates in CI/CD&lt;/text>
&lt;text x="555" y="202" class="sm">• Calibration 85-90% vs human&lt;/text>
&lt;rect x="20" y="245" width="240" height="170" class="stage deploy"/>
&lt;text x="140" y="267" text-anchor="middle" class="stage-title">4 · DEPLOY&lt;/text>
&lt;text x="35" y="287" class="sm">• Model registry: MLflow, OME&lt;/text>
&lt;text x="35" y="303" class="sm">• Serving: vLLM, SGLang, TRT-LLM&lt;/text>
&lt;text x="35" y="319" class="sm">• K8s operators: vLLM Prod Stack,&lt;/text>
&lt;text x="35" y="333" class="sm"> KServe, OME, NVIDIA Dynamo, llm-d&lt;/text>
&lt;text x="35" y="349" class="sm">• Gateway / router: LiteLLM&lt;/text>
&lt;text x="35" y="365" class="sm">• Strategies: canary, blue-green,&lt;/text>
&lt;text x="35" y="379" class="sm"> shadow, A/B versioning&lt;/text>
&lt;text x="35" y="395" class="sm">• Autoscaling: KEDA + LLM metrics&lt;/text>
&lt;rect x="280" y="245" width="240" height="170" class="stage obs"/>
&lt;text x="400" y="267" text-anchor="middle" class="stage-title">5 · OBSERVE&lt;/text>
&lt;text x="295" y="287" class="sm">• Tracing: OpenLLMetry, Langfuse,&lt;/text>
&lt;text x="295" y="301" class="sm"> Phoenix, LangSmith&lt;/text>
&lt;text x="295" y="319" class="sm">• Metrics: Prometheus, Grafana&lt;/text>
&lt;text x="295" y="335" class="sm">• Guardrails: NeMo, Llama Guard 4,&lt;/text>
&lt;text x="295" y="349" class="sm"> LLM Guard, Lakera&lt;/text>
&lt;text x="295" y="367" class="sm">• eBPF: Hubble, Tetragon, AgentSight&lt;/text>
&lt;text x="295" y="383" class="sm">• MCP observability (OTel GenAI)&lt;/text>
&lt;text x="295" y="399" class="sm">• Drift: Evidently, NannyML, WhyLabs&lt;/text>
&lt;rect x="540" y="245" width="220" height="170" class="stage retrain"/>
&lt;text x="650" y="267" text-anchor="middle" class="stage-title">6 · RETRAIN&lt;/text>
&lt;text x="555" y="287" class="sm">• Explicit feedback (thumbs)&lt;/text>
&lt;text x="555" y="303" class="sm">• Implicit feedback (latency,&lt;/text>
&lt;text x="555" y="317" class="sm"> abandonment, retries)&lt;/text>
&lt;text x="555" y="335" class="sm">• Incident triaging&lt;/text>
&lt;text x="555" y="351" class="sm">• Dataset enrichment with cases&lt;/text>
&lt;text x="555" y="365" class="sm"> where the model failed&lt;/text>
&lt;text x="555" y="383" class="sm">• Cadence: quarterly or&lt;/text>
&lt;text x="555" y="397" class="sm"> incident-driven&lt;/text>
&lt;rect x="100" y="440" width="580" height="120" class="cross"/>
&lt;text x="390" y="462" text-anchor="middle" class="stage-title">Cross-cutting components (they run through every stage)&lt;/text>
&lt;text x="115" y="482" class="sm">• OpenTelemetry Collector (gen_ai.* and mcp.* semantic conventions)&lt;/text>
&lt;text x="115" y="498" class="sm">• Prompt versioning: Langfuse / MLflow Prompts (v1/v2/v3 versioning + labels + cache)&lt;/text>
&lt;text x="115" y="514" class="sm">• MCP servers + MCP Gateway (Traefik Hub, MintMCP) — tool-model interface&lt;/text>
&lt;text x="115" y="530" class="sm">• Model gateway: LiteLLM (100+ providers unified as one OpenAI-compatible API)&lt;/text>
&lt;text x="115" y="546" class="sm">• Schema Registry (Avro/Protobuf/JSON Schema) shared between data and serving&lt;/text>
&lt;path class="arr" d="M260,135 L280,135"/>
&lt;path class="arr" d="M520,135 L540,135"/>
&lt;path class="arr" d="M650,220 L650,245"/>
&lt;path class="arr" d="M540,330 L520,330"/>
&lt;path class="arr" d="M280,330 L260,330"/>
&lt;path class="arr" d="M140,415 L140,440"/>
&lt;path class="arr" d="M400,415 L400,440"/>
&lt;path class="arr" d="M650,415 L650,440"/>
&lt;path class="cycle" d="M650,330 C780,330 780,135 760,135 L760,135"/>
&lt;text x="745" y="245" class="sm" text-anchor="middle">cycle&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>What you see: the &lt;strong>six large boxes&lt;/strong> are the stages; the &lt;strong>solid arrows&lt;/strong> are the flow of the pipeline; the &lt;strong>dashed arrow&lt;/strong> that goes from &lt;strong>Retrain&lt;/strong> to &lt;strong>Data&lt;/strong> is the feedback cycle that turns LLMOps into a living process rather than a project that ends. The grey band at the foot holds the &lt;strong>cross-cutting components&lt;/strong> (observability, prompt versioning, MCP, gateway, schema) that run through every stage and connect to each one.&lt;/p>
&lt;p>Three quick readings of the map:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Horizontal, top&lt;/strong>: the happy path, &lt;strong>data → tune → eval&lt;/strong>. What happens while you prepare the model.&lt;/li>
&lt;li>&lt;strong>Horizontal, bottom&lt;/strong>: the service path, &lt;strong>deploy → observe → retrain&lt;/strong>. What happens once the model is alive.&lt;/li>
&lt;li>&lt;strong>Vertical&lt;/strong>: the connection between the two floors. Eval gateway feeds Deploy; Observe feeds Retrain; Retrain returns to Data.&lt;/li>
&lt;/ul>
&lt;p>From here on, every stage will include a &lt;strong>navigation mini-map&lt;/strong> (&amp;ldquo;you are here&amp;rdquo;) to place you in the complete cycle. Let us go through each one.&lt;/p>
&lt;h2 id="stage-1--data-ingestion-transport-versioning-indexing">Stage 1 — Data: ingestion, transport, versioning, indexing&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Data">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn)}&lt;/style>
&lt;defs>&lt;marker id="mn" 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 · ingestion → transport → versioning → indexing&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;/svg>
&lt;/div>
&lt;h3 id="operational-sub-tasks">Operational sub-tasks&lt;/h3>
&lt;p>The Data stage is the most underrated and the one that blocks the most projects. Its sub-tasks:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Ingestion&lt;/strong> from heterogeneous sources: OLTP databases (Postgres, MySQL), external APIs, file shares, scraping, SaaS systems, application logs, internal messaging.&lt;/li>
&lt;li>&lt;strong>Change capture&lt;/strong> (CDC) in streaming if the data is dynamic. Debezium over Kafka, Flink CDC, modern alternatives such as RisingWave that reads the WAL directly.&lt;/li>
&lt;li>&lt;strong>Transformation&lt;/strong> (cleansing, dedup, normalisation, PII sanitisation).&lt;/li>
&lt;li>&lt;strong>Schema management&lt;/strong>: schema registry, compatible evolution, backward/forward compatibility.&lt;/li>
&lt;li>&lt;strong>Versioning&lt;/strong> of training datasets and golden datasets: DVC + lakeFS (unified in November 2025). Covered in detail in the &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">dedicated data versioning post&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Indexing&lt;/strong> for RAG: chunking, embeddings, writing to vector stores. Covered in depth in the &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">Kafka post&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Materialisation&lt;/strong> into analytical tables: Tableflow → Iceberg/Delta, for BI consumption and low-latency queries.&lt;/li>
&lt;/ul>
&lt;h3 id="dominant-tools">Dominant tools&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Sub-task&lt;/th>
&lt;th>Tools 2026&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>CDC&lt;/td>
&lt;td>Debezium, Flink CDC, RisingWave&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Transport&lt;/td>
&lt;td>Kafka (Confluent Cloud, Redpanda, pure Apache)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Schema Registry&lt;/td>
&lt;td>Confluent Schema Registry, Apicurio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stream processing&lt;/td>
&lt;td>Apache Flink, RisingWave, Kafka Streams&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Data versioning&lt;/td>
&lt;td>DVC + lakeFS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vector stores&lt;/td>
&lt;td>Milvus, Qdrant, Weaviate, pgvector, LanceDB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Materialised tables&lt;/td>
&lt;td>Tableflow → Iceberg/Delta&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Batch ETL/ELT (where it applies)&lt;/td>
&lt;td>dbt + Snowflake/Databricks&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="design-decisions">Design decisions&lt;/h3>
&lt;p>The three decisions that always show up:&lt;/p>
&lt;p>&lt;strong>Batch vs streaming&lt;/strong>: the more dynamic the data, the more streaming. For static corpora (manuals that never change) a nightly batch is enough; for transactional data the agent needs to see minute by minute, streaming from day 1.&lt;/p>
&lt;p>&lt;strong>Embedding model&lt;/strong>: changing the embedding model invalidates every indexed vector. An architectural decision: pin the model and have an explicit migration plan (the dual-index pattern seen in the Kafka post).&lt;/p>
&lt;p>&lt;strong>Vector store&lt;/strong>: pgvector if you already run Postgres and you are below 10M vectors; Qdrant if you want mid-scale simplicity; Milvus if you need billions; Weaviate if you value native hybrid search.&lt;/p>
&lt;h3 id="traps">Traps&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Hardcoding connections to the source&lt;/strong> (with no abstraction): when the database changes (version, host, schema), you break the whole pipeline. An &lt;strong>adapter layer&lt;/strong> from day 1.&lt;/li>
&lt;li>&lt;strong>No schema registry&lt;/strong>: topics start breaking silently.&lt;/li>
&lt;li>&lt;strong>Full reindexing whenever something changes&lt;/strong>: it costs hours or days. Design the &lt;strong>dual-index pattern&lt;/strong> from the start.&lt;/li>
&lt;li>&lt;strong>Unsanitised PII&lt;/strong>: the RAG is serving sensitive data without meaning to. Anonymisation in the pipeline, not at consumption time.&lt;/li>
&lt;/ul>
&lt;h2 id="stage-2--tune-preparing-the-model-for-your-case">Stage 2 — Tune: preparing the model for your case&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Tune">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff7777;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn2)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn2)}&lt;/style>
&lt;defs>&lt;marker id="mn2" 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: TUNE · fine-tuning / RAG-as-tuning / agent training&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&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 active"/>&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;/svg>
&lt;/div>
&lt;h3 id="operational-sub-tasks-1">Operational sub-tasks&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Base model selection&lt;/strong>: Llama, Qwen, Mistral, Gemma, DeepSeek according to licence, size, quality in your domain.&lt;/li>
&lt;li>&lt;strong>Dataset preparation&lt;/strong>: train/val/test split, format (chat templates, JSONL), augmentation where it applies.&lt;/li>
&lt;li>&lt;strong>Adapter configuration&lt;/strong>: LoRA rank, target modules, alpha; QLoRA if you want to train on a consumer GPU; full fine-tune only if you have the budget.&lt;/li>
&lt;li>&lt;strong>Training loop&lt;/strong>: HuggingFace Transformers + PEFT + TRL as the canonical stack; Axolotl or llama-factory as convenience wrappers; Unsloth if you want 2-4× more speed on consumer GPUs.&lt;/li>
&lt;li>&lt;strong>Hyperparameter sweep&lt;/strong>: W&amp;amp;B Sweeps, Optuna, Ray Tune.&lt;/li>
&lt;li>&lt;strong>Checkpointing and resumability&lt;/strong>: save every N steps, resume from failure.&lt;/li>
&lt;li>&lt;strong>Promotion&lt;/strong>: the adapter is promoted to the registry after passing the next stage (Eval).&lt;/li>
&lt;/ul>
&lt;h3 id="the-three-modes-of-tune">The three modes of Tune&lt;/h3>
&lt;p>The detail of the table we saw in the &lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">landscape&lt;/a>:&lt;/p>
&lt;p>&lt;strong>Supervised fine-tuning (SFT)&lt;/strong> with LoRA/QLoRA. You collect (prompt, ideal-response) pairs and apply SFT with cross-entropy loss. The simplest option. The rule of thumb: &lt;strong>300-3,000&lt;/strong> well-curated examples are usually more useful than 50,000 noisy ones.&lt;/p>
&lt;p>&lt;strong>DPO (Direct Preference Optimization)&lt;/strong> and &lt;strong>RLAIF&lt;/strong>. Instead of &amp;ldquo;ideal-response&amp;rdquo;, you collect &lt;strong>(prompt, good_response, bad_response)&lt;/strong> triples and train the model to prefer the good one. More stable than classic RLHF, same objective. It is what most teams use when they go beyond SFT.&lt;/p>
&lt;p>&lt;strong>Agent training&lt;/strong> (RFT / Reinforcement Fine-Tuning, pure RLHF). For cases where the model needs to learn &lt;strong>multistep trajectories&lt;/strong>: when to choose tool A over B, when to ask for confirmation, how to decompose a large task. Far more expensive and complex. OpenAI&amp;rsquo;s work with RFT set the pattern in 2024-2025; in 2026 it is coming out of the experimental phase.&lt;/p>
&lt;p>&lt;strong>RAG as an alternative to Tune&lt;/strong>: although conceptually it is another stage (it lives in Data + Deploy), functionally it competes with fine-tuning for many cases. The 2026 verdict: &lt;strong>hybrid is the default&lt;/strong> (60% of deployments), fine-tune for behaviour plus RAG for volatile knowledge.&lt;/p>
&lt;h3 id="tools">Tools&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Aspect&lt;/th>
&lt;th>Tools 2026&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Base framework&lt;/td>
&lt;td>HuggingFace Transformers, PEFT, TRL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Convenience wrappers&lt;/td>
&lt;td>Axolotl, llama-factory&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Consumer speed&lt;/td>
&lt;td>Unsloth (2-4× speedup on RTX GPUs)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Distributed training&lt;/td>
&lt;td>DeepSpeed, FSDP, NeMo Framework&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Experiment tracking&lt;/td>
&lt;td>MLflow, W&amp;amp;B, ClearML&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Adapter registry&lt;/td>
&lt;td>Private HuggingFace Hub, MLflow registry&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hyperparameter&lt;/td>
&lt;td>W&amp;amp;B Sweeps, Optuna, Ray Tune&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="traps-1">Traps&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Catastrophic forgetting&lt;/strong>: very aggressive SFT destroys the model&amp;rsquo;s general capabilities. Keep a small % of the original dataset or use regularisation.&lt;/li>
&lt;li>&lt;strong>Overfitting to the golden dataset&lt;/strong>: the model learns to memorise the eval set. Keep a &lt;strong>holdout test set&lt;/strong> that nobody on the team looks at until the final release.&lt;/li>
&lt;li>&lt;strong>Train/serve skew&lt;/strong>: prompts in training with a different format from production. The &lt;strong>same chat template&lt;/strong> in both.&lt;/li>
&lt;li>&lt;strong>LoRA rank too high&lt;/strong>: it looks like it improves metrics but it inflates the adapter with no real benefit. Start with &lt;code>r=8&lt;/code> or &lt;code>r=16&lt;/code>; raise it only if there is evidence.&lt;/li>
&lt;/ul>
&lt;h2 id="stage-3--eval-validating-before-promoting">Stage 3 — Eval: validating before promoting&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Eval">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7aafff;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn3)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn3)}&lt;/style>
&lt;defs>&lt;marker id="mn3" 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: EVAL · CI gates + platform regression + human review&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&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 active"/>&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;/svg>
&lt;/div>
&lt;h3 id="operational-sub-tasks-2">Operational sub-tasks&lt;/h3>
&lt;p>Covered in depth in &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a>. A structured summary for the pipeline:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Golden dataset curation&lt;/strong>: 100-500 examples as a minimum, actively maintained with incident cases.&lt;/li>
&lt;li>&lt;strong>Evaluators&lt;/strong>: heuristic (regex, length), semantic (embeddings), LLM-as-judge (G-Eval), human (golden labels).&lt;/li>
&lt;li>&lt;strong>Running in CI&lt;/strong>: block the merge if critical metrics drop by more than X%.&lt;/li>
&lt;li>&lt;strong>Running on the platform&lt;/strong>: over sampled production traffic, persist results, detect long-term regression.&lt;/li>
&lt;li>&lt;strong>Judge calibration&lt;/strong>: 85-90% agreement with humans before accepting the judge as productive.&lt;/li>
&lt;li>&lt;strong>Eval gates&lt;/strong>: explicit thresholds per metric (faithfulness &amp;gt; 0.85, relevancy &amp;gt; 0.80, and so on).&lt;/li>
&lt;/ul>
&lt;h3 id="tools-1">Tools&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>CI gates&lt;/strong>: DeepEval (Apache 2.0, pytest-style), Promptfoo (MIT, CLI), Ragas (RAG-specific), Inspect AI (safety/capability).&lt;/li>
&lt;li>&lt;strong>Platform&lt;/strong>: Langfuse (MIT, complete suite), LangSmith (LangChain), Phoenix (ELv2, OTel), Braintrust.&lt;/li>
&lt;li>&lt;strong>Judges&lt;/strong>: GPT-4 (expensive but the reference), Claude 3.5 Sonnet, Prometheus (OSS, 0.897 correlation), JudgeLM.&lt;/li>
&lt;/ul>
&lt;h3 id="traps-2">Traps&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Aged golden dataset&lt;/strong>: if it is not updated, it stops reflecting production.&lt;/li>
&lt;li>&lt;strong>Contaminated judge&lt;/strong>: the judge knows the dataset (it appeared in its training).&lt;/li>
&lt;li>&lt;strong>Insufficient sample size&lt;/strong>: fewer than 50 examples makes differences look like noise.&lt;/li>
&lt;li>&lt;strong>Runaway costs&lt;/strong>: G-Eval with GPT-4 over many cases costs thousands of USD a month.&lt;/li>
&lt;li>&lt;strong>Forgetting the segment&lt;/strong>: an average of 0.85 can hide a 0.55 in German.&lt;/li>
&lt;/ul>
&lt;h2 id="stage-4--deploy-putting-the-model-into-production">Stage 4 — Deploy: putting the model into production&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7adb7a;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn4)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn4)}&lt;/style>
&lt;defs>&lt;marker id="mn4" 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: DEPLOY · operators + serving + canary + autoscaling&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&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 active"/>&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;/svg>
&lt;/div>
&lt;h3 id="operational-sub-tasks-3">Operational sub-tasks&lt;/h3>
&lt;p>Covered in depth in &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM operators on K8s&lt;/a>. A summary for the pipeline:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Runtime selection&lt;/strong>: vLLM (default), SGLang (agents with high prefix caching), TensorRT-LLM (pure latency), llama.cpp (edge).&lt;/li>
&lt;li>&lt;strong>Operator selection&lt;/strong>: vLLM Production Stack, KServe, OME (LMSYS), NVIDIA Dynamo, llm-d (CNCF).&lt;/li>
&lt;li>&lt;strong>Serving configuration&lt;/strong>: &lt;code>--tensor-parallel-size&lt;/code>, &lt;code>--kv-cache-dtype=fp8&lt;/code>, &lt;code>--enable-prefix-caching&lt;/code>, &lt;code>--enable-chunked-prefill&lt;/code>, &lt;code>--gpu-memory-utilization=0.92&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Routing between models&lt;/strong>: LiteLLM as the multi-provider abstraction.&lt;/li>
&lt;li>&lt;strong>Release strategy&lt;/strong>: canary (1% → 10% → 100%), blue-green (all or nothing with fast rollback), shadow (eval in parallel without affecting users).&lt;/li>
&lt;li>&lt;strong>Autoscaling with LLM metrics&lt;/strong>: KEDA + Prometheus over &lt;code>vllm:num_requests_waiting&lt;/code> or equivalent.&lt;/li>
&lt;li>&lt;strong>Gateway / Inference Extension&lt;/strong>: Gateway API Inference Extension once it is GA.&lt;/li>
&lt;/ul>
&lt;h3 id="dominant-tools-1">Dominant tools&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Serving engines&lt;/strong>: vLLM, SGLang, TensorRT-LLM, llama.cpp, MLX.&lt;/li>
&lt;li>&lt;strong>Operators&lt;/strong>: OME, vLLM Production Stack, NVIDIA Dynamo, llm-d, KServe.&lt;/li>
&lt;li>&lt;strong>Routing&lt;/strong>: LiteLLM (100+ providers), OpenRouter (managed), LangChain Router.&lt;/li>
&lt;li>&lt;strong>GPU primitives&lt;/strong>: NVIDIA GPU Operator, LeaderWorkerSet (LWS) for multi-pod tensor parallel, KEDA for autoscaling.&lt;/li>
&lt;/ul>
&lt;h3 id="traps-3">Traps&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Naïve rolling update&lt;/strong> that cuts sessions: &lt;code>maxUnavailable: 0, maxSurge: 1&lt;/code> and &lt;code>terminationGracePeriodSeconds: 120+&lt;/code>.&lt;/li>
&lt;li>&lt;strong>A short readiness probe&lt;/strong> that kills pods while they load: &lt;code>startupProbe&lt;/code> with &lt;code>failureThreshold: 60&lt;/code>.&lt;/li>
&lt;li>&lt;strong>HPA on CPU%&lt;/strong> with no LLM metrics: vLLM batches internally, one replica serves dozens. KEDA on queue depth.&lt;/li>
&lt;li>&lt;strong>Unquantised KV cache&lt;/strong>: &lt;code>--kv-cache-dtype=fp8&lt;/code> is almost always worth it.&lt;/li>
&lt;li>&lt;strong>Tensor parallel on GPUs without NVLink&lt;/strong>: all-reduce saturates PCIe and throughput collapses.&lt;/li>
&lt;/ul>
&lt;h2 id="stage-5--observe-seeing-what-happens-in-production">Stage 5 — Observe: seeing what happens in production&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#c47aff;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn5)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn5)}&lt;/style>
&lt;defs>&lt;marker id="mn5" 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: OBSERVE · tracing + metrics + guardrails + drift + eBPF&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&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 active"/>&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;/svg>
&lt;/div>
&lt;h3 id="operational-sub-tasks-4">Operational sub-tasks&lt;/h3>
&lt;p>This is the stage we have covered most deeply in previous series: the whole eBPF series (4 posts) and the whole post-tracing series (4 posts) deal with sub-tasks of Observe. A structured summary:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tracing&lt;/strong>: OpenLLMetry/Traceloop, Langfuse, Phoenix, LangSmith. Spans with OTel GenAI semantic conventions (&lt;code>gen_ai.*&lt;/code>, &lt;code>mcp.*&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Metrics&lt;/strong>: Prometheus + Grafana. TTFT, TPOT, throughput, queue depth, KV cache usage, cost per tool.&lt;/li>
&lt;li>&lt;strong>Active guardrails&lt;/strong> (not just eval): NeMo Guardrails with 5 types of rails, multimodal Llama Guard 4, Llama Prompt Guard 2 (86M/22M), LLM Guard.&lt;/li>
&lt;li>&lt;strong>eBPF observability&lt;/strong> (zero-instrumentation): Hubble (network), Tetragon (process/syscall), AgentSight (LLM agent with SSL uprobes + stdiocap MCP).&lt;/li>
&lt;li>&lt;strong>eBPF in the local engine&lt;/strong> (inference): ProfInfer-style with uprobes in llama.cpp / vLLM / libcudart.&lt;/li>
&lt;li>&lt;strong>Drift detection&lt;/strong>: Evidently AI, NannyML, WhyLabs. KS, PSI, MMD over embeddings.&lt;/li>
&lt;li>&lt;strong>MCP observability&lt;/strong>: OpenTelemetry GenAI MCP semantic conventions, trace propagation via &lt;code>params._meta&lt;/code>, a centralised MCP Gateway.&lt;/li>
&lt;/ul>
&lt;h3 id="the-four-mandatory-metrics">The four mandatory metrics&lt;/h3>
&lt;p>Out of everything covered, the four any minimal dashboard must have:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>TTFT p50/p95&lt;/strong> (time to first token) — what the user perceives.&lt;/li>
&lt;li>&lt;strong>TPOT p50/p95&lt;/strong> (time per output token) — streaming speed.&lt;/li>
&lt;li>&lt;strong>Throughput&lt;/strong> (aggregate tokens per second) — capacity planning.&lt;/li>
&lt;li>&lt;strong>Queue depth&lt;/strong> (&lt;code>vllm:num_requests_waiting&lt;/code>) — the leading indicator.&lt;/li>
&lt;/ol>
&lt;p>To these you add, per domain:&lt;/p>
&lt;ul>
&lt;li>For RAG: faithfulness rolling mean, retrieval hit rate.&lt;/li>
&lt;li>For agents: tool call accuracy, multi-step task completion.&lt;/li>
&lt;li>For multi-tenant: cost per tenant, p95 latency per tenant.&lt;/li>
&lt;/ul>
&lt;h3 id="traps-4">Traps&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Cardinality in Prometheus&lt;/strong>: metrics carrying every K8s label blow up.&lt;/li>
&lt;li>&lt;strong>Tracing with no sampling&lt;/strong>: storage grows out of control.&lt;/li>
&lt;li>&lt;strong>Guardrails permanently in monitoring mode&lt;/strong>: they never reach enforce.&lt;/li>
&lt;li>&lt;strong>Drift with no alerts&lt;/strong>: you spot drift on the dashboard once a month; meanwhile the problem has been running for weeks.&lt;/li>
&lt;li>&lt;strong>OTel with no propagation&lt;/strong>: MCP, Tetragon and AgentSight spans left disconnected.&lt;/li>
&lt;/ul>
&lt;h2 id="stage-6--retrain-closing-the-loop">Stage 6 — Retrain: closing the loop&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Retrain">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ffd24a;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn6)}.cyc{stroke:#c66;stroke-width:2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn6)}&lt;/style>
&lt;defs>&lt;marker id="mn6" 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="#c66"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: RETRAIN · closing the loop back to DATA&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&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 active"/>&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;/svg>
&lt;/div>
&lt;h3 id="operational-sub-tasks-5">Operational sub-tasks&lt;/h3>
&lt;p>This is the stage most neglected in GenAI projects. Closing the loop turns LLMOps into a living practice; not closing it leaves it as a project that ages.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Explicit feedback&lt;/strong>: thumbs up/down in the UI, annotations by power users, forms for &amp;ldquo;what went wrong&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Implicit feedback&lt;/strong>: anomalous latency, abandonment rate, user retries, aborted sessions.&lt;/li>
&lt;li>&lt;strong>Incident triaging&lt;/strong>: classify incidents by root cause (model issue, retrieval issue, prompt issue, infra issue).&lt;/li>
&lt;li>&lt;strong>Dataset enrichment&lt;/strong>: add to the golden dataset the cases where the system failed, with the correct answer labelled by a human.&lt;/li>
&lt;li>&lt;strong>Retrain cadence&lt;/strong>: quarterly by default, &lt;strong>incident-driven&lt;/strong> when a problematic pattern goes over a threshold.&lt;/li>
&lt;li>&lt;strong>Promotion&lt;/strong>: the new model/adapter goes through the Tune → Eval → Deploy stages, with eval gates that compare against the model in production.&lt;/li>
&lt;/ul>
&lt;h3 id="the-two-cadences">The two cadences&lt;/h3>
&lt;p>&lt;strong>Scheduled retrain&lt;/strong> (quarterly or half-yearly): an established process. It lets you plan capacity, budget and risk. The default.&lt;/p>
&lt;p>&lt;strong>Incident-driven retrain&lt;/strong>: when a serious incident (detected drift, a failing segment, a prompt injection attack) crosses a threshold, a mini-cycle fires. More expensive but necessary for critical cases.&lt;/p>
&lt;h3 id="dominant-tools-2">Dominant tools&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Annotation and feedback collection&lt;/strong>: Langfuse (built-in UI), Argilla (OSS), Label Studio.&lt;/li>
&lt;li>&lt;strong>Dataset enrichment&lt;/strong>: pipelines in Airflow or Argo Workflows.&lt;/li>
&lt;li>&lt;strong>Triaging&lt;/strong>: Langfuse dashboards with filters on traces with a low eval.&lt;/li>
&lt;li>&lt;strong>Promoting candidates&lt;/strong>: MLflow model registry stages.&lt;/li>
&lt;/ul>
&lt;h3 id="traps-5">Traps&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>An open loop&lt;/strong>: production does not inform the dataset; the model never improves.&lt;/li>
&lt;li>&lt;strong>Human feedback gets lost&lt;/strong>: thumbs down with no structured capture channel.&lt;/li>
&lt;li>&lt;strong>Undefined cadence&lt;/strong>: &amp;ldquo;we retrain when it is needed&amp;rdquo; → nobody ever retrains.&lt;/li>
&lt;li>&lt;strong>No holdout test set&lt;/strong>: the golden dataset is enriched with the same cases used to evaluate; eval measures memorisation.&lt;/li>
&lt;li>&lt;strong>Promotion with no gates&lt;/strong>: the new model enters production without passing the checks the previous models passed.&lt;/li>
&lt;/ul>
&lt;h2 id="the-complete-cycle-how-the-stages-fit-together">The complete cycle: how the stages fit together&lt;/h2>
&lt;p>Now that we have seen each stage separately, the key insight is &lt;strong>how they hook into each other&lt;/strong>. Five emergent properties of the cycle:&lt;/p>
&lt;p>&lt;strong>1. Data is the raw material of every stage&lt;/strong>. Tune reads from the golden dataset. Eval reads from the eval dataset. Deploy reads from the RAG (vector store). Observe produces new data. Retrain creates new datasets. &lt;strong>The Kafka log is the gospel of the whole system&lt;/strong> (post 2 in the series).&lt;/p>
&lt;p>&lt;strong>2. Eval is the bidirectional gatekeeper&lt;/strong>. Before Deploy: it blocks the release if the model regresses. After Observe: it feeds Retrain by identifying the worst-scoring cases. The quality of the eval determines the quality of the whole cycle.&lt;/p>
&lt;p>&lt;strong>3. Observe feeds Retrain and Eval simultaneously&lt;/strong>. Traces produce metrics for Observe; problematic traces are annotated and go to the dataset; the new cases enrich the eval golden set. &lt;strong>Observe is the operational source of truth&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>4. The cross-cutting components (the grey band on the map) are not a stage, they are an infrastructure&lt;/strong>. OpenTelemetry, prompt versioning, MCP gateway, model gateway, schema registry. Badly configured, every stage suffers separately. Well configured, the stages integrate without friction.&lt;/p>
&lt;p>&lt;strong>5. The cycle is not strictly sequential, it is concurrent&lt;/strong>. At any given moment the system has: requests being served (Deploy + Observe), a new version in training (Tune), continuous eval in CI (Eval), data arriving from CDC (Data), incident analysis (Retrain). &lt;strong>Every stage is alive at once&lt;/strong>.&lt;/p>
&lt;h2 id="cross-stage-traps-things-that-break-the-whole-system">Cross-stage traps: things that break the whole system&lt;/h2>
&lt;p>There are errors that do not belong to a stage but to the interfaces between stages. The most common ones:&lt;/p>
&lt;h3 id="trainserve-skew">Train/serve skew&lt;/h3>
&lt;p>The exact format of the prompt in training is different from the one in production. The result: the model trained to answer &lt;code>&amp;lt;|im_start|&amp;gt;user\n...\n&amp;lt;|im_end|&amp;gt;&lt;/code> receives &lt;code>User: ...\nAssistant:&lt;/code> in production and performs worse. &lt;strong>Solution&lt;/strong>: extract the chat template into a shared library used by the Tune pipeline &lt;strong>and&lt;/strong> the Deploy one.&lt;/p>
&lt;h3 id="an-eval-that-does-not-reflect-production">An eval that does not reflect production&lt;/h3>
&lt;p>Your golden dataset is careful questions; production is real questions with typos, mixed languages, and so on. Eval passes at 95%, production performs at 70%. &lt;strong>Solution&lt;/strong>: continuously enrich the golden set with real samples.&lt;/p>
&lt;h3 id="drift-with-no-response-pipeline">Drift with no response pipeline&lt;/h3>
&lt;p>You spot drift on the Observe dashboard; nobody has a defined workflow for what to do. &lt;strong>Solution&lt;/strong>: every drift alert must have a clear runbook: investigate, classify, act (retrain, adjust the prompt, widen the retrieval).&lt;/p>
&lt;h3 id="schema-break-cascade">Schema break cascade&lt;/h3>
&lt;p>You change the schema at the OLTP source; Debezium reflects it; the Flink job breaks; the embedded topic stops updating; the vector store ages; the RAG answers over old data. Three stages affected by one change in Data. &lt;strong>Solution&lt;/strong>: mandatory &lt;strong>backward-compatible&lt;/strong> schema evolution, contracts between producers and consumers.&lt;/p>
&lt;h3 id="no-observability-of-the-pipeline-itself">No observability of the pipeline itself&lt;/h3>
&lt;p>The LLMOps pipeline is a complex system. If it has no observability of its own (how long training takes, how many jobs fail, how many re-embeddings run), debugging failures is an exercise in spelunking. &lt;strong>Solution&lt;/strong>: OTel over the pipeline itself, not just over the LLM calls.&lt;/p>
&lt;h3 id="invisible-vendor-lock-in">Invisible vendor lock-in&lt;/h3>
&lt;p>Pipelines written against LangChain, prompts stuck in LangSmith, embeddings in Pinecone, the model in OpenAI. Migrating is a months-long project. &lt;strong>Solution&lt;/strong>: LiteLLM and OpenLLMetry abstractions, vendor-neutral from the start.&lt;/p>
&lt;h2 id="what-comes-in-the-following-posts">What comes in the following posts&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Post 4 — &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">PostgreSQL + Qdrant in the ingestion stage&lt;/a>&lt;/strong> — the first post that applies the &amp;ldquo;you are here&amp;rdquo; pattern over the Data stage. Synchronisation patterns (outbox + CDC), microservices architecture, deployment manifests.&lt;/li>
&lt;li>&lt;strong>Upcoming posts&lt;/strong> — still to be decided: the cluster as a multi-tenant platform, Constitutional AI / runtime alignment, continuous fine-tuning in depth, edge LLMs.&lt;/li>
&lt;li>In any later post in this or future series, the &lt;strong>&amp;ldquo;you are here&amp;rdquo; mini-map&lt;/strong> will tell you which stage of the cycle the topic fits into. If you read a post about quantization, you will know you are in Deploy. If you read one about evaluator ensembles, you will know you are in Eval. If you read one about RAG over Iceberg, you will know you are in Data.&lt;/li>
&lt;li>If you want to see &lt;strong>the whole pipeline in action following a single real request&lt;/strong>, the synthesis post &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> does exactly that: it rewinds a request back to the data that trained it 90 days earlier and follows it forward to the feedback that will reappear in the next Retrain cycle, crossing the six stages and the two cross-cutting components in one coherent story.&lt;/li>
&lt;li>If what interests you is &lt;strong>comparing how each stage is built in open source against the hyperscalers&lt;/strong>, the post &lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">The parallel catalogue: the six LLMOps stages in OSS and in AWS / GCP / Azure&lt;/a> makes the vertical cut: for every stage, which tools the blog&amp;rsquo;s reference OSS stack uses and what the cloud equivalents are, with summary tables, gap identification and the multi-tenant chatbot ported to an AWS stack as a concrete example.&lt;/li>
&lt;li>If you want the &lt;strong>OSS toolbox piece by piece&lt;/strong>, the post &lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages: entry by entry&lt;/a> does the zoom in: ~150 words of description per core tool (what it does, how it differs from its alternatives, licence and governance, typical gotcha), a decision matrix per stage, a diagram of the connected OSS stack and a master table of licences and EE / SaaS offerings.&lt;/li>
&lt;li>If what you are missing is &lt;strong>the common vocabulary that runs through the six stages&lt;/strong>, the post &lt;a href="https://blog.lo0.es/en/posts/ontologies-knowledge-graphs-llmops-linnaean-nomenclature/">Ontologies and knowledge graphs in LLMOps&lt;/a> walks the six stages from the perspective of shared formal nomenclature: how TBox + ABox + SHACL + SKOS change the operation of Data, Train/Adapt, Eval, Deploy, Observe and Govern, the GraphRAG 2026 landscape (Microsoft GraphRAG v2, LightRAG, HippoRAG 2, KAG/OpenSPG), vertical ontologies actually deployed (FIBO, SNOMED CT, schema.org, ENS, EU AI Act) and an open source stack with its licensing caveats (Neo4j Community GPLv3, KuzuDB upstream archived in October 2025).&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;p>Foundations:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/@sanjeebmeister/the-complete-mlops-llmops-roadmap-for-2026-building-production-grade-ai-systems-bdcca5ed2771">The Complete MLOps/LLMOps Roadmap for 2026 (Sanjeeb Panda)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://hyscaler.com/insights/mlops-in-2026-guide/">MLOps in 2026: Architecture, Trends &amp;amp; Strategy (Hyscaler)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Per stage (entries from the blog series):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Data&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka — technical architecture&lt;/a> and the &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">dedicated post on data versioning with DVC and lakeFS&lt;/a> — the four artefacts to version separately (training, RAG corpus, golden eval, enriched retrain), schema contracts, end-to-end lineage from dataset to trace, and why a golden set without a strict holdout measures memorisation.&lt;/li>
&lt;li>&lt;strong>Tune&lt;/strong>: partly covered in &lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">Landscape 2026&lt;/a>; a deeper treatment in post 4 if continuous fine-tuning is chosen.&lt;/li>
&lt;li>&lt;strong>Eval&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Deploy&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM operators on K8s&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Observe&lt;/strong>: the whole eBPF series and the whole post-tracing series.&lt;/li>
&lt;li>&lt;strong>Retrain&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">the dedicated post on how to close the loop&lt;/a> — feedback capture (explicit + implicit), triage by root cause, dataset enrichment with human annotation (Argilla / Label Studio), scheduled vs incident-driven cadences, governed promotion with eval gates.&lt;/li>
&lt;/ul>
&lt;p>Cross-cutting components:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Prompt versioning&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">the dedicated post with Langfuse and MLflow Prompts&lt;/a> — the three-primitive pattern (immutable version, mutable label, cache), eval gates on promotion, and per-request traceability.&lt;/li>
&lt;li>&lt;strong>MCP&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">deep MCP observability&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Drift detection&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">eBPF + drift detection&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Local inference&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention deep dive&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Frameworks and tools referenced:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://mlflow.org/">MLflow&lt;/a>, &lt;a href="https://wandb.ai/">W&amp;amp;B&lt;/a>, &lt;a href="https://www.kubeflow.org/">Kubeflow&lt;/a>, &lt;a href="https://www.zenml.io/">ZenML&lt;/a>, &lt;a href="https://www.bentoml.com/">BentoML&lt;/a>, &lt;a href="https://metaflow.org/">Metaflow&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/huggingface/peft">HuggingFace PEFT&lt;/a>, &lt;a href="https://github.com/huggingface/trl">TRL&lt;/a>, &lt;a href="https://github.com/axolotl-ai-cloud/axolotl">Axolotl&lt;/a>, &lt;a href="https://github.com/unslothai/unsloth">Unsloth&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dvc.org/">DVC&lt;/a> + &lt;a href="https://lakefs.io/">lakeFS&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/">Langfuse&lt;/a>, &lt;a href="https://www.evidentlyai.com/">Evidently AI&lt;/a>, &lt;a href="https://phoenix.arize.com/">Phoenix&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/vllm-project/production-stack">vLLM Production Stack&lt;/a>, &lt;a href="https://kserve.github.io/website/">KServe&lt;/a>, &lt;a href="https://github.com/ome-projects/ome">OME&lt;/a>, &lt;a href="https://developer.nvidia.com/dynamo">NVIDIA Dynamo&lt;/a>, &lt;a href="https://github.com/llm-d/llm-d">llm-d&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>