<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Helm on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/helm/</link><description>Recent content in Helm on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 16 Jun 2026 03:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/helm/index.xml" rel="self" type="application/rss+xml"/><item><title>LLM quality benchmarks: the contamination trap and the OSS tools for not fooling yourself</title><link>https://blog.lo0.es/en/posts/llm-quality-benchmarks-contamination-trap-oss-tools/</link><pubDate>Tue, 16 Jun 2026 03:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/llm-quality-benchmarks-contamination-trap-oss-tools/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a full stop. The dollar sign is not used
(on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>Post &lt;strong>B7&lt;/strong> of the benchmarking track. The &lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">B1 introduction&lt;/a>
established the distinction between the three axes (performance, quality, energy). The &lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">B2 tool catalogue&lt;/a>
covered the performance axis (TTFT, ITL, goodput). This article covers the &lt;strong>quality&lt;/strong> axis:
what each benchmark measures, how it is run, and why the leaderboard numbers do not always
say what they appear to say. The &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">LLMOps eval pipeline&lt;/a>
and &lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge&lt;/a> cover the system&amp;rsquo;s internal
evaluation layer; this article covers the evaluation of the &lt;strong>base model&lt;/strong> with standard
academic benchmarks.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>An inference engine with an 80 ms TTFT serving a low-quality model is worth nothing: the platform&amp;rsquo;s Pareto front has two independent axes. In 2025–2026, the most widely used static benchmarks (MMLU, HumanEval) have crossed the saturation threshold (HumanEval &amp;gt; 93% in frontier models, MMLU-Pro &amp;gt; 75% in the top-5), which invalidates their ability to discriminate at the high end. The main cause is not that the models are better: it is data contamination, with 18% n-gram overlap detected in Llama-2-70B against evaluation datasets. The OSS tools active in 2026 are &lt;strong>lm-evaluation-harness&lt;/strong> (EleutherAI, MIT, 60+ tasks, the ecosystem&amp;rsquo;s de facto engine), &lt;strong>HELM&lt;/strong> (Stanford, Apache 2.0, in maintenance since June 2026), &lt;strong>LiveBench&lt;/strong> (MIT, 1,000 questions refreshed monthly to avoid contamination), &lt;strong>lighteval&lt;/strong> (Hugging Face, MIT, 1,000+ tasks, vLLM support), and &lt;strong>LMArena/Chatbot Arena&lt;/strong> (Bradley-Terry over millions of human comparisons). OpenLLM Leaderboard v2 was archived in 2025 and replaced by the &lt;strong>OpenEvals&lt;/strong> ecosystem (200+ specialised leaderboards). Reproducing an eval number requires pinning: harness version, model version, prompt template version and number of shots.&lt;/p>
&lt;hr>
&lt;h2 id="quality--performance-the-independent-pareto-axis">Quality ≠ performance: the independent Pareto axis&lt;/h2>
&lt;p>The performance axis (TTFT, ITL, throughput) measures &lt;strong>how long&lt;/strong> the engine takes to produce tokens.
The quality axis measures &lt;strong>how correct&lt;/strong> those tokens are. They are orthogonal: a small model
served with vLLM at 3,000 tok/s can score 42% on MMLU-Pro; a frontier model may need
4× more GPU and score 72%. The product throughput × quality is the only number that matters for
a platform in production.&lt;/p>
&lt;div class="diagram" style="max-width:700px;margin:1rem auto;">
&lt;svg viewBox="0 0 700 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pareto space of quality versus performance with four quadrants">
&lt;style>
.ax{fill:none;stroke:currentColor;stroke-width:1.4}
.tl{font:600 13px sans-serif;fill:currentColor}
.ts{font:11px sans-serif;fill:currentColor}
.dot{stroke:currentColor;stroke-width:1.2}
.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#arh)}
&lt;/style>
&lt;defs>
&lt;marker id="arh" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>
&lt;/marker>
&lt;/defs>
&lt;line class="ax" x1="60" y1="260" x2="660" y2="260"/>
&lt;line class="ax" x1="60" y1="260" x2="60" y2="30"/>
&lt;path class="ar" d="M650,260 L665,260"/>
&lt;path class="ar" d="M60,40 L60,25"/>
&lt;text x="670" y="264" class="ts">Performance (tok/s)&lt;/text>
&lt;text x="65" y="22" class="ts">Quality (MMLU-Pro %)&lt;/text>
&lt;text x="62" y="258" class="ts">0&lt;/text>
&lt;text x="55" y="155" class="ts" text-anchor="end">50&lt;/text>
&lt;text x="55" y="80" class="ts" text-anchor="end">72&lt;/text>
&lt;text x="200" y="270" class="ts">1000&lt;/text>
&lt;text x="460" y="270" class="ts">3000&lt;/text>
&lt;line class="ax" x1="58" y1="155" x2="62" y2="155"/>
&lt;line class="ax" x1="58" y1="80" x2="62" y2="80"/>
&lt;line class="ax" x1="200" y1="258" x2="200" y2="262"/>
&lt;line class="ax" x1="460" y1="258" x2="460" y2="262"/>
&lt;circle class="dot" cx="200" cy="155" r="10" fill="none"/>
&lt;text x="212" y="150" class="ts">Small fast model&lt;/text>
&lt;text x="212" y="163" class="ts">(42% / 3,000 tok/s) — useless if the task demands it&lt;/text>
&lt;circle class="dot" cx="460" cy="80" r="10" fill="none"/>
&lt;text x="472" y="75" class="ts">Optimised frontier model&lt;/text>
&lt;text x="472" y="88" class="ts">(72% / 3,000 tok/s) — Pareto zone&lt;/text>
&lt;circle class="dot" cx="180" cy="80" r="10" fill="none"/>
&lt;text x="192" y="75" class="ts">Slow frontier&lt;/text>
&lt;text x="192" y="88" class="ts">(72% / 800 tok/s)&lt;/text>
&lt;circle class="dot" cx="500" cy="200" r="10" fill="none"/>
&lt;text x="512" y="195" class="ts">Fast engine, bad model&lt;/text>
&lt;text x="512" y="208" class="ts">(35% / 4,000 tok/s) — unusable&lt;/text>
&lt;text x="330" y="300" class="tl" text-anchor="middle">Useful zone: quality ≥ task threshold ∧ performance ≥ production SLO&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The operational consequence: select model and engine &lt;strong>first by quality&lt;/strong> (does it clear the minimum threshold for the task?) and &lt;strong>then by performance&lt;/strong> (does it meet the SLO within the GPU budget?). Inverting the order, picking the fastest engine and fitting the model into it, is the most frequent mistake in initial sizing.&lt;/p>
&lt;hr>
&lt;h2 id="reference-benchmarks-what-each-one-measures">Reference benchmarks: what each one measures&lt;/h2>
&lt;p>The table below collects the standard academic benchmarks in 2026, the capability each one measures, its test set size and the current saturation level in frontier models.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Benchmark&lt;/th>
&lt;th>Capability measured&lt;/th>
&lt;th>Format&lt;/th>
&lt;th>Test set&lt;/th>
&lt;th>Frontier saturation 2026&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>MMLU&lt;/strong>&lt;/td>
&lt;td>Multidisciplinary knowledge (57 subjects)&lt;/td>
&lt;td>4-choice MCQ&lt;/td>
&lt;td>14,079&lt;/td>
&lt;td>High: top-5 &amp;gt; 88%; insufficient separation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MMLU-Pro&lt;/strong>&lt;/td>
&lt;td>MMLU with 10 options, multistep reasoning&lt;/td>
&lt;td>10-choice MCQ&lt;/td>
&lt;td>12,032&lt;/td>
&lt;td>Medium: top-5 between 72–77%; still separates&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GSM8K&lt;/strong>&lt;/td>
&lt;td>Primary-school maths reasoning (word problems)&lt;/td>
&lt;td>answer generation&lt;/td>
&lt;td>1,319&lt;/td>
&lt;td>Very high: top models &amp;gt; 97%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MATH&lt;/strong>&lt;/td>
&lt;td>Competition maths (algebra, geometry, calculus, etc.)&lt;/td>
&lt;td>LaTeX generation&lt;/td>
&lt;td>5,000&lt;/td>
&lt;td>Medium-high: top models 85–92%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GPQA Diamond&lt;/strong>&lt;/td>
&lt;td>PhD-level scientific reasoning (biology, physics, chemistry)&lt;/td>
&lt;td>4-choice MCQ&lt;/td>
&lt;td>198&lt;/td>
&lt;td>Emerging: top &amp;gt; 90% in 2026; still separates in 60–90%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>BBH&lt;/strong> (BIG-Bench Hard)&lt;/td>
&lt;td>23 tasks where earlier models fell below human level&lt;/td>
&lt;td>generation&lt;/td>
&lt;td>6,511&lt;/td>
&lt;td>Medium: useful for complex reasoning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>IFEval&lt;/strong>&lt;/td>
&lt;td>Verifiable instruction following (format, length, language)&lt;/td>
&lt;td>generation + rules&lt;/td>
&lt;td>541&lt;/td>
&lt;td>Low: notable separation between models&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>HumanEval&lt;/strong>&lt;/td>
&lt;td>Python code generation (164 problems with signature)&lt;/td>
&lt;td>generation + execution&lt;/td>
&lt;td>164&lt;/td>
&lt;td>Very high: top &amp;gt; 93%; saturated&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MBPP&lt;/strong>&lt;/td>
&lt;td>Code generation (500 problems without a function signature)&lt;/td>
&lt;td>generation + execution&lt;/td>
&lt;td>500&lt;/td>
&lt;td>High: top models 85–92%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MuSR&lt;/strong>&lt;/td>
&lt;td>Multistep reasoning over long texts&lt;/td>
&lt;td>generation&lt;/td>
&lt;td>~1,000&lt;/td>
&lt;td>Low: useful for extended reasoning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TruthfulQA&lt;/strong>&lt;/td>
&lt;td>Truthfulness: resistance to asserting popular myths&lt;/td>
&lt;td>generation + MCQ&lt;/td>
&lt;td>817&lt;/td>
&lt;td>Medium: visible separation between families&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Note on saturation:&lt;/strong> a saturated benchmark does not distinguish models at the high end; it is only valuable as a &lt;em>floor check&lt;/em> (confirming the model has not regressed to the level of three years ago). For the frontier zone, the useful benchmarks in 2026 are MMLU-Pro, GPQA Diamond, IFEval and MuSR.&lt;/p>
&lt;hr>
&lt;h2 id="oss-evaluation-tools-data-sheets">OSS evaluation tools: data sheets&lt;/h2>
&lt;h3 id="lm-evaluation-harness-eleutherai">lm-evaluation-harness (EleutherAI)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/EleutherAI/lm-evaluation-harness&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>MIT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer&lt;/td>
&lt;td>EleutherAI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Active version (Jun 2026)&lt;/td>
&lt;td>0.4.x (&lt;code>main&lt;/code> branch)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Available tasks&lt;/td>
&lt;td>60+ benchmarks, hundreds of subtasks and variants&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported backends&lt;/td>
&lt;td>Hugging Face Transformers, vLLM, litellm, OpenAI-compatible API, GGUF&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Task format&lt;/td>
&lt;td>YAML + Python; each task defines dataset, template, metric and number of shots&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What it measures&lt;/td>
&lt;td>MCQ accuracy, exact match, pass@k on code, perplexity, BLEU/ROUGE&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>How it is invoked&lt;/td>
&lt;td>&lt;code>lm_eval --model hf --model_args pretrained=&amp;lt;model&amp;gt; --tasks mmlu,gsm8k --num_fewshot 5 --output_path results/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Output&lt;/td>
&lt;td>JSON with per-task metrics + configuration metadata&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Use in the ecosystem&lt;/td>
&lt;td>Official engine of the OpenLLM Leaderboard (v1 and v2); de facto reference in research&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The harness is the industry standard for reproducible evaluation. Its strength is the breadth of tasks and the normalisation of the prompt format. Its limitation: the tasks are static, the datasets do not change between harness versions, which exposes it to the contamination problem if the model was trained on the same data.&lt;/p>
&lt;p>Full invocation with vLLM as the backend:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">lm_eval &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model_args &lt;span class="nv">pretrained&lt;/span>&lt;span class="o">=&lt;/span>meta-llama/Llama-3.1-70B-Instruct,dtype&lt;span class="o">=&lt;/span>bfloat16 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --tasks mmlu_pro,gpqa_diamond,ifeval,humaneval &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num_fewshot &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --batch_size auto &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --output_path ./eval_results/ &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --log_samples
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="helm-stanford-crfm">HELM (Stanford CRFM)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/stanford-crfm/helm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Official site&lt;/td>
&lt;td>crfm.stanford.edu/helm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer&lt;/td>
&lt;td>Stanford CRFM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Status (Jun 2026)&lt;/td>
&lt;td>&lt;strong>Maintenance mode&lt;/strong> since 1 June 2026&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What it measures&lt;/td>
&lt;td>Accuracy, calibration, robustness, fairness, toxicity, efficiency (latency/cost)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Task format&lt;/td>
&lt;td>Python + YAML; scenarios with multiple simultaneous metrics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported backends&lt;/td>
&lt;td>API (OpenAI, Anthropic, Cohere, etc.) + local models via Hugging Face&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Active variants&lt;/td>
&lt;td>HELM Classic, HELM Capabilities, HELM Lite, MedHELM, HELM Safety&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Installation&lt;/td>
&lt;td>&lt;code>pip install crfm-helm&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>How it is invoked&lt;/td>
&lt;td>&lt;code>helm-run --conf-paths run_specs.conf --suite v1 --max-eval-instances 1000&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Output&lt;/td>
&lt;td>JSON + web UI with a reproducible leaderboard and per-prompt transparency&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>HELM is the tool that introduced &lt;strong>holistic&lt;/strong> evaluation (multiple metrics over the same scenario instead of a single number). In 2026, with HELM in maintenance mode, the ecosystem has fragmented into specialised leaderboards; but HELM remains the methodological reference for evaluations that need fairness and toxicity as well as accuracy.&lt;/p>
&lt;p>&lt;strong>HELM Capabilities&lt;/strong> (published March 2025): an active leaderboard with full prompt transparency, reproducible with the HELM framework. Available at crfm.stanford.edu/helm/capabilities.&lt;/p>
&lt;hr>
&lt;h3 id="livebench">LiveBench&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/LiveBench/LiveBench&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Official site&lt;/td>
&lt;td>livebench.ai&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>MIT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer&lt;/td>
&lt;td>NYU, UT Austin, UC Santa Barbara&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Paper&lt;/td>
&lt;td>arXiv:2406.19314&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Size&lt;/td>
&lt;td>1,000 active questions (monthly refresh, same total size)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Categories&lt;/td>
&lt;td>6: math, coding, reasoning, language, instruction following, data analysis&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tasks&lt;/td>
&lt;td>18 subtasks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What it measures&lt;/td>
&lt;td>General capabilities over questions with objectively verifiable answers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Anti-contamination&lt;/td>
&lt;td>Questions generated from recent sources: arXiv papers, news, IMDb, new datasets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scoring&lt;/td>
&lt;td>No LLM-as-judge: automatically verifiable answer (exact ground truth)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cadence&lt;/td>
&lt;td>New questions monthly; old ones retired to avoid accumulated memorisation&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>LiveBench is the architectural answer to the contamination problem: if the benchmark changes every month with questions based on sources published &lt;strong>after&lt;/strong> the cutoff of the model under evaluation, memorisation of the training set cannot inflate the result. The limitation is size (1,000 questions) and that it does not cover every domain with the same depth as MMLU.&lt;/p>
&lt;p>Execution:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/LiveBench/LiveBench
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> LiveBench
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install -e .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python livebench/gen_model_answer.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model-path meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model-id llama3-70b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python livebench/gen_ground_truth_judgment.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model-id llama3-70b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python livebench/show_livebench_result.py
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="lighteval-hugging-face">lighteval (Hugging Face)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/huggingface/lighteval&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>MIT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer&lt;/td>
&lt;td>Hugging Face (Leaderboard &amp;amp; Evals Team)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Available tasks&lt;/td>
&lt;td>1,000+ tasks across multiple domains and languages&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported backends&lt;/td>
&lt;td>Hugging Face Accelerate (CPU/GPU/multi-GPU), Nanotron, vLLM, TGI, OpenAI-compatible endpoints&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What it measures&lt;/td>
&lt;td>Accuracy, BERT score, BLEU, exact match; same as the harness + HF&amp;rsquo;s own tasks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Differentiator&lt;/td>
&lt;td>Designed to be the engine behind HF&amp;rsquo;s leaderboards; easy extension with custom tasks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Installation&lt;/td>
&lt;td>&lt;code>pip install lighteval&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>How it is invoked&lt;/td>
&lt;td>`lighteval accelerate &amp;ndash;model_args &amp;ldquo;pretrained=&lt;model>&amp;rdquo; &amp;ndash;tasks &amp;ldquo;lighteval&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Output&lt;/td>
&lt;td>JSON + direct integration with the Hugging Face Hub to publish results&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>lighteval is the internal successor to lm-evaluation-harness in the HF ecosystem: more modern, better vLLM and distributed backend support, and designed to run without leaving the HF stack. For teams already using the Hub to manage models, it is the lowest-friction option.&lt;/p>
&lt;hr>
&lt;h3 id="openllm-leaderboard-v2-archived-2025--openevals">OpenLLM Leaderboard v2 (archived 2025) → OpenEvals&lt;/h3>
&lt;p>The &lt;strong>OpenLLM Leaderboard v2&lt;/strong> evaluated more than 13,000 models between June 2024 and its closure in 2025, using six fixed benchmarks: &lt;strong>IFEval, MuSR, GPQA, MATH, BBH and MMLU-Pro&lt;/strong>, all run with lm-evaluation-harness. Its closure was a direct consequence of the saturation and gaming problem: as the benchmarks became well known, models were optimised for them.&lt;/p>
&lt;p>&lt;strong>Successor: OpenEvals&lt;/strong> (huggingface.co/spaces/OpenEvals/find-a-leaderboard)&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Aspect&lt;/th>
&lt;th>OpenLLM Leaderboard v2&lt;/th>
&lt;th>OpenEvals&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Architecture&lt;/td>
&lt;td>Single centralised leaderboard&lt;/td>
&lt;td>200+ specialised community leaderboards&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Benchmarks&lt;/td>
&lt;td>Fixed (6 tasks)&lt;/td>
&lt;td>Variable per leaderboard (math, code, medical, safety, etc.)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Models evaluated&lt;/td>
&lt;td>13,000+&lt;/td>
&lt;td>Distributed across leaderboards&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Eval engine&lt;/td>
&lt;td>lm-evaluation-harness&lt;/td>
&lt;td>lighteval (most) or harness&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Status&lt;/td>
&lt;td>Archived with an updated UI (Mar 2025)&lt;/td>
&lt;td>Active; you can create your own leaderboard&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Advantage&lt;/td>
&lt;td>Historical consistency&lt;/td>
&lt;td>Specialisation, speed of adaptation&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The archived collection is available for reference at &lt;code>huggingface.co/collections/OpenEvals/archived-open-llm-leaderboard-2024-2025&lt;/code>.&lt;/p>
&lt;hr>
&lt;h3 id="lmarena--chatbot-arena">LMArena / Chatbot Arena&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Site&lt;/td>
&lt;td>lmarena.ai (rebrand of LMSys Chatbot Arena, January 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Methodology&lt;/td>
&lt;td>Anonymous pairwise comparisons: the user votes for the answer they prefer without knowing the model&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Statistical model&lt;/td>
&lt;td>&lt;strong>Bradley-Terry&lt;/strong> (maximum likelihood over the full history of match-ups; replaced classic Elo for greater stability)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Accumulated votes&lt;/td>
&lt;td>&amp;gt; 6 million (Jun 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Variants&lt;/td>
&lt;td>Overall, Coding, Math, Hard Prompts, Multimodal — the specialised ones are more reliable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence/access&lt;/td>
&lt;td>Free (web); vote data available under a CC licence&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Limitations&lt;/td>
&lt;td>Overall is biased by labs&amp;rsquo; selective disclosure and by format gaming; Bradley-Terry was recalibrated in mid-2025 (Style Control), shifting some models by ±20–40 Elo&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>LMArena is the only benchmark based on human preference at scale. Its advantage: it captures real perceived quality, not just MCQ accuracy. Its limitation: it is hard to reproduce on-premise (it requires a user interface and a panel of human voters). Useful as an external validation signal, not as a CI gate.&lt;/p>
&lt;hr>
&lt;h2 id="decision-table-which-tool-for-which-objective">Decision table: which tool for which objective&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Objective&lt;/th>
&lt;th>Tool&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Reproduce a standard academic eval&lt;/td>
&lt;td>lm-evaluation-harness&lt;/td>
&lt;td>De facto engine; maximum compatibility with papers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Holistic evaluation (accuracy + fairness + toxicity)&lt;/td>
&lt;td>HELM (+ HELM Capabilities)&lt;/td>
&lt;td>The only framework measuring every axis at once&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Avoid contamination with recent questions&lt;/td>
&lt;td>LiveBench&lt;/td>
&lt;td>Monthly refresh; verifiable answers without a judge&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Native integration with HF Hub and vLLM&lt;/td>
&lt;td>lighteval&lt;/td>
&lt;td>vLLM backend, 1,000+ tasks, direct publishing to the Hub&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Compare against the public state of the art (OSS)&lt;/td>
&lt;td>OpenEvals + lighteval&lt;/td>
&lt;td>The successor ecosystem to the OpenLLM Leaderboard&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Human preference at scale&lt;/td>
&lt;td>LMArena&lt;/td>
&lt;td>Bradley-Terry over millions of real comparisons&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Custom domain eval (medical, legal, etc.)&lt;/td>
&lt;td>lighteval with a custom task&lt;/td>
&lt;td>Simpler to extend than the harness&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fast CI gate (&amp;lt;10 min)&lt;/td>
&lt;td>lm-eval harness (subset)&lt;/td>
&lt;td>Individual tasks with &lt;code>--tasks ifeval,gpqa&lt;/code> + vLLM batching&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="the-data-contamination-trap">The data contamination trap&lt;/h2>
&lt;h3 id="what-it-is">What it is&lt;/h3>
&lt;p>A model that has seen a benchmark&amp;rsquo;s &lt;strong>test data&lt;/strong> during pretraining memorises answers instead of generalising. The benchmark measures memorisation, not capability. This is called &lt;strong>data contamination&lt;/strong> or &lt;strong>benchmark leakage&lt;/strong>.&lt;/p>
&lt;h3 id="scale-of-the-problem-20242025-data">Scale of the problem (2024–2025 data)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Finding&lt;/th>
&lt;th>Source&lt;/th>
&lt;th>Figure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>N-gram overlap in Llama-2-70B&lt;/td>
&lt;td>&amp;ldquo;Data Contamination or Genuine Generalization?&amp;rdquo; (2025)&lt;/td>
&lt;td>18.1% overlap with eval datasets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Out-of-distribution drop in contaminated large models&lt;/td>
&lt;td>Ibid.&lt;/td>
&lt;td>−9.4 pp on reformulated prompts vs the original test&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HumanEval saturated&lt;/td>
&lt;td>Artificial Analysis / multiple 2026 reports&lt;/td>
&lt;td>Top models &amp;gt; 93%; separation ≤ 2 pp between the best 5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPQA Diamond: appearance of indirect contamination&lt;/td>
&lt;td>mindstudio.ai analysis 2026&lt;/td>
&lt;td>&amp;ldquo;Data laundering&amp;rdquo;: questions leaked via online forums&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="detection-mechanisms">Detection mechanisms&lt;/h3>
&lt;p>&lt;strong>1. N-gram overlap (the classic method)&lt;/strong>&lt;/p>
&lt;p>Compute the fraction of test set n-grams that appear in the model&amp;rsquo;s pretraining corpus. A practical threshold: if more than 13% of the test&amp;rsquo;s 8-grams match the corpus, the result is compromised.&lt;/p>
\[
\text{contamination ratio} = \frac{|\{g \in \text{test n-grams}\} \cap \{g \in \text{train n-grams}\}|}{|\text{test n-grams}|}
\]
&lt;p>Limit: it only works if you have access to the pretraining corpus. For closed models, it is not applicable.&lt;/p>
&lt;p>&lt;strong>2. Canary strings (decoy insertion)&lt;/strong>&lt;/p>
&lt;p>Unique random strings are inserted into the evaluation dataset while it is being created. If the model reproduces those strings without having seen them in context, it confirms that it memorised them from pretraining. The technique was proposed in &amp;ldquo;Extracting Training Data from Large Language Models&amp;rdquo; (Carlini et al., 2021) and adopted in recent benchmarks.&lt;/p>
&lt;p>&lt;strong>3. Perturbation and reformulation&lt;/strong>&lt;/p>
&lt;p>Semantically equivalent variants of the test are generated (paraphrasing, changing proper nouns, translation and back-translation). A model that generalises keeps its accuracy; a model that memorises drops. The average drop detected in contaminated models is 9–15 pp on equivalent reformulated prompts.&lt;/p>
&lt;p>&lt;strong>4. LiveBench (monthly refresh)&lt;/strong>&lt;/p>
&lt;p>The architectural solution: if the questions are generated from sources published &lt;strong>after&lt;/strong> the model&amp;rsquo;s cutoff (recent arXiv papers, news, new data), memorisation from pretraining cannot help. The model has to reason over information it could never have seen.&lt;/p>
&lt;p>&lt;strong>5. Kernel divergence analysis&lt;/strong> (Savelka et al., 2025)&lt;/p>
&lt;p>A more sophisticated statistical method: it compares the embedding distribution of the test set with the distribution of the training corpus. An abnormally low divergence indicates that the benchmark and the training corpus come from the same source.&lt;/p>
&lt;h3 id="why-static-leaderboards-saturate">Why static leaderboards saturate&lt;/h3>
&lt;p>The cycle is predictable:&lt;/p>
&lt;ol>
&lt;li>A new benchmark is published (MMLU in 2021, GPQA in 2023).&lt;/li>
&lt;li>The labs use it as an &lt;em>evaluation target&lt;/em> during pretraining and fine-tuning.&lt;/li>
&lt;li>Performance on that benchmark rises quickly, faster than real capability.&lt;/li>
&lt;li>The benchmark stops discriminating at the high end.&lt;/li>
&lt;li>The community needs a new benchmark.&lt;/li>
&lt;/ol>
&lt;p>The average time between the publication of a benchmark and its saturation in frontier models has gone from ~36 months (MMLU: 2021–2024) to ~18 months (HumanEval: 2021–2023, GPQA: 2023–2025). Saturation is accelerating.&lt;/p>
&lt;hr>
&lt;h2 id="honest-methodology-reproducibility-and-sources-of-variance">Honest methodology: reproducibility and sources of variance&lt;/h2>
&lt;h3 id="the-four-parameters-that-must-be-pinned">The four parameters that must be pinned&lt;/h3>
&lt;p>An eval result is only reproducible if it declares:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Parameter&lt;/th>
&lt;th>Example&lt;/th>
&lt;th>Why it matters&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Harness version&lt;/td>
&lt;td>&lt;code>lm-evaluation-harness==0.4.3&lt;/code>&lt;/td>
&lt;td>Changes to the prompt template between versions invalidate the comparison&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Exact model version&lt;/td>
&lt;td>&lt;code>meta-llama/Llama-3.1-70B-Instruct@sha256:abc…&lt;/code>&lt;/td>
&lt;td>Silent model updates on the Hub change the results&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prompt template&lt;/td>
&lt;td>&lt;code>mmlu_pro_cot_0shot&lt;/code> vs &lt;code>mmlu_pro_5shot&lt;/code>&lt;/td>
&lt;td>The difference between 0-shot and 5-shot can be 5–12 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Number of shots&lt;/td>
&lt;td>&lt;code>--num_fewshot 5&lt;/code>&lt;/td>
&lt;td>The published standard varies by benchmark; 0-shot and 5-shot are not comparable&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="few-shot-vs-zero-shot-the-numerical-difference">Few-shot vs zero-shot: the numerical difference&lt;/h3>
&lt;p>Sensitivity to the number of shots varies dramatically by benchmark:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Benchmark&lt;/th>
&lt;th>Typical 0-shot&lt;/th>
&lt;th>Typical 5-shot&lt;/th>
&lt;th>Difference&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>MMLU-Pro&lt;/td>
&lt;td>62%&lt;/td>
&lt;td>72%&lt;/td>
&lt;td>+10 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPQA Diamond&lt;/td>
&lt;td>55%&lt;/td>
&lt;td>65%&lt;/td>
&lt;td>+10 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GSM8K&lt;/td>
&lt;td>70%&lt;/td>
&lt;td>82%&lt;/td>
&lt;td>+12 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>IFEval&lt;/td>
&lt;td>68%&lt;/td>
&lt;td>71%&lt;/td>
&lt;td>+3 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HumanEval&lt;/td>
&lt;td>75%&lt;/td>
&lt;td>80%&lt;/td>
&lt;td>+5 pp&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Publishing an MMLU-Pro result in 0-shot without saying so, when the leaderboard standard is 5-shot, inflates the comparison by ~10 pp. Two results from the same model published with different numbers of shots &lt;strong>are not comparable&lt;/strong>.&lt;/p>
&lt;h3 id="prompt-sensitivity">Prompt sensitivity&lt;/h3>
&lt;p>Minor perturbations in the prompt template produce variations of up to ±8 pp on MCQ and ±15 pp on generation tasks. Sources of variance identified in the literature:&lt;/p>
&lt;ul>
&lt;li>Order of the options in MCQ (position bias): up to ±4 pp.&lt;/li>
&lt;li>Wording of the question (paraphrasing): ±3–8 pp.&lt;/li>
&lt;li>Presence or absence of &amp;ldquo;Let&amp;rsquo;s think step by step&amp;rdquo;: ±5–12 pp on reasoning.&lt;/li>
&lt;li>Language of the system instructions: ±3–10 pp in non-multilingual models.&lt;/li>
&lt;/ul>
&lt;h3 id="why-two-evals-give-different-results">Why two evals give different results&lt;/h3>
&lt;p>A common scenario: the paper reports MMLU-Pro 74.3% and the reproduction gives 71.8%. Frequent causes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Cause&lt;/th>
&lt;th>Typical magnitude&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Different harness version (changed template)&lt;/td>
&lt;td>1–4 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model with a silent update&lt;/td>
&lt;td>0.5–3 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Different context truncation&lt;/td>
&lt;td>0.5–2 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sampling seed (for generation)&lt;/td>
&lt;td>0.3–1.5 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokenisation with a chat template vs without one&lt;/td>
&lt;td>2–8 pp&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The sum of these sources explains gaps of 2–6 pp that are not noise but systematic protocol differences.&lt;/p>
&lt;h3 id="the-confidence-interval-formula-for-comparing-two-models">The confidence interval formula for comparing two models&lt;/h3>
&lt;p>To know whether the difference between model A (accuracy \(p_A\)) and model B (\(p_B\)) over a test set of size \(n\) is statistically significant:&lt;/p>
\[
\Delta p \pm 1.96 \cdot \sqrt{\frac{p_A(1-p_A) + p_B(1-p_B)}{n}}
\]
&lt;p>Applied to GPQA Diamond (\(n = 198\)), if \(p_A = 0.90\) and \(p_B = 0.88\):&lt;/p>
\[
\Delta p = 0.02 \quad;\quad \text{margin} = 1.96 \cdot \sqrt{\frac{0.09 + 0.1056}{198}} \approx 1.96 \cdot 0.0314 \approx 0.062
\]
&lt;p>The 95% confidence interval is \([{-0.042},\ {+0.082}]\): it &lt;strong>crosses zero&lt;/strong>. A difference of 2 pp over 198 questions is not statistically distinguishable from noise. To distinguish 90% from 88% with 95% confidence you need:&lt;/p>
\[
n \approx \frac{(1.96)^2 \cdot (p_A(1-p_A) + p_B(1-p_B))}{(\Delta p)^2} \approx \frac{3.84 \cdot 0.1956}{0.0004} \approx 1{,}878 \text{ questions}
\]
&lt;p>GPQA Diamond is not that size. Differences of 1–2 pp on GPQA Diamond are statistically invisible.&lt;/p>
&lt;hr>
&lt;h2 id="the-quality-axis-in-the-scorecard">The quality axis in the scorecard&lt;/h2>
&lt;p>The platform&amp;rsquo;s scorecard has three columns: performance (goodput under SLO), quality (score on a reference benchmark), energy (J/token). The quality column is built like this:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Content&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Primary benchmark&lt;/td>
&lt;td>MMLU-Pro (5-shot) — separates in the 60–77% zone&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reasoning benchmark&lt;/td>
&lt;td>GPQA Diamond (0-shot) — separates in the 55–90% zone&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Instruction benchmark&lt;/td>
&lt;td>IFEval (0-shot prompt-level accuracy)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code benchmark&lt;/td>
&lt;td>MBPP (0-shot, pass@1) — less saturated than HumanEval&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Anti-contamination&lt;/td>
&lt;td>LiveBench global score (monthly)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tool&lt;/td>
&lt;td>lm-evaluation-harness (primary) + lighteval (secondary, HF Hub)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pinned version&lt;/td>
&lt;td>&lt;code>harness==0.4.3&lt;/code>, &lt;code>lighteval==0.6.x&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Shots&lt;/td>
&lt;td>Declared explicitly per benchmark&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reproducibility&lt;/td>
&lt;td>Output JSON stored with the model SHA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Frequency&lt;/td>
&lt;td>Per model release; not in the CI of every PR (GPU-hour cost)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A full eval over 4 benchmarks (MMLU-Pro 12,032 + GPQA 198 + IFEval 541 + MBPP 500) with a 70B model on a generic 2×H100 node takes on the order of &lt;strong>4–8 hours&lt;/strong> depending on batch size and number of shots. The GPU cost is real: at ~5 €/h per amortised card, a full eval costs on the order of &lt;strong>40–80 €&lt;/strong>. It is not run on every commit; it is run on every model release or major adapter release.&lt;/p>
&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">LLM evals: the layer after tracing&lt;/a> — the system&amp;rsquo;s internal evaluation layer (golden dataset, CI gate, calibrated judge). A necessary complement to the academic benchmarks in this article.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge: fundamentals&lt;/a> — the LLM judge that evaluates outputs in production; different from an academic benchmark, but sharing the need for calibration and reproducibility.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evaluating-rag-ragas-golden-dataset-metrics/">Evaluating a RAG: RAGAS and the golden dataset&lt;/a> — a specialisation of evaluation for RAG systems; faithfulness and context precision do not appear in the academic benchmarks in this article.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">Benchmark tool catalogue: data sheet by data sheet&lt;/a> — the performance axis (TTFT, ITL, goodput); the complementary half of this article in the scorecard.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">LLM benchmarking: state of the art&lt;/a> — the B1 introduction with the three axes and the track&amp;rsquo;s general methodology.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>EleutherAI · lm-evaluation-harness — &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness">https://github.com/EleutherAI/lm-evaluation-harness&lt;/a>&lt;/li>
&lt;li>Stanford CRFM · HELM — &lt;a href="https://github.com/stanford-crfm/helm">https://github.com/stanford-crfm/helm&lt;/a>&lt;/li>
&lt;li>Stanford CRFM · HELM Capabilities (mar-2025) — &lt;a href="https://crfm.stanford.edu/2025/03/20/helm-capabilities.html">https://crfm.stanford.edu/2025/03/20/helm-capabilities.html&lt;/a>&lt;/li>
&lt;li>Stanford CRFM · HELM (sitio oficial) — &lt;a href="https://crfm.stanford.edu/helm/">https://crfm.stanford.edu/helm/&lt;/a>&lt;/li>
&lt;li>LiveBench · paper arXiv:2406.19314 — &lt;a href="https://arxiv.org/abs/2406.19314">https://arxiv.org/abs/2406.19314&lt;/a>&lt;/li>
&lt;li>LiveBench · sitio oficial — &lt;a href="https://livebench.ai">https://livebench.ai&lt;/a>&lt;/li>
&lt;li>LiveBench · GitHub — &lt;a href="https://github.com/LiveBench/LiveBench">https://github.com/LiveBench/LiveBench&lt;/a>&lt;/li>
&lt;li>NYU Center for Data Science · LiveBench blog — &lt;a href="https://nyudatascience.medium.com/livebench-challenging-language-models-with-contamination-free-questions-999b52967ec8">https://nyudatascience.medium.com/livebench-challenging-language-models-with-contamination-free-questions-999b52967ec8&lt;/a>&lt;/li>
&lt;li>Hugging Face · lighteval — &lt;a href="https://github.com/huggingface/lighteval">https://github.com/huggingface/lighteval&lt;/a>&lt;/li>
&lt;li>Hugging Face · OpenLLM Leaderboard (archivado) — &lt;a href="https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard">https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard&lt;/a>&lt;/li>
&lt;li>Hugging Face · OpenEvals (sucesor) — &lt;a href="https://huggingface.co/spaces/OpenEvals/find-a-leaderboard">https://huggingface.co/spaces/OpenEvals/find-a-leaderboard&lt;/a>&lt;/li>
&lt;li>Hugging Face · Archived Open LLM Leaderboard 2024-2025 — &lt;a href="https://huggingface.co/collections/OpenEvals/archived-open-llm-leaderboard-2024-2025">https://huggingface.co/collections/OpenEvals/archived-open-llm-leaderboard-2024-2025&lt;/a>&lt;/li>
&lt;li>LMArena · Chatbot Arena — &lt;a href="https://lmarena.ai">https://lmarena.ai&lt;/a>&lt;/li>
&lt;li>LMSys · blog Chatbot Arena Elo — &lt;a href="https://www.lmsys.org/blog/2023-05-03-arena/">https://www.lmsys.org/blog/2023-05-03-arena/&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;A Statistical Framework for Ranking LLM-Based Chatbots&amp;rdquo; · arXiv:2412.18407 — &lt;a href="https://arxiv.org/pdf/2412.18407">https://arxiv.org/pdf/2412.18407&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;Data Contamination or Genuine Generalization?&amp;rdquo; (2025) — &lt;a href="https://www.suaspress.org/ojs/index.php/AJNS/article/view/v2n2a03">https://www.suaspress.org/ojs/index.php/AJNS/article/view/v2n2a03&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;When Benchmarks Leak: Inference-Time Decontamination&amp;rdquo; (2025) · arXiv:2601.19334 — &lt;a href="https://arxiv.org/pdf/2601.19334">https://arxiv.org/pdf/2601.19334&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;How Contaminated Is Your Benchmark?&amp;rdquo; (2025) · arXiv:2502.00678 — &lt;a href="https://arxiv.org/pdf/2502.00678">https://arxiv.org/pdf/2502.00678&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;When AI Benchmarks Plateau&amp;rdquo; (2026) · arXiv:2602.16763 — &lt;a href="https://arxiv.org/pdf/2602.16763">https://arxiv.org/pdf/2602.16763&lt;/a>&lt;/li>
&lt;li>GPQA · arXiv:2311.12022 — &lt;a href="https://arxiv.org/abs/2311.12022">https://arxiv.org/abs/2311.12022&lt;/a>&lt;/li>
&lt;li>GPQA Diamond leaderboard · Artificial Analysis — &lt;a href="https://artificialanalysis.ai/evaluations/gpqa-diamond">https://artificialanalysis.ai/evaluations/gpqa-diamond&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;The Emperor&amp;rsquo;s New Clothes in Benchmarking?&amp;rdquo; (2025) · arXiv:2503.16402 — &lt;a href="https://arxiv.org/pdf/2503.16402">https://arxiv.org/pdf/2503.16402&lt;/a>&lt;/li>
&lt;li>lm-evaluation-harness · CLI reference — &lt;a href="https://lm-evaluation-harness.readthedocs.io/running_evals/cli_reference/">https://lm-evaluation-harness.readthedocs.io/running_evals/cli_reference/&lt;/a>&lt;/li>
&lt;li>crfm-helm · PyPI — &lt;a href="https://pypi.org/project/crfm-helm/">https://pypi.org/project/crfm-helm/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>GitOps for the inference stack with Flux: operating the assistant as code</title><link>https://blog.lo0.es/en/posts/gitops-inference-stack-flux/</link><pubDate>Thu, 11 Jun 2026 10:20:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/gitops-inference-stack-flux/</guid><description>&lt;blockquote>
&lt;p>This post is part of the operational series on how to squeeze a generic on-premise 4×H100 SXM 80 GB LLM cluster. The sibling pieces: the &lt;a href="https://blog.lo0.es/en/posts/end-to-end-document-ingestion-pdf-to-indexed-chunk/">document ingestion pipeline for RAG&lt;/a> that fills the vector store we deploy here, the &lt;a href="https://blog.lo0.es/en/posts/serving-embeddings-rerankers-tei-production/">embeddings and reranker service with TEI&lt;/a> that is one of the services GitOps manages, and the &lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">secrets hardening of the sovereign stack&lt;/a>, which goes deep into the secrets problem we only state here. The complete end-to-end assistant (LibreChat + LiteLLM + RAG) that orchestrates all of this will get its own post.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>An LLM assistant in production is a distributed system, not a binary. At a minimum it has an &lt;strong>inference engine&lt;/strong> (vLLM or similar), an &lt;strong>L7 gateway&lt;/strong> that routes per model and applies rate limiting, a &lt;strong>chat front end&lt;/strong>, a &lt;strong>vector store&lt;/strong> for RAG, an &lt;strong>embeddings/reranker&lt;/strong> service, and an &lt;strong>observability&lt;/strong> stack. Six substantial services, spread across three or four namespaces, with start-up dependencies (the front end is useless if the gateway is not there, the gateway is useless if the engine has not loaded the model, RAG does not answer if the vector store is empty). Operating it by hand, a &lt;code>kubectl apply&lt;/code> here and a &lt;code>helm upgrade&lt;/code> there, produces a cluster whose real state nobody can reconstruct: there is no record of what was applied, or when, or why. &lt;strong>GitOps&lt;/strong> solves this with four principles (&lt;a href="https://opengitops.dev/">OpenGitOps&lt;/a>): the desired state is &lt;strong>declarative&lt;/strong>, it is &lt;strong>versioned and immutable&lt;/strong> in git, it is &lt;strong>applied automatically&lt;/strong> by an agent (nobody SSHes in to deploy) and it is &lt;strong>continuously reconciled&lt;/strong> by comparing the actual state against the declared one. &lt;strong>Flux&lt;/strong> is that agent: six controllers (source, kustomize, helm, notification, image-reflector, image-automation) that clone the repo, render Kustomize/Helm, apply to the cluster, correct drift and, optionally, write back to git to bump the tag of a new image. The unit of work is the &lt;code>Kustomization&lt;/code> or the &lt;code>HelmRelease&lt;/code>, with &lt;code>interval&lt;/code> (how often it reconciles), &lt;code>prune&lt;/code> (deletes what is no longer in git), &lt;code>dependsOn&lt;/code> (orders the start-up) and health checks. The &lt;code>interval&lt;/code> sets the MTTR of drift: with &lt;code>interval=1m&lt;/code>, a manual edit to the cluster is reverted in ≤ 1 min on average. Secrets cannot sit in the clear in git, a chicken-and-egg problem, and it is solved with SOPS/age, sealed-secrets or External Secrets + Vault. Rollback is a &lt;code>git revert&lt;/code>. GitOps is not free: the learning curve is real, and debugging why a &lt;code>Kustomization&lt;/code> is not reconciling is a new skill.&lt;/p>
&lt;h2 id="the-analogy-the-master-plan-and-the-foreman-who-does-not-negotiate">The analogy: the master plan and the foreman who does not negotiate&lt;/h2>
&lt;p>Picture a large building site with a signed and filed &lt;strong>master plan&lt;/strong>, and a &lt;strong>foreman&lt;/strong> who has a single order: the site must be identical to the plan, at all times. The foreman does not improvise. Every so often he walks the site with the plan in hand and compares: if a wall is where the plan says, he leaves it; if someone moved a partition overnight without updating the plan, he puts it back; if the plan says there is a column and it does not exist, he builds it; if a column exists but no longer appears on the plan, he knocks it down. The plan is the &lt;strong>single source of truth&lt;/strong>: to change the site you do not touch the site, you change the plan, and the foreman takes care of the rest on his next round.&lt;/p>
&lt;p>That is exactly the mechanics of GitOps. &lt;strong>Git is the master plan&lt;/strong>: declarative (it describes the end state, not the steps), versioned (every change is signed into the history, with author and reason), immutable (a commit is not rewritten). &lt;strong>Flux is the foreman&lt;/strong>: every &lt;code>interval&lt;/code> it walks the cluster, compares against git and converges. And here is the lesson that separates GitOps from &amp;ldquo;keeping the YAML in a repo&amp;rdquo;: the foreman &lt;strong>undoes manual changes&lt;/strong>. If an operator comes in with &lt;code>kubectl edit&lt;/code> and raises the inference engine replicas from 2 to 4 at three in the morning to put out a fire, on the next round Flux takes it back to 2, because the plan says 2. This enrages anyone coming from the imperative world, and it is precisely the point: if you want 4 replicas permanently, you update the plan. The cluster stops being a system with its own opaque memory and becomes a &lt;strong>reproducible projection of git&lt;/strong>. Delete the entire cluster, point a fresh Flux at the same repo, and the site is rebuilt identically.&lt;/p>
&lt;p>The analogy also marks the boundary: the plan describes the building, not who holds the keys to the materials store. &lt;strong>Secrets&lt;/strong> (passwords, tokens, keys) cannot go into the public plan. That is the chicken-and-egg problem we cover further down and that the &lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">hardening piece&lt;/a> develops in full.&lt;/p>
&lt;h2 id="the-problem-an-assistant-is-six-services-not-one">The problem: an assistant is six services, not one&lt;/h2>
&lt;p>Before Flux, it is worth fixing the size of the problem. A minimally serious sovereign LLM assistant, deployed on the generic reference cluster, has this service topology:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Service&lt;/th>
&lt;th>Function&lt;/th>
&lt;th>Typical namespace&lt;/th>
&lt;th>State&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Inference engine (vLLM)&lt;/td>
&lt;td>Serves the tokens of the general LLM and the code one&lt;/td>
&lt;td>&lt;code>llm-serving&lt;/code>&lt;/td>
&lt;td>Stateless (model from object store)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Embeddings + reranker (TEI)&lt;/td>
&lt;td>Vectorises queries and reorders candidates for RAG&lt;/td>
&lt;td>&lt;code>llm-serving&lt;/code>&lt;/td>
&lt;td>Stateless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vector store (Qdrant)&lt;/td>
&lt;td>Stores and searches document embeddings&lt;/td>
&lt;td>&lt;code>data&lt;/code>&lt;/td>
&lt;td>&lt;strong>Stateful&lt;/strong> (PVC)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>L7 gateway (Envoy AI Gateway / LiteLLM)&lt;/td>
&lt;td>Per-model routing, rate limiting, auth&lt;/td>
&lt;td>&lt;code>gateway&lt;/code>&lt;/td>
&lt;td>Semi-stateless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chat front end&lt;/td>
&lt;td>Assistant UI&lt;/td>
&lt;td>&lt;code>apps&lt;/code>&lt;/td>
&lt;td>Stateless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Observability (Langfuse, OTel Collector, dashboards)&lt;/td>
&lt;td>LLM-aware traces, metrics and logs&lt;/td>
&lt;td>&lt;code>observability&lt;/code>&lt;/td>
&lt;td>Stateful (Langfuse Postgres)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Six services, four or five namespaces, two components with persistent state, and a real dependency graph: the front end calls the gateway, the gateway calls the engine and the embeddings service, RAG depends on the vector store being &lt;strong>full&lt;/strong>, and everything emits traces to observability. Deploying this by hand means remembering the order, the values of each &lt;code>helm install&lt;/code>, the &lt;code>ConfigMap&lt;/code>s, the &lt;code>Secret&lt;/code>s, the &lt;code>nodeSelector&lt;/code>s that pin the GPU pods to the right nodes. Do it twice (a staging environment and a production one) and the two diverge within days. That is exactly what GitOps eliminates.&lt;/p>
&lt;h2 id="the-six-flux-controllers">The six Flux controllers&lt;/h2>
&lt;p>Flux is not a monolithic binary but a set of cooperating controllers, the &lt;em>GitOps Toolkit&lt;/em>. A default installation brings four; the two image automation ones are added with &lt;code>--components-extra&lt;/code> (&lt;a href="https://fluxcd.io/flux/installation/">Flux installation&lt;/a>):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>source-controller&lt;/strong>: clones and keeps the sources up to date, whether git repos (&lt;code>GitRepository&lt;/code>), Helm repos (&lt;code>HelmRepository&lt;/code>), buckets or OCI artifacts. It is the one &amp;ldquo;holding the plan&amp;rdquo;: it exposes the repo contents as an internal artifact that the others consume.&lt;/li>
&lt;li>&lt;strong>kustomize-controller&lt;/strong>: takes a source artifact, renders Kustomize (bases + overlays) and applies the result to the cluster. It is responsible for &lt;code>prune&lt;/code>, &lt;code>dependsOn&lt;/code> and the health checks of the &lt;code>Kustomization&lt;/code>s.&lt;/li>
&lt;li>&lt;strong>helm-controller&lt;/strong>: reconciles &lt;code>HelmRelease&lt;/code> objects, installing and updating Helm charts declaratively, without anybody running &lt;code>helm&lt;/code> from a terminal (&lt;a href="https://github.com/fluxcd/helm-controller">helm-controller&lt;/a>).&lt;/li>
&lt;li>&lt;strong>notification-controller&lt;/strong>: the bridge to the outside world in both directions. It receives webhooks (to reconcile instantly on every push, instead of waiting for the &lt;code>interval&lt;/code>) and emits events and alerts to Slack, an internal chat or an incident system.&lt;/li>
&lt;li>&lt;strong>image-reflector-controller&lt;/strong>: scans container registries and stores the tags it finds in an internal database. It is the &amp;ldquo;eyes&amp;rdquo; of image automation.&lt;/li>
&lt;li>&lt;strong>image-automation-controller&lt;/strong>: uses what the eyes see to write back to git, committing the new image tag into the manifests when a version appears that satisfies the policy.&lt;/li>
&lt;/ul>
&lt;p>A cluster without image automation does not need the last two. But for an inference stack where the model server is updated fairly often, they are what automates version promotion without touching anything by hand.&lt;/p>
&lt;div class="diagram" style="max-width:860px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 860 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="GitOps loop: git, source-controller, kustomize and helm controllers, cluster, drift and image automation writing back to git">
&lt;defs>
&lt;marker id="ga" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>&lt;/marker>
&lt;marker id="gb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#8b5cf6"/>&lt;/marker>
&lt;marker id="gr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#ef4444"/>&lt;/marker>
&lt;/defs>
&lt;text x="430" y="26" text-anchor="middle" font-size="14" font-weight="700" fill="currentColor">The Flux reconciliation loop&lt;/text>
&lt;rect x="40" y="60" width="150" height="80" rx="8" fill="#3b82f6" fill-opacity="0.12" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="115" y="92" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">git&lt;/text>
&lt;text x="115" y="112" text-anchor="middle" font-size="10" fill="currentColor">master plan&lt;/text>
&lt;text x="115" y="127" text-anchor="middle" font-size="10" fill="currentColor">desired state&lt;/text>
&lt;rect x="270" y="50" width="180" height="46" rx="6" fill="#22c55e" fill-opacity="0.12" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="360" y="70" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">source-controller&lt;/text>
&lt;text x="360" y="86" text-anchor="middle" font-size="9.5" fill="currentColor">clones the repo · artifact&lt;/text>
&lt;rect x="270" y="106" width="180" height="46" rx="6" fill="#22c55e" fill-opacity="0.12" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="360" y="126" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">kustomize-controller&lt;/text>
&lt;text x="360" y="142" text-anchor="middle" font-size="9.5" fill="currentColor">renders overlays · applies&lt;/text>
&lt;rect x="270" y="162" width="180" height="46" rx="6" fill="#22c55e" fill-opacity="0.12" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="360" y="182" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">helm-controller&lt;/text>
&lt;text x="360" y="198" text-anchor="middle" font-size="9.5" fill="currentColor">reconciles HelmRelease&lt;/text>
&lt;rect x="560" y="90" width="170" height="120" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.6"/>
&lt;text x="645" y="118" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">cluster&lt;/text>
&lt;text x="645" y="138" text-anchor="middle" font-size="10" fill="currentColor">actual state&lt;/text>
&lt;text x="645" y="158" text-anchor="middle" font-size="9.5" fill="currentColor">engine · gateway&lt;/text>
&lt;text x="645" y="173" text-anchor="middle" font-size="9.5" fill="currentColor">front end · vector store&lt;/text>
&lt;text x="645" y="188" text-anchor="middle" font-size="9.5" fill="currentColor">observability&lt;/text>
&lt;path d="M190,90 L268,75" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#ga)"/>
&lt;path d="M450,129 L558,140" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#ga)"/>
&lt;path d="M450,185 L558,165" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#ga)"/>
&lt;path d="M645,210 C645,250 360,250 360,213" fill="none" stroke="#ef4444" stroke-width="1.8" stroke-dasharray="5 3" marker-end="url(#gr)"/>
&lt;text x="430" y="268" text-anchor="middle" font-size="10" fill="#ef4444">reconcile every interval: compares actual vs desired, fixes drift, prunes&lt;/text>
&lt;rect x="270" y="312" width="180" height="58" rx="6" fill="#8b5cf6" fill-opacity="0.12" stroke="#8b5cf6" stroke-width="1.6"/>
&lt;text x="360" y="334" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor">image automation&lt;/text>
&lt;text x="360" y="350" text-anchor="middle" font-size="9.5" fill="currentColor">reflector scans registry&lt;/text>
&lt;text x="360" y="364" text-anchor="middle" font-size="9.5" fill="currentColor">policy picks tag · commit&lt;/text>
&lt;path d="M560,200 C500,300 460,320 452,335" fill="none" stroke="#8b5cf6" stroke-width="1.6" marker-end="url(#gb)"/>
&lt;text x="540" y="300" text-anchor="middle" font-size="9.5" fill="#8b5cf6">registry: new tag&lt;/text>
&lt;path d="M268,338 C160,330 115,250 115,142" fill="none" stroke="#8b5cf6" stroke-width="1.8" marker-end="url(#gb)"/>
&lt;text x="120" y="250" text-anchor="middle" font-size="9.5" fill="#8b5cf6">writes back&lt;/text>
&lt;text x="120" y="263" text-anchor="middle" font-size="9.5" fill="#8b5cf6">to git (commit)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The diagram has three paths. The &lt;strong>grey&lt;/strong> one is the main flow: git → source-controller → kustomize/helm-controller → cluster. The &lt;strong>dashed red&lt;/strong> one is the reconciliation loop that runs every &lt;code>interval&lt;/code>: it compares actual state against desired, fixes drift and prunes what is left over. The &lt;strong>purple&lt;/strong> one is image automation: the reflector sees a new tag in the registry, the policy decides whether it qualifies, and the automation-controller &lt;strong>writes back to git&lt;/strong>. That last path is the one that closes the circle and turns git into a system that updates itself.&lt;/p>
&lt;h2 id="repo-structure-clusters-infrastructure-apps">Repo structure: clusters, infrastructure, apps&lt;/h2>
&lt;p>The most widespread convention separates three levels of responsibility. It is not the only one, but it ages well:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">gitops-repo/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── clusters/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── prod/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── infrastructure.yaml # Kustomization → ./infrastructure
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── apps.yaml # Kustomization → ./apps (dependsOn infrastructure)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── infrastructure/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── controllers/ # ingress, cert-manager, GPU operator, KEDA...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── configs/ # ClusterIssuer, RuntimeClass, StorageClass...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── apps/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── base/ # manifests common to all environments
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── llm-engine/ # vLLM HelmRelease
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── gateway/ # L7 gateway HelmRelease
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── chat-front/ # Deployment + Service + Ingress
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── vector-store/ # Qdrant HelmRelease (+ PVC)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── observability/ # Langfuse + OTel HelmRelease
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── staging/ # overlay: low replicas, small model
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── prod/ # overlay: high replicas, large model, MIG
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The key piece is Kustomize&amp;rsquo;s &lt;strong>base / overlay&lt;/strong> separation. The &lt;code>base/&lt;/code> describes the service once; each overlay (&lt;code>staging/&lt;/code>, &lt;code>prod/&lt;/code>) applies a &lt;code>patch&lt;/code> with the environment&amp;rsquo;s differences: number of replicas, model size, MIG profile, &lt;code>nodeSelector&lt;/code>, &lt;code>--gpu-memory-utilization&lt;/code>. That avoids duplicating complete manifests per environment. The &lt;code>clusters/&lt;/code> folder is the entry point Flux reconciles first: it holds the root &lt;code>Kustomization&lt;/code>s that point to &lt;code>infrastructure/&lt;/code> and &lt;code>apps/&lt;/code>, with a &lt;code>dependsOn&lt;/code> that guarantees the infrastructure (CRDs, operators, storage classes) is ready &lt;strong>before&lt;/strong> the applications.&lt;/p>
&lt;h3 id="how-many-manifests-the-calculation-that-justifies-overlays">How many manifests: the calculation that justifies overlays&lt;/h3>
&lt;p>This is where the numbers make the argument. Without overlays, each service needs a complete set of manifests per environment. With $N$ services and $M$ environments, the cost in files to maintain is:&lt;/p>
$$
\text{files}_{\text{naïve}} = N \times M \times k
$$
&lt;p>where $k$ is the average number of manifests per service (Deployment/HelmRelease + Service + ConfigMap + PVC + Ingress ≈ 5). For our assistant, $N=6$ services and $M=3$ environments (dev, staging, prod), with $k=5$:&lt;/p>
$$
6 \times 3 \times 5 = 90 \text{ complete files, each maintained separately.}
$$
&lt;p>With the base/overlay structure, the base is written once and each overlay contains only the &lt;code>patch&lt;/code> with the differences (typically 1 short file per service per environment):&lt;/p>
$$
\text{files}_{\text{overlay}} = \underbrace{N \times k}_{\text{base}} + \underbrace{N \times M}_{\text{patches}} = 6 \times 5 + 6 \times 3 = 30 + 18 = 48
$$
&lt;p>It is not just that there are fewer files (48 against 90): it is that &lt;strong>the bulk of a change happens in a single place&lt;/strong>. Changing the engine&amp;rsquo;s memory limit for every environment is one edit in &lt;code>base/&lt;/code>, not three synchronised edits. The saving factor grows with $M$: for 5 environments, the naïve version is 150 files and the overlay one is 60. Duplication is the enemy of auditability, and overlays attack it at the root.&lt;/p>
&lt;h2 id="the-reconciliation-loop-desired-vs-actual">The reconciliation loop: desired vs actual&lt;/h2>
&lt;p>The heart of Flux is a control loop that never ends: every &lt;code>interval&lt;/code> it reads the desired state (git), reads the actual state (cluster), computes the difference and applies it. It is the same principle as a &lt;strong>thermostat&lt;/strong>: it reads the target temperature (git), reads the actual temperature (cluster) and switches the boiler on or off until they match. It does not &amp;ldquo;deploy once&amp;rdquo;; it converges forever.&lt;/p>
&lt;p>A &lt;code>Kustomization&lt;/code> manifest for the applications, with the pieces that matter (&lt;a href="https://fluxcd.io/flux/components/kustomize/kustomizations/">Kustomization reference&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kustomize.toolkit.fluxcd.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Kustomization&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">flux-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval: 1m # reconciles every minute&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sets the drift MTTR&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./apps/prod &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># production overlay&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prune&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># deletes from the cluster whatever is removed from git&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sourceRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GitRepository&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gitops-repo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dependsOn&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">infrastructure &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># does not apply apps until infra is Ready&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">wait&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># waits for the resources to be healthy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># fails the reconciliation if it does not converge in 5 min&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthChecks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Four fields do the heavy lifting:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>interval&lt;/code>&lt;/strong> defines how often the foreman&amp;rsquo;s round runs. It is the direct determinant of the drift MTTR (next section).&lt;/li>
&lt;li>&lt;strong>&lt;code>prune: true&lt;/code>&lt;/strong> switches on garbage collection: objects that were applied before but are no longer in the current git revision are deleted from the cluster automatically. Without &lt;code>prune&lt;/code>, a service withdrawn from the repo keeps running, and the plan and the site diverge silently.&lt;/li>
&lt;li>&lt;strong>&lt;code>dependsOn&lt;/code>&lt;/strong> orders the graph: Flux does not apply the manifests of a &lt;code>Kustomization&lt;/code> until every referenced one is in state &lt;code>Ready: True&lt;/code>. It is the mechanism that guarantees data → gateway → front end.&lt;/li>
&lt;li>&lt;strong>&lt;code>wait&lt;/code> + &lt;code>healthChecks&lt;/code>&lt;/strong>: with &lt;code>wait: true&lt;/code>, Flux monitors every applied resource and waits for them to be ready before marking the reconciliation as successful; &lt;code>healthChecks&lt;/code> lets you tune exactly which resources to watch. That is what makes a &lt;code>dependsOn&lt;/code> mean something: the dependency is not satisfied until it is &lt;strong>healthy&lt;/strong>, not merely applied.&lt;/li>
&lt;/ul>
&lt;h3 id="mttr-and-drift-detection-as-a-function-of-the-interval">MTTR and drift detection as a function of the interval&lt;/h3>
&lt;p>The &lt;code>interval&lt;/code> is the only parameter the operator chooses to govern the speed of the loop, and it translates directly into operational metrics. If a deviation (drift) happens at a random instant within the reconciliation period $T$, the waiting time until Flux detects it is uniformly distributed over $[0, T]$, with mean:&lt;/p>
$$
\mathbb{E}[t_{\text{detection}}] = \frac{T}{2}
$$
&lt;p>To that you add the correction time $t_c$ (render, diff, apply), normally a few seconds. The drift MTTR comes out as:&lt;/p>
$$
\text{MTTR}_{\text{drift}} = \frac{T}{2} + t_c
$$
&lt;p>With &lt;code>interval=1m&lt;/code> and $t_c \approx 10\text{s}$, drift is corrected in $\frac{60}{2} + 10 = 40$ s on average, with a worst case of $60 + 10 = 70$ s. With &lt;code>interval=10m&lt;/code>, the mean rises to $5\text{min}\;10\text{s}$ and the worst case to more than 10 min: a window in which a manual change stays active. The temptation is to set &lt;code>interval=10s&lt;/code> and forget about it, but there is a cost: every reconciliation consumes CPU and, above all, makes requests to the cluster API and to the registry. With dozens of &lt;code>Kustomization&lt;/code>s reconciling every 10 s, the &lt;code>kube-apiserver&lt;/code> and the image-reflector start to feel the pressure. The practical rule: a short &lt;code>interval&lt;/code> (1m) for critical applications whose drift hurts, a long &lt;code>interval&lt;/code> (10–30m) for stable infrastructure that almost never changes, and notification-controller &lt;strong>webhooks&lt;/strong> for instant reconciliation on every push, so that the &lt;code>interval&lt;/code> governs only drift and not deployment latency.&lt;/p>
&lt;h2 id="helm-as-code-the-inference-engine-helmrelease">Helm as code: the inference engine HelmRelease&lt;/h2>
&lt;p>For the engine, the gateway and the front end, the usual approach is to package them as Helm charts and declare them with &lt;code>HelmRelease&lt;/code>. The helm-controller reconciles them without anyone ever running &lt;code>helm&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">helm.toolkit.fluxcd.io/v2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HelmRelease&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-engine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-serving&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">chart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">chart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sourceRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HelmRepository&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-charts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.8.4&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># managed by image automation (see below)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicaCount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">H100-SXM&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extraArgs&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="s2">&amp;#34;--gpu-memory-utilization=0.90&amp;#34;&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="s2">&amp;#34;--tensor-parallel-size=1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything that in the imperative world would be a &lt;code>vllm serve&lt;/code> flag, &lt;code>--gpu-memory-utilization&lt;/code> or &lt;code>--tensor-parallel-size&lt;/code>, now lives as versioned data. Changing the fraction of VRAM the engine reserves is a commit, reviewable in a PR, with history. The relationship with &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">sharing a GPU&lt;/a> is direct: the MIG profile and the &lt;code>--gpu-memory-utilization&lt;/code> are deployment parameters, and here they are code.&lt;/p>
&lt;h2 id="image-automation-bumping-the-tag-without-touching-anything-by-hand">Image automation: bumping the tag without touching anything by hand&lt;/h2>
&lt;p>The inference engine is updated fairly often (vLLM patches, security fixes). Without automation, every new version requires somebody to edit the &lt;code>tag&lt;/code> in the &lt;code>HelmRelease&lt;/code>. Flux image automation does it on its own, with three objects (&lt;a href="https://fluxcd.io/flux/guides/image-update/">automate image updates&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">image.toolkit.fluxcd.io/v1beta2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ImageRepository&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">flux-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry.example.local/inference/vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># scans the registry tags every 5 min&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="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">image.toolkit.fluxcd.io/v1beta2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ImagePolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">flux-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imageRepositoryRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">semver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">range&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;gt;=0.8.0 &amp;lt;0.9.0&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># only patches and minors within 0.8.x–0.8.x&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="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">image.toolkit.fluxcd.io/v1beta1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ImageUpdateAutomation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">flux-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sourceRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GitRepository&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gitops-repo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">git&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">author&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fluxbot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fluxbot@example.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">messageTemplate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;auto: bump vllm to {{ .NewTag }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The division of roles: &lt;strong>&lt;code>ImageRepository&lt;/code>&lt;/strong> scans all the tags of the image repository and stores them in the reflector&amp;rsquo;s internal database. &lt;strong>&lt;code>ImagePolicy&lt;/code>&lt;/strong> reads those tags and picks the &amp;ldquo;latest&amp;rdquo; according to the policy; the &lt;code>policy&lt;/code> field is mandatory and defines how the selection is made (&lt;a href="https://fluxcd.io/flux/components/image/imagepolicies/">ImagePolicy&lt;/a>). &lt;strong>&lt;code>ImageUpdateAutomation&lt;/code>&lt;/strong> takes the chosen tag, edits the manifest in the repo (wherever there is a &lt;code># {&amp;quot;$imagepolicy&amp;quot;: &amp;quot;flux-system:vllm&amp;quot;}&lt;/code> marker) and commits (&lt;a href="https://fluxcd.io/flux/components/image/imageupdateautomations/">ImageUpdateAutomation&lt;/a>).&lt;/p>
&lt;p>The choice of policy matters:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>semver&lt;/strong>: interprets tags as semantic versions and picks the highest one that satisfies the range (&lt;code>range: &amp;quot;&amp;gt;=0.8.0 &amp;lt;0.9.0&amp;quot;&lt;/code>). By default it excludes prereleases (&lt;code>0.8.0-rc.1&lt;/code> does not qualify) unless they are asked for explicitly. It is the right option for production: you stay inside a tested version range and do not jump to a new major by accident.&lt;/li>
&lt;li>&lt;strong>regex / alphabetical / numerical&lt;/strong>: for tag schemes that are not pure semver, for example &lt;code>main-&amp;lt;sha&amp;gt;-&amp;lt;timestamp&amp;gt;&lt;/code> from an internal pipeline. More flexible, but it forces you to trust that tag order reflects version order, which is fragile.&lt;/li>
&lt;/ul>
&lt;p>A safety guideline: instead of having &lt;code>ImageUpdateAutomation&lt;/code> push directly to &lt;code>main&lt;/code>, configure it to write to a &lt;strong>branch&lt;/strong> and open a &lt;strong>PR&lt;/strong>. That way the version bump goes through human review or a validation pipeline before reaching the cluster. Automation proposes; the human (or an automatic gate) disposes. And for production, the version bump is not the deployment: it is the trigger for the progressive rollout described in &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">canary, blue-green and shadow&lt;/a>.&lt;/p>
&lt;h2 id="secrets-in-gitops-the-chicken-and-egg-problem">Secrets in GitOps: the chicken-and-egg problem&lt;/h2>
&lt;p>GitOps demands that all the desired state is in git. But secrets, the Langfuse Postgres password, the registry token, the gateway API keys, cannot go into a repo in the clear, not even a private one: git history is immutable, and a secret committed once stays there forever. This is the fundamental tension: GitOps wants everything in git; security forbids secrets in git. There are three families of solution, all with the same underlying idea, that &lt;strong>only the encrypted secret goes into git and the cluster holds the key to decrypt it&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SOPS + age/KMS&lt;/strong>: you encrypt the sensitive values of a manifest with &lt;a href="https://github.com/getsops/sops">SOPS&lt;/a> (it leaves the keys readable and encrypts only the values). The encrypted YAML goes to git; the kustomize-controller carries an age key or reaches a KMS to decrypt it at apply time. Simple, with no extra components in the cluster.&lt;/li>
&lt;li>&lt;strong>sealed-secrets&lt;/strong>: a controller in the cluster holds a private key. You encrypt the secret against its public key (with &lt;code>kubeseal&lt;/code>), the encrypted &lt;code>SealedSecret&lt;/code> goes to git, and the controller decrypts it into a normal &lt;code>Secret&lt;/code> inside the cluster. The private key never leaves the cluster.&lt;/li>
&lt;li>&lt;strong>External Secrets + Vault&lt;/strong>: not even the encrypted secret goes into git, only a &lt;strong>reference&lt;/strong> (&lt;code>ExternalSecret&lt;/code>) saying &amp;ldquo;the value of this key is in Vault, at this path&amp;rdquo;. The External Secrets operator resolves it at apply time. It is the cleanest pattern for many secrets and frequent rotation, at the cost of operating a Vault.&lt;/li>
&lt;/ul>
&lt;p>The choice depends on the volume of secrets and on whether you already have a central manager. For a small stack, SOPS+age is enough and has no dependencies. The &lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">sibling hardening piece&lt;/a> goes into the detail of each option, key rotation and the threat model.&lt;/p>
&lt;h2 id="promotion-and-rollback-git-revert-is-the-panic-button">Promotion and rollback: git revert is the panic button&lt;/h2>
&lt;p>The most elegant consequence of having the cluster as a projection of git is that &lt;strong>rollback is a &lt;code>git revert&lt;/code>&lt;/strong>. If a deployment breaks production, the new engine gives worse latency or the gateway starts returning 5xx, there is no need to remember which version was there before or to rebuild the state by hand. You revert the commit that introduced the change, and on the next reconciliation (≤ &lt;code>interval&lt;/code>, or instantly with a webhook) Flux returns the cluster to the previous state. The git history is, literally, the deployment history: every commit is a restore point with author, date and reason.&lt;/p>
&lt;p>This fits with the promotion strategies of the &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">canary, blue-green and shadow&lt;/a> post: Flux manages &lt;strong>what is declared&lt;/strong>, and the progressive rollout tool manages &lt;strong>how traffic moves&lt;/strong> between the old version and the new one. The git revert is the last-resort rollback (it returns everything to a known state); the canary is the mechanism that avoids needing it. For services where the version change is delicate, the right approach is for image automation to do the bump on a branch, for the canary to validate the regression gates, and only then to promote the commit to &lt;code>main&lt;/code>.&lt;/p>
&lt;h2 id="the-seams-gitops-is-not-free">The seams: GitOps is not free&lt;/h2>
&lt;p>It would be dishonest to sell GitOps as magic. It has real costs that any team pays:&lt;/p>
&lt;p>&lt;strong>Learning curve.&lt;/strong> The declarative model is a change of mindset. The operator who has spent years fixing incidents with &lt;code>kubectl edit&lt;/code> has to unlearn the reflex: now the cluster reverts their changes and that, at first, feels like Flux &amp;ldquo;fighting them&amp;rdquo;. Understanding that the change is made in git, not in the cluster, takes weeks of discomfort.&lt;/p>
&lt;p>&lt;strong>Debugging the reconciler.&lt;/strong> When a &lt;code>Kustomization&lt;/code> does not apply, the error is not in the pod; it is in the reconciliation chain. Did the source-controller clone the right revision? Did the kustomize-controller render the overlay properly? Did a &lt;code>dependsOn&lt;/code> health check fail? Did the timeout fire before the large model finished loading? Diagnosing this needs &lt;code>flux get&lt;/code>, &lt;code>flux logs&lt;/code>, reading the &lt;code>status.conditions&lt;/code> of the Flux objects and understanding which link got stuck. It is a new skill, different from debugging plain Kubernetes.&lt;/p>
&lt;p>&lt;strong>Timeouts and slow loads.&lt;/strong> The &lt;code>timeout&lt;/code> of a &lt;code>Kustomization&lt;/code> with &lt;code>wait: true&lt;/code> has to be longer than the start-up time of the slowest service. An inference engine that takes 4 minutes to load a large model from the object store will fail a &lt;code>Kustomization&lt;/code> with &lt;code>timeout: 2m&lt;/code>, even though everything is fine. Calibrating timeouts per service is fine-grained work.&lt;/p>
&lt;p>&lt;strong>The drift you did want.&lt;/strong> Sometimes the operator &lt;em>needs&lt;/em> an urgent temporary change and Flux reverts it. The correct answer, suspending reconciliation of that &lt;code>Kustomization&lt;/code> with &lt;code>flux suspend&lt;/code>, making the change, then reflecting it in git and resuming, is discipline that has to be built. Without that discipline, people end up fighting the foreman at 3 in the morning.&lt;/p>
&lt;p>None of these costs cancels the benefit. But a team adopting GitOps expecting &amp;ldquo;everything to be easier from day one&amp;rdquo; gets frustrated. It is easier &lt;strong>from month two onwards&lt;/strong>, once auditability, reproducibility and trivial rollback have paid off the curve.&lt;/p>
&lt;h2 id="applied-to-the-generic-4h100-cluster">Applied to the generic 4×H100 cluster&lt;/h2>
&lt;p>On the reference cluster (4×H100 SXM 80 GB per GPU node, NVLink, plus CPU and control nodes), the assistant stack is managed entirely by GitOps. Hardware decisions become data in the overlays.&lt;/p>
&lt;p>&lt;strong>nodeSelectors and MIG profiles as code.&lt;/strong> The &lt;code>prod&lt;/code> overlay pins each service to the right node and declares the GPU profile. The general engine uses a whole GPU; embeddings and the small LLM fit in MIG slices, exactly the split from the &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">sharing a GPU&lt;/a> post, but now versioned:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># apps/prod/llm-engine-patch.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">H100-SXM&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extraArgs&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="s2">&amp;#34;--gpu-memory-utilization=0.92&amp;#34;&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="s2">&amp;#34;--tensor-parallel-size=1&amp;#34;&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="nn">---&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="c"># apps/prod/embeddings-patch.yaml — on a MIG slice&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/mig.config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;3g.40gb&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/mig-3g.40gb&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The MIG profile (&lt;code>3g.40gb&lt;/code>), the VRAM fraction (&lt;code>0.92&lt;/code>), the tensor-parallel setting: all of it is text in a PR. Changing the GPU split between services is a reviewable commit, not a hand-run &lt;code>nvidia-smi mig&lt;/code> session that nobody records.&lt;/p>
&lt;p>&lt;strong>Start-up order with dependsOn.&lt;/strong> The assistant&amp;rsquo;s dependency graph is encoded with chained &lt;code>dependsOn&lt;/code>. The right order is &lt;strong>data → embeddings/engine → gateway → front end&lt;/strong>:&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 180" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="start-up order with dependsOn: data, engine and embeddings, gateway, front end">
&lt;defs>
&lt;marker id="da" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>&lt;/marker>
&lt;/defs>
&lt;text x="410" y="26" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">Assistant start-up order via dependsOn&lt;/text>
&lt;rect x="20" y="60" width="150" height="70" rx="8" fill="#8b5cf6" fill-opacity="0.12" stroke="#8b5cf6" stroke-width="1.6"/>
&lt;text x="95" y="88" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">data&lt;/text>
&lt;text x="95" y="106" text-anchor="middle" font-size="9.5" fill="currentColor">vector store&lt;/text>
&lt;text x="95" y="120" text-anchor="middle" font-size="9.5" fill="currentColor">Langfuse Postgres&lt;/text>
&lt;rect x="220" y="60" width="160" height="70" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.6"/>
&lt;text x="300" y="84" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">engine + embeddings&lt;/text>
&lt;text x="300" y="102" text-anchor="middle" font-size="9.5" fill="currentColor">vLLM (whole GPU)&lt;/text>
&lt;text x="300" y="116" text-anchor="middle" font-size="9.5" fill="currentColor">TEI (MIG slice)&lt;/text>
&lt;rect x="430" y="60" width="150" height="70" rx="8" fill="#3b82f6" fill-opacity="0.12" stroke="#3b82f6" stroke-width="1.6"/>
&lt;text x="505" y="88" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">L7 gateway&lt;/text>
&lt;text x="505" y="106" text-anchor="middle" font-size="9.5" fill="currentColor">per-model routing&lt;/text>
&lt;text x="505" y="120" text-anchor="middle" font-size="9.5" fill="currentColor">rate-limit · auth&lt;/text>
&lt;rect x="630" y="60" width="150" height="70" rx="8" fill="#22c55e" fill-opacity="0.12" stroke="#22c55e" stroke-width="1.6"/>
&lt;text x="705" y="88" text-anchor="middle" font-size="12" font-weight="700" fill="currentColor">chat front end&lt;/text>
&lt;text x="705" y="106" text-anchor="middle" font-size="9.5" fill="currentColor">assistant UI&lt;/text>
&lt;text x="705" y="120" text-anchor="middle" font-size="9.5" fill="currentColor">exposed to the client&lt;/text>
&lt;path d="M170,95 L218,95" fill="none" stroke="#64748b" stroke-width="1.8" marker-end="url(#da)"/>
&lt;path d="M380,95 L428,95" fill="none" stroke="#64748b" stroke-width="1.8" marker-end="url(#da)"/>
&lt;path d="M580,95 L628,95" fill="none" stroke="#64748b" stroke-width="1.8" marker-end="url(#da)"/>
&lt;text x="194" y="150" text-anchor="middle" font-size="9" fill="currentColor">dependsOn&lt;/text>
&lt;text x="404" y="150" text-anchor="middle" font-size="9" fill="currentColor">dependsOn&lt;/text>
&lt;text x="604" y="150" text-anchor="middle" font-size="9" fill="currentColor">dependsOn&lt;/text>
&lt;/svg>
&lt;/div>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># apps/prod/gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dependsOn&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-engine &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the gateway is useless without the engine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">embeddings&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="c"># ...&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="nn">---&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="c"># apps/prod/chat-front.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dependsOn&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the front end is useless without the gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With &lt;code>wait: true&lt;/code> on each &lt;code>Kustomization&lt;/code>, Flux does not mark &lt;code>llm-engine&lt;/code> as &lt;code>Ready&lt;/code> until the engine pod passes its health check, which in the case of vLLM means &lt;strong>model loaded and endpoint responding&lt;/strong>, not just pod started. Only then does it begin applying the gateway. This avoids the classic cascade of failures of manual deployment: bring up the front end first, see a 502 because the gateway is not there, bring up the gateway, see a 503 because the engine is still loading the model. With &lt;code>dependsOn&lt;/code> + &lt;code>wait&lt;/code>, the order is guaranteed by the reconciler, not by the operator&amp;rsquo;s memory.&lt;/p>
&lt;p>The result: deleting the assistant&amp;rsquo;s entire namespace and letting Flux rebuild it from git produces exactly the same stack, in the same order, with the same GPU configuration. The 4×H100 cluster stops having a configuration known only to whoever built it, and becomes a reproducible projection of a repo that anyone can audit.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>GitOps is not a tool, it is an &lt;strong>inversion of the direction of control&lt;/strong>: instead of pushing changes to the cluster, you declare the state in git and let an agent pull the cluster towards it. For an LLM assistant (six services, several namespaces, start-up dependencies, delicate GPU configuration) that inversion turns a fragile and opaque system into a reproducible and auditable one. Flux is the foreman who keeps the site identical to the plan and undoes any change that has not gone through the plan first. The price is a real learning curve and a new debugging skill. The prize is that the cluster stops keeping secrets about itself: everything it is, is written down, signed and reproducible in git.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">Hardening and secrets of the sovereign LLM stack&lt;/a> — the sibling piece: the detail of SOPS, sealed-secrets and External Secrets, key rotation and the threat model of the chicken-and-egg problem we only state here.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">Seven deployment phases of an on-premise LLM platform&lt;/a> — GitOps is phase F3 of that journey; here we deploy it, there it is placed in the full sequence.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels of the on-premise LLM platform&lt;/a> — moving from &lt;code>kubectl apply&lt;/code> to git as the sole authority is the level jump this post operationalises.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">LLM autoscaling on Kubernetes with KEDA&lt;/a> — the autoscaler coexists with GitOps: KEDA adjusts replicas by metric while Flux maintains the rest of the state; how they avoid fighting each other.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow for LLM models&lt;/a> — the progressive promotion that image automation triggers and that git revert backs up as a last-resort rollback.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/cpu-memory-topology-manager-rke2/">Kubelet resource managers in RKE2 and NUMA&lt;/a> — the &lt;code>nodeSelector&lt;/code>s and the topology of the GPU pods we declare here as code have their counterpart in kubelet policy.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">Sharing one GPU: time-slicing, MPS and MIG&lt;/a> — the MIG profiles and the &lt;code>--gpu-memory-utilization&lt;/code> that in this post are overlay data; there, the mechanics of why.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>OpenGitOps — GitOps principles — &lt;a href="https://opengitops.dev/">opengitops.dev&lt;/a>&lt;/li>
&lt;li>Flux installation — &lt;a href="https://fluxcd.io/flux/installation/">fluxcd.io/flux/installation&lt;/a>&lt;/li>
&lt;li>Flux Kustomization — &lt;a href="https://fluxcd.io/flux/components/kustomize/kustomizations/">fluxcd.io/flux/components/kustomize/kustomizations&lt;/a>&lt;/li>
&lt;li>helm-controller — &lt;a href="https://github.com/fluxcd/helm-controller">github.com/fluxcd/helm-controller&lt;/a>&lt;/li>
&lt;li>Flux Image Policies — &lt;a href="https://fluxcd.io/flux/components/image/imagepolicies/">fluxcd.io/flux/components/image/imagepolicies&lt;/a>&lt;/li>
&lt;li>Flux Image Update Automations — &lt;a href="https://fluxcd.io/flux/components/image/imageupdateautomations/">fluxcd.io/flux/components/image/imageupdateautomations&lt;/a>&lt;/li>
&lt;li>Automate image updates to Git — &lt;a href="https://fluxcd.io/flux/guides/image-update/">fluxcd.io/flux/guides/image-update&lt;/a>&lt;/li>
&lt;li>SOPS — &lt;a href="https://github.com/getsops/sops">github.com/getsops/sops&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>