<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Production on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/production/</link><description>Recent content in Production 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/production/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></channel></rss>