<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://andrelair-platform.github.io/minicloud-platform-docs/blog</id>
    <title>Mini Cloud Platform Blog</title>
    <updated>2026-07-17T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://andrelair-platform.github.io/minicloud-platform-docs/blog"/>
    <subtitle>Mini Cloud Platform Blog</subtitle>
    <icon>https://andrelair-platform.github.io/minicloud-platform-docs/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Un modèle sort de l'entraînement à 28 Go. Tu le télécharges à 4.7 Go. Qui a fait la compression ?]]></title>
        <id>https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization</id>
        <link href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization"/>
        <updated>2026-07-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[La quantization ne sert pas qu'à réduire la RAM. Derrière cette technique, il y a trois objectifs concrets : démocratiser l'accès, accélérer l'inférence, et permettre le déploiement privé. Et entre le modèle sorti du labo et celui qui tourne sur ton cluster, il s'est passé beaucoup de choses.]]></summary>
        <content type="html"><![CDATA[<p>Un modèle sort de l'entraînement à 28 Go. Tu le télécharges à 4.7 Go. Qui a fait la compression entre les deux — et pourquoi ?</p>
<p>Ce post répond à cette question, et explique comment les modèles open-source circulent depuis les labos de recherche jusqu'à ton cluster.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-la-quantization-existe--trois-objectifs">Pourquoi la quantization existe : trois objectifs<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#pourquoi-la-quantization-existe--trois-objectifs" class="hash-link" aria-label="Direct link to Pourquoi la quantization existe : trois objectifs" title="Direct link to Pourquoi la quantization existe : trois objectifs" translate="no">​</a></h2>
<p>La quantization n'est pas une astuce technique pour "faire tenir un modèle". C'est une réponse à trois problèmes concrets.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-démocratiser-laccès">1. Démocratiser l'accès<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#1-d%C3%A9mocratiser-lacc%C3%A8s" class="hash-link" aria-label="Direct link to 1. Démocratiser l'accès" title="Direct link to 1. Démocratiser l'accès" translate="no">​</a></h3>
<p>Sans quantization, un modèle 7B en FP16 occupe 14 Go de VRAM. Il te faut une RTX 3080 (700€ minimum) ou une A100 (10 000€ en datacenter). Seuls les labos de recherche bien financés et les grandes entreprises peuvent faire de l'inférence locale.</p>
<p>Avec Q4_K_M, ce même modèle tient en 4.7 Go de RAM. Un ThinkPad d'occasion à 300€ peut le faire tourner. C'est ce changement qui a déclenché l'explosion de l'open-source LLM en 2023 : le jour où des modèles de qualité sont devenus accessibles sur du hardware grand public.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-accélérer-linférence">2. Accélérer l'inférence<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#2-acc%C3%A9l%C3%A9rer-linf%C3%A9rence" class="hash-link" aria-label="Direct link to 2. Accélérer l'inférence" title="Direct link to 2. Accélérer l'inférence" translate="no">​</a></h3>
<p>Moins de bits ne signifie pas seulement moins de mémoire — cela signifie aussi moins de données à lire depuis la RAM à chaque calcul.</p>
<p>Sur CPU, la vitesse de génération de tokens est souvent limitée par la <strong>bande passante mémoire</strong> : le processeur doit lire les poids du modèle à chaque étape de génération. Un modèle Q4 lit 8× moins de données qu'un modèle FP32. En pratique, un 7B Q4_K_M génère souvent 1.5 à 2× plus de tokens par seconde qu'un 7B Q8 — pas seulement parce qu'il est plus petit, mais parce que les lectures mémoire sont plus rapides.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-permettre-le-déploiement-privé">3. Permettre le déploiement privé<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#3-permettre-le-d%C3%A9ploiement-priv%C3%A9" class="hash-link" aria-label="Direct link to 3. Permettre le déploiement privé" title="Direct link to 3. Permettre le déploiement privé" translate="no">​</a></h3>
<p>C'est l'objectif le plus stratégique pour les entreprises.</p>
<p>Si tu utilises l'API OpenAI ou Groq, chaque message de tes utilisateurs traverse les serveurs d'une entreprise tierce. Pour un usage personnel ou grand public, c'est acceptable. Pour des données médicales, financières, ou industrielles, c'est souvent impossible légalement et inacceptable en termes de confidentialité.</p>
<p>Un LLM quantisé qui tourne en local résout ce problème par l'architecture : les données ne quittent jamais l'infrastructure. Sur minicloud, les interactions avec Open WebUI ne quittent pas le cluster. C'est une propriété garantie par le design, pas par une promesse contractuelle.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-cycle-de-vie-dun-modèle-open-source">Le cycle de vie d'un modèle open-source<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#le-cycle-de-vie-dun-mod%C3%A8le-open-source" class="hash-link" aria-label="Direct link to Le cycle de vie d'un modèle open-source" title="Direct link to Le cycle de vie d'un modèle open-source" translate="no">​</a></h2>
<p>Il y a une question que peu de gens se posent quand ils font <code>ollama pull</code> : d'où vient ce fichier, et qui l'a préparé ?</p>
<p>Voici ce qui se passe entre la sortie d'un modèle et son exécution sur ton cluster.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-1--entraînement-par-le-labo">Étape 1 — Entraînement par le labo<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#%C3%A9tape-1--entra%C3%AEnement-par-le-labo" class="hash-link" aria-label="Direct link to Étape 1 — Entraînement par le labo" title="Direct link to Étape 1 — Entraînement par le labo" translate="no">​</a></h3>
<p>Un laboratoire (Meta, Alibaba, Mistral, Microsoft...) entraîne le modèle sur des milliers de GPUs pendant des semaines ou des mois. Le résultat : des poids en <strong>FP32 ou BF16</strong> — la représentation la plus précise possible des connaissances acquises.</p>
<p>Ces poids sont publiés sur <strong>HuggingFace</strong>. Le modèle original de Qwen 2.5 7B par Alibaba, par exemple, est disponible là-bas en BF16 (~14 Go).</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-2--quantization-par-la-communauté">Étape 2 — Quantization par la communauté<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#%C3%A9tape-2--quantization-par-la-communaut%C3%A9" class="hash-link" aria-label="Direct link to Étape 2 — Quantization par la communauté" title="Direct link to Étape 2 — Quantization par la communauté" translate="no">​</a></h3>
<p>Des membres de la communauté open-source prennent ces poids FP16/BF16 et créent des versions quantisées. Les noms les plus connus dans cet écosystème : <strong>bartowski</strong>, <strong>TheBloke</strong>, <strong>unsloth</strong>.</p>
<p>Leur travail :</p>
<ol>
<li class="">Télécharger les poids originaux depuis HuggingFace</li>
<li class="">Appliquer llama.cpp ou d'autres outils pour créer les fichiers GGUF</li>
<li class="">Générer toutes les variantes : Q2_K, Q3_K_M, Q4_K_M, Q5_K_M, Q8_0</li>
<li class="">Publier ces fichiers dérivés sur HuggingFace</li>
</ol>
<p>C'est un travail bénévole, non rémunéré, qui rend l'écosystème possible.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-3--packaging-par-ollama">Étape 3 — Packaging par Ollama<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#%C3%A9tape-3--packaging-par-ollama" class="hash-link" aria-label="Direct link to Étape 3 — Packaging par Ollama" title="Direct link to Étape 3 — Packaging par Ollama" translate="no">​</a></h3>
<p>Ollama récupère les fichiers GGUF créés par la communauté et les rend disponibles via son registry (<code>registry.ollama.ai</code>). Il ajoute un <code>Modelfile</code> qui précise le template de prompt, le system message par défaut, et les paramètres d'inférence recommandés.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"># Ce que tu fais :</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ollama pull qwen2.5:7b-instruct-q4_k_m</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># Ce qui se passe :</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># → Ollama contacte registry.ollama.ai</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># → Télécharge les blobs GGUF Q4_K_M (~4.7 Go)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># → Les stocke dans /root/.ollama/models/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># → Le modèle est prêt à l'inférence</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-4--inférence-sur-ton-cluster">Étape 4 — Inférence sur ton cluster<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#%C3%A9tape-4--inf%C3%A9rence-sur-ton-cluster" class="hash-link" aria-label="Direct link to Étape 4 — Inférence sur ton cluster" title="Direct link to Étape 4 — Inférence sur ton cluster" translate="no">​</a></h3>
<p>Le modèle tourne dans un pod Ollama, pinné sur un nœud spécifique. LiteLLM route les requêtes vers l'instance la moins occupée. L'utilisateur voit une réponse en quelques secondes.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Utilisateur → Open WebUI → LiteLLM → Ollama (pod fast-heron)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                                           ↓</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                                   qwen2.5:7b-instruct-q4_k_m</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                                   (GGUF Q4_K_M, 4.7 Go en RAM)</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="modèles-non-quantisés-vs-quantisés--ce-sont-des-artefacts-différents">Modèles non-quantisés vs quantisés — ce sont des artefacts différents<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#mod%C3%A8les-non-quantis%C3%A9s-vs-quantis%C3%A9s--ce-sont-des-artefacts-diff%C3%A9rents" class="hash-link" aria-label="Direct link to Modèles non-quantisés vs quantisés — ce sont des artefacts différents" title="Direct link to Modèles non-quantisés vs quantisés — ce sont des artefacts différents" translate="no">​</a></h2>
<p>Une confusion fréquente : on parle de "le modèle" comme s'il n'en existait qu'une version. En réalité :</p>
<table><thead><tr><th>Type</th><th>Format</th><th>Poids</th><th>Usage typique</th></tr></thead><tbody><tr><td>Original (sorti du labo)</td><td>FP32 / BF16</td><td>14–28 Go (7B)</td><td>Fine-tuning, recherche</td></tr><tr><td>Quantisé par la communauté</td><td>GGUF Q4_K_M</td><td>4.7 Go (7B)</td><td>Inférence sur hardware grand public</td></tr><tr><td>Spécialisé par Modelfile</td><td>GGUF (poids inchangés)</td><td>identique au base</td><td>Pipeline avec persona + comportement custom</td></tr><tr><td>Fine-tuné + quantisé</td><td>GGUF custom</td><td>variable</td><td>Spécialisation profonde, nouvelle connaissance</td></tr></tbody></table>
<p>Le modèle original et ses versions quantisées <strong>coexistent</strong> sur HuggingFace. Elles ne se remplacent pas — elles servent des usages différents.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="les-3-vraies-façons-de-spécialiser-un-llm">Les 3 vraies façons de spécialiser un LLM<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#les-3-vraies-fa%C3%A7ons-de-sp%C3%A9cialiser-un-llm" class="hash-link" aria-label="Direct link to Les 3 vraies façons de spécialiser un LLM" title="Direct link to Les 3 vraies façons de spécialiser un LLM" translate="no">​</a></h2>
<p>Avant d'aller plus loin, une distinction essentielle que beaucoup de ressources mélangent.</p>
<table><thead><tr><th>Technique</th><th>Poids modifiés ?</th><th>GPU requis ?</th><th>Ce que ça change</th></tr></thead><tbody><tr><td><strong>System prompt / Modelfile</strong></td><td>Non</td><td>Non</td><td>Comportement et persona</td></tr><tr><td><strong>RAG</strong></td><td>Non</td><td>Non</td><td>Accès à des données fraîches et privées</td></tr><tr><td><strong>Fine-tuning</strong></td><td>Oui</td><td>Oui</td><td>La connaissance encodée dans les paramètres</td></tr></tbody></table>
<p>Ces trois techniques sont complémentaires, pas substituables. On peut les combiner — et c'est exactement ce que fait le pipeline <code>phi3-financial</code> sur minicloud.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-cas-de-phi3-financial--modelfile-pas-fine-tuning">Le cas de <code>phi3-financial</code> — Modelfile, pas fine-tuning<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#le-cas-de-phi3-financial--modelfile-pas-fine-tuning" class="hash-link" aria-label="Direct link to le-cas-de-phi3-financial--modelfile-pas-fine-tuning" title="Direct link to le-cas-de-phi3-financial--modelfile-pas-fine-tuning" translate="no">​</a></h2>
<p>Il est tentant d'appeler <code>phi3-financial</code> un modèle fine-tuné parce qu'il a un nom distinct dans <code>ollama list</code>. Mais la preuve par les chiffres contredit cette interprétation :</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">kubectl exec -n ai deployment/ollama -- ollama list</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">phi4-mini:latest       78fad5d182a7    2.5 GB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">phi3-financial:latest  66e3380808ef    2.5 GB</span><br></div></code></pre></div></div>
<p><strong>Exactement la même taille.</strong> Un vrai fine-tuning modifie les valeurs des poids — la taille du fichier GGUF résultant varie légèrement. Ici les deux fichiers font 2.5 GB, ce qui indique que les poids sont identiques.</p>
<p><code>phi3-financial</code> est un <strong>Ollama Modelfile</strong> : une configuration qui enveloppe <code>phi4-mini</code> avec un system prompt et des paramètres d'inférence, sans toucher aux poids.</p>
<div class="language-dockerfile codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-dockerfile codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">FROM phi4-mini</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">SYSTEM """</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Tu es un expert en analyse financière spécialisé dans le secteur des assurances.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Tu analyses uniquement des données financières et réponds de façon précise et factuelle.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">"""</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">PARAMETER temperature 0.3</span><br></div></code></pre></div></div>
<p>Quand tu fais <code>ollama create phi3-financial -f Modelfile</code>, Ollama crée une nouvelle entrée dans son registry avec ce nom et cette configuration. Les poids sont <strong>identiques</strong> à ceux de <code>phi4-mini</code> — aucun entraînement n'a eu lieu.</p>
<p>La spécialisation de <code>phi3-financial</code> vient en réalité de trois couches empilées :</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Requête utilisateur</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">LangfusePromptHandler (LiteLLM CustomLogger)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    │ Injecte le system prompt financier depuis Langfuse</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    │ (versioning des prompts, A/B testing possible)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">phi3-financial Modelfile</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    │ Applique temperature=0.3, contraintes de persona</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">phi4-mini GGUF Q4_K_M</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    │ Poids identiques au modèle original Microsoft</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Réponse générée</span><br></div></code></pre></div></div>
<p>C'est une <strong>spécialisation par prompt engineering en profondeur</strong>, pas du fine-tuning. C'est parfaitement valide et largement utilisé en production — plus rapide à itérer, plus facile à maintenir, aucun GPU requis.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="quand-le-fine-tuning-devient-nécessaire">Quand le fine-tuning devient nécessaire<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#quand-le-fine-tuning-devient-n%C3%A9cessaire" class="hash-link" aria-label="Direct link to Quand le fine-tuning devient nécessaire" title="Direct link to Quand le fine-tuning devient nécessaire" translate="no">​</a></h2>
<p>Le fine-tuning n'est justifié que dans des cas précis :</p>
<p><strong>Nouvelles connaissances factuelles</strong> — si le modèle doit connaître des faits qui n'étaient pas dans ses données d'entraînement (documentation interne, terminologie propriétaire, données post-cutoff).</p>
<p><strong>Comportement très contraignant</strong> — si le system prompt seul ne suffit pas à maintenir le persona sous des prompts adversariaux.</p>
<p><strong>Efficacité à l'inférence</strong> — un modèle fine-tuné sur une tâche précise peut atteindre de meilleures performances avec un context plus court, ce qui réduit les coûts.</p>
<p>Pour <code>phi3-financial</code>, le RAG + LangfusePromptHandler couvre les deux premiers besoins sans les coûts d'un fine-tuning réel. Si le pipeline nécessitait un jour de connaître des règlements ACPR très spécifiques absents du pre-training, le fine-tuning deviendrait pertinent.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-ça-implique-concrètement-pour-linférence">Ce que ça implique concrètement pour l'inférence<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#ce-que-%C3%A7a-implique-concr%C3%A8tement-pour-linf%C3%A9rence" class="hash-link" aria-label="Direct link to Ce que ça implique concrètement pour l'inférence" title="Direct link to Ce que ça implique concrètement pour l'inférence" translate="no">​</a></h2>
<p>Ces types de modèles ne s'utilisent pas de la même façon dans un stack de production.</p>
<p><strong>Le modèle original (FP16)</strong> — uniquement pour le fine-tuning. Il faut au minimum un GPU avec 14 Go de VRAM et du temps de calcul. C'est l'entrée d'un pipeline ML, pas sa sortie.</p>
<p><strong>Le modèle quantisé (GGUF Q4_K_M)</strong> — ce que tu déploies en production pour l'inférence. Tourne sur CPU, sur des GPUs grand public, sur du hardware embarqué. C'est la sortie du pipeline, celle qui crée de la valeur.</p>
<p><strong>Le Modelfile</strong> — une couche de configuration au-dessus d'un modèle quantisé existant. Zéro coût de calcul supplémentaire, itération rapide sur le comportement. C'est l'état réel de <code>phi3-financial</code> sur minicloud.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-cest-important-pour-le-déploiement-en-entreprise">Pourquoi c'est important pour le déploiement en entreprise<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-model-lifecycle-quantization#pourquoi-cest-important-pour-le-d%C3%A9ploiement-en-entreprise" class="hash-link" aria-label="Direct link to Pourquoi c'est important pour le déploiement en entreprise" title="Direct link to Pourquoi c'est important pour le déploiement en entreprise" translate="no">​</a></h2>
<p>Dans un contexte professionnel, ces quatre niveaux correspondent à des rôles distincts :</p>
<table><thead><tr><th>Artefact</th><th>Équipe responsable</th></tr></thead><tbody><tr><td>Modèle FP16 original</td><td>Data scientists / ML researchers</td></tr><tr><td>Modèle GGUF quantisé</td><td>MLOps / Platform engineers</td></tr><tr><td>Modelfile + system prompt</td><td>AI engineers / Prompt engineers</td></tr><tr><td>RAG pipeline</td><td>AI engineers + Data engineers</td></tr><tr><td>Fine-tuning</td><td>ML engineers (besoin GPU, dataset, évaluation)</td></tr></tbody></table>
<p>Sur minicloud, ces rôles sont joués par une seule personne. En entreprise, ce sont des équipes distinctes avec des compétences différentes. Comprendre quelle technique appartient à quel niveau — et pourquoi — est ce qui distingue un ingénieur qui "utilise des LLMs" d'un ingénieur qui "conçoit des systèmes AI".</p>]]></content>
        <author>
            <name>Andre Kanmegne</name>
            <uri>https://www.devandre.sbs</uri>
        </author>
        <category label="ai" term="ai"/>
        <category label="llm" term="llm"/>
        <category label="quantization" term="quantization"/>
        <category label="ollama" term="ollama"/>
        <category label="fine-tuning" term="fine-tuning"/>
        <category label="mlops" term="mlops"/>
        <category label="open-source" term="open-source"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[La quantization LLM démystifiée : comment faire tourner un 7B sur un ThinkPad]]></title>
        <id>https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified</id>
        <link href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified"/>
        <updated>2026-07-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Un 7B en FP32 c'est 28 Go de RAM. Sur mon cluster ThinkPad, impossible. La quantization m'a permis de descendre à 4.7 Go sans perdre la qualité qui compte. Voici comment ça fonctionne.]]></summary>
        <content type="html"><![CDATA[<p>Un modèle 7B en FP32, c'est 28 Go de RAM. Mes ThinkPads en ont 16 à 32. Impossible de charger le modèle — sans parler de faire de l'inférence.</p>
<p>La quantization a résolu ce problème. Pas en sacrifiant la qualité — en changeant la façon dont les poids sont stockés.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-quest-un-paramètre-concrètement">Ce qu'est un paramètre, concrètement<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#ce-quest-un-param%C3%A8tre-concr%C3%A8tement" class="hash-link" aria-label="Direct link to Ce qu'est un paramètre, concrètement" title="Direct link to Ce qu'est un paramètre, concrètement" translate="no">​</a></h2>
<p>Un LLM "7B" a 7 milliards de paramètres. Chaque paramètre est un nombre en virgule flottante — un poids appris pendant l'entraînement qui encode une fraction des patterns vus dans les données.</p>
<p>En <strong>FP32</strong> (float 32 bits), chaque paramètre occupe 4 octets.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">7 000 000 000 paramètres × 4 octets = 28 000 000 000 octets = 28 Go</span><br></div></code></pre></div></div>
<p>28 Go juste pour charger le modèle en RAM. Sans compter le KV cache pendant l'inférence, les activations, l'overhead du runtime.</p>
<p>Sur un ThinkPad avec 16 Go de RAM système et 8 à 12 Go disponibles pour les workloads : <strong>impossible</strong>.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-principe-de-la-quantization">Le principe de la quantization<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#le-principe-de-la-quantization" class="hash-link" aria-label="Direct link to Le principe de la quantization" title="Direct link to Le principe de la quantization" translate="no">​</a></h2>
<p>La quantization réduit la précision de chaque paramètre — moins de bits par nombre, moins de mémoire, au prix d'une légère perte de précision.</p>
<p>L'analogie : imagine que tu mesures une distance. En FP32, tu utilises une règle au millimètre. En Q4, tu utilises une règle au centimètre. Pour construire une maison, la différence est négligeable. Pour de l'horlogerie de précision, ça change tout.</p>
<p>Pour les LLMs, la grande majorité des tâches — conversation, résumé, code, Q&amp;A — tolèrent très bien la réduction de précision des poids. Le réseau neuronal est redondant par nature : des millions de paramètres collaborent pour chaque réponse, et une légère imprécision sur chacun ne change pas le résultat global de façon perceptible.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-hiérarchie-des-formats">La hiérarchie des formats<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#la-hi%C3%A9rarchie-des-formats" class="hash-link" aria-label="Direct link to La hiérarchie des formats" title="Direct link to La hiérarchie des formats" translate="no">​</a></h2>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">FP32    4 bytes/param   7B = 28.0 Go   référence</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">FP16    2 bytes/param   7B = 14.0 Go   </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Q8_0    1 byte/param    7B =  7.0 Go   </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Q4_K_M  ~0.5 byte       7B =  4.7 Go   ← le point d'équilibre</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Q3_K_M  ~0.4 byte       7B =  3.9 Go   dégradation notable</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Q2_K    ~0.25 byte      7B =  2.7 Go   à éviter</span><br></div></code></pre></div></div>
<p>Chaque niveau vers le bas divise approximativement la mémoire par deux, mais la perte de qualité n'est pas linéaire. De FP32 à Q4_K_M, la dégradation est quasi imperceptible en pratique. De Q4 à Q2, elle devient audible — hallucinations plus fréquentes, raisonnement moins stable.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-signifient-les-suffixes">Ce que signifient les suffixes<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#ce-que-signifient-les-suffixes" class="hash-link" aria-label="Direct link to Ce que signifient les suffixes" title="Direct link to Ce que signifient les suffixes" translate="no">​</a></h2>
<p>Si tu vas sur Ollama ou HuggingFace, tu verras des noms comme <code>Q4_K_M</code>, <code>Q5_K_S</code>, <code>IQ3_XS</code>. Voici comment les lire :</p>
<p><strong>Le chiffre</strong> — le nombre de bits par paramètre.</p>
<ul>
<li class=""><code>Q4</code> = 4 bits</li>
<li class=""><code>Q5</code> = 5 bits</li>
<li class=""><code>Q8</code> = 8 bits</li>
</ul>
<p><strong>La lettre après le chiffre</strong> — la méthode de quantization.</p>
<ul>
<li class=""><code>_K</code> = "K-quant" — une méthode qui quantize différemment selon l'importance relative de chaque couche du réseau. Plus intelligente que la quantization uniforme.</li>
<li class=""><code>_0</code> = quantization uniforme basique (plus ancienne).</li>
</ul>
<p><strong>La dernière lettre</strong> — la taille des "blocs" de calcul.</p>
<ul>
<li class=""><code>_S</code> (Small) = plus petit, légèrement plus rapide, légèrement moins précis.</li>
<li class=""><code>_M</code> (Medium) = équilibre qualité/vitesse.</li>
<li class=""><code>_L</code> (Large) = plus précis, un peu plus lent.</li>
</ul>
<p><strong>Résumé pratique :</strong></p>
<ul>
<li class=""><code>Q4_K_M</code> = 4 bits, K-quant, taille medium → <strong>le choix par défaut</strong>.</li>
<li class=""><code>Q5_K_M</code> = 5 bits, K-quant, medium → +20% de RAM, presque aucune perte vs Q4.</li>
<li class=""><code>Q8_0</code> = 8 bits, uniforme → pratiquement identique à FP16.</li>
<li class=""><code>IQ3_XS</code> = une variante "importance-aware" à 3 bits → pour situations très contraintes.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="les-quatre-familles-de-formats">Les quatre familles de formats<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#les-quatre-familles-de-formats" class="hash-link" aria-label="Direct link to Les quatre familles de formats" title="Direct link to Les quatre familles de formats" translate="no">​</a></h2>
<p>Selon ton hardware, tu croiseras quatre formats principaux :</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="gguf--le-format-cpu-first">GGUF — le format CPU-first<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#gguf--le-format-cpu-first" class="hash-link" aria-label="Direct link to GGUF — le format CPU-first" title="Direct link to GGUF — le format CPU-first" translate="no">​</a></h3>
<p>Développé par llama.cpp, utilisé par <strong>Ollama</strong>. Conçu pour tourner sur CPU avec possibilité d'offloader des couches vers un GPU si disponible.</p>
<p>C'est le format qu'on utilise sur minicloud. Tous les suffixes <code>Q4_K_M</code>, <code>Q5_K_M</code>, etc. font référence à GGUF.</p>
<p><strong>Avantage :</strong> fonctionne sans GPU, très bien optimisé pour les architectures x86 modernes.
<strong>Inconvénient :</strong> moins efficace qu'un format GPU-natif si tu as un GPU.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="gptq--quantization-post-training-orientée-gpu">GPTQ — quantization post-training orientée GPU<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#gptq--quantization-post-training-orient%C3%A9e-gpu" class="hash-link" aria-label="Direct link to GPTQ — quantization post-training orientée GPU" title="Direct link to GPTQ — quantization post-training orientée GPU" translate="no">​</a></h3>
<p>Quantize les poids après entraînement en minimisant l'erreur de reconstruction. Très bon si tu as assez de VRAM.</p>
<p><strong>Avantage :</strong> bonne qualité sur GPU.
<strong>Inconvénient :</strong> CPU-unfriendly, nécessite ExLlama ou AutoGPTQ pour l'inférence.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="awq--activation-aware-weight-quantization">AWQ — Activation-aware Weight Quantization<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#awq--activation-aware-weight-quantization" class="hash-link" aria-label="Direct link to AWQ — Activation-aware Weight Quantization" title="Direct link to AWQ — Activation-aware Weight Quantization" translate="no">​</a></h3>
<p>Plus récent que GPTQ. L'idée : certains paramètres sont plus importants que d'autres (ceux qui activent souvent). AWQ les protège en les quantizant moins agressivement.</p>
<p><strong>Avantage :</strong> meilleure qualité que GPTQ à même taille, de plus en plus standard.
<strong>Inconvénient :</strong> nécessite un runtime compatible (vLLM, AutoAWQ).</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="exl2--exllamav2">EXL2 — ExLlamaV2<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#exl2--exllamav2" class="hash-link" aria-label="Direct link to EXL2 — ExLlamaV2" title="Direct link to EXL2 — ExLlamaV2" translate="no">​</a></h3>
<p>Format propriétaire à ExLlamaV2, très efficace pour l'inférence GPU en batch. Permet des taux de bits mixtes par couche.</p>
<p><strong>Avantage :</strong> débit maximal sur GPU NVIDIA.
<strong>Inconvénient :</strong> écosystème plus fermé, pas pour CPU.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-règle-dor">La règle d'or<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#la-r%C3%A8gle-dor" class="hash-link" aria-label="Direct link to La règle d'or" title="Direct link to La règle d'or" translate="no">​</a></h2>
<blockquote>
<p>Descends d'un niveau de quantization avant de descendre à un modèle plus petit.</p>
</blockquote>
<p>Un 7B Q4 surpasse un 3B Q8 sur presque toutes les tâches — raisonnement, suivi d'instructions, cohérence longue. Les 7 milliards de paramètres, même légèrement imprécis, encodent plus de connaissance que 3 milliards de paramètres très précis.</p>
<p>Concrètement sur minicloud :</p>
<table><thead><tr><th>Option</th><th>RAM</th><th>Qualité relative</th></tr></thead><tbody><tr><td>qwen2.5 3B Q8</td><td>3.0 Go</td><td>Baseline</td></tr><tr><td>qwen2.5 7B Q4_K_M</td><td>4.7 Go</td><td>+20–30% qualité</td></tr><tr><td>qwen2.5 7B Q5_K_M</td><td>5.8 Go</td><td>+22–32% qualité</td></tr></tbody></table>
<p>Pour 1.7 Go de RAM supplémentaire, on gagne un saut de qualité massif.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="limpact-sur-la-qualité--les-chiffres-réels">L'impact sur la qualité — les chiffres réels<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#limpact-sur-la-qualit%C3%A9--les-chiffres-r%C3%A9els" class="hash-link" aria-label="Direct link to L'impact sur la qualité — les chiffres réels" title="Direct link to L'impact sur la qualité — les chiffres réels" translate="no">​</a></h2>
<p>Le standard de mesure de la dégradation par quantization est la <strong>perplexité</strong> — une mesure de la "surprise" du modèle face à un texte de référence. Plus la perplexité est basse, mieux le modèle prédit le langage.</p>
<p>Sur Llama 3 8B (valeurs représentatives de la famille 7-8B) :</p>
<table><thead><tr><th>Format</th><th>Perplexité (WikiText-2)</th><th>Différence vs FP16</th></tr></thead><tbody><tr><td>FP16</td><td>6.12</td><td>référence</td></tr><tr><td>Q8_0</td><td>6.13</td><td>+0.01 (+0.2%)</td></tr><tr><td>Q4_K_M</td><td>6.22</td><td>+0.10 (+1.6%)</td></tr><tr><td>Q3_K_M</td><td>6.47</td><td>+0.35 (+5.7%)</td></tr><tr><td>Q2_K</td><td>7.15</td><td>+1.03 (+16.8%)</td></tr></tbody></table>
<p>En pratique : entre FP16 et Q4_K_M, la différence de perplexité est de 1.6%. Dans une conversation réelle, c'est imperceptible. Entre Q4 et Q2, le saut de 16.8% correspond à des réponses notablement moins cohérentes sur les tâches complexes.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-ça-donne-sur-le-cluster-minicloud">Ce que ça donne sur le cluster minicloud<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#ce-que-%C3%A7a-donne-sur-le-cluster-minicloud" class="hash-link" aria-label="Direct link to Ce que ça donne sur le cluster minicloud" title="Direct link to Ce que ça donne sur le cluster minicloud" translate="no">​</a></h2>
<p>Le cluster tourne sur des ThinkPads CPU-only (pas de GPU). La contrainte est réelle.</p>
<p>Avant : <code>phi4-mini</code> (3.8B, format propriétaire Microsoft) — 2.5 Go en RAM, ~15 tokens/sec sur ThinkPad.</p>
<p>Après : <code>qwen2.5:7b-instruct-q4_k_m</code> (7B, GGUF Q4_K_M) — 4.7 Go en RAM, ~10 tokens/sec.</p>
<p>On perd 5 tokens/sec (vitesse), on gagne 15–20% de qualité sur les benchmarks. Pour du chat interactif où l'utilisateur lit à 4–5 mots/seconde, 10 tokens/sec est largement suffisant.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"># Vérifier les modèles chargés sur les 3 instances Ollama</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl exec -n ai deployment/ollama -- ollama list</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl exec -n ai deployment/ollama-secondary -- ollama list</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl exec -n ai deployment/ollama-tertiary -- ollama list</span><br></div></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">NAME                          ID              SIZE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">qwen2.5:7b-instruct-q4_k_m    845dbda0ea48    4.7 GB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">phi4-mini:latest               78fad5d182a7    2.5 GB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">deepseek-r1:7b                 755ced02ce7b    4.7 GB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">...</span><br></div></code></pre></div></div>
<p>Trois instances Ollama, chacune sur un nœud différent. LiteLLM route les requêtes vers l'instance la moins occupée. La quantization Q4_K_M est ce qui rend ce setup possible sur du hardware grand public.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="comment-choisir">Comment choisir<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#comment-choisir" class="hash-link" aria-label="Direct link to Comment choisir" title="Direct link to Comment choisir" translate="no">​</a></h2>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Tu as un GPU avec assez de VRAM ?</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Oui → AWQ ou GPTQ (meilleure qualité à même taille que GGUF)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└── Non → GGUF, et le format :</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    RAM disponible pour le modèle ?</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    ├── &lt; 5 Go    → Q4_K_M  (défaut)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    ├── 5–7 Go    → Q5_K_M  (si tu peux)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    ├── 7–15 Go   → Q8_0    (quasi-FP16)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    └── &gt; 15 Go   → FP16    (référence)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Q2 ou Q3 ?</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    └── Éviter sauf contrainte extrême (edge, IoT, &lt;3 Go impératif)</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-quon-retient">Ce qu'on retient<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-quantization-demystified#ce-quon-retient" class="hash-link" aria-label="Direct link to Ce qu'on retient" title="Direct link to Ce qu'on retient" translate="no">​</a></h2>
<p>La quantization n'est pas un compromis entre "bon modèle" et "modèle qui tient en RAM". C'est une technique mature, mesurée, avec des pertes de qualité documentées et prévisibles.</p>
<p>Q4_K_M est devenu le standard de facto de l'écosystème open-source pour une raison : c'est le point où la perte de précision devient théorique plutôt que pratique pour la grande majorité des tâches.</p>
<p>Sur minicloud, c'est ce qui permet de faire tourner un LLM 7B de qualité production sur du matériel qu'on aurait jeté il y a trois ans.</p>]]></content>
        <author>
            <name>Andre Kanmegne</name>
            <uri>https://www.devandre.sbs</uri>
        </author>
        <category label="ai" term="ai"/>
        <category label="llm" term="llm"/>
        <category label="quantization" term="quantization"/>
        <category label="ollama" term="ollama"/>
        <category label="inference" term="inference"/>
        <category label="mlops" term="mlops"/>
        <category label="bare-metal" term="bare-metal"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why k3s? Installing Kubernetes on 5 Laptops with a Single curl Command]]></title>
        <id>https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal</id>
        <link href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal"/>
        <updated>2026-07-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How k3s was installed on a 5-node bare-metal cluster — one curl command per machine — and why k3s beat kubeadm, microk8s, RKE2, and k0s for this specific hardware.
]]></summary>
        <content type="html"><![CDATA[<p>The hardware was provisioned. MAAS had PXE-booted four ThinkPads and cloud-init had written the SSH keys and hostnames. The next question was: <strong>how do you actually get Kubernetes running on five laptops?</strong></p>
<p>There are more ways to install Kubernetes than there are Kubernetes certification paths. I ended up with k3s. Here is why — and exactly what the installation looked like.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-constraint-that-made-the-decision">The Constraint That Made the Decision<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal#the-constraint-that-made-the-decision" class="hash-link" aria-label="Direct link to The Constraint That Made the Decision" title="Direct link to The Constraint That Made the Decision" translate="no">​</a></h2>
<p>Choosing a Kubernetes distribution for this cluster was not a philosophical debate. The hardware made the decision for me.</p>
<p>The cluster includes a <strong>MacBook Pro 13" (late 2012)</strong> — an Intel Core i5-3210M (dual-core, 2012 vintage), 8 GB DDR3 RAM. That machine became <code>swift-mac</code>, running Ubuntu 22.04 as a Longhorn storage worker. The MacBook cannot be provisioned via PXE (Apple's EFI firmware does not support network boot), so Ubuntu was installed manually from a USB drive. After that, it joins the cluster exactly like any other node.</p>
<p>If the MacBook can run a k3s agent without being starved of RAM, any node in this cluster can. That is the selection criterion.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-options-and-why-they-fell-short">The Options and Why They Fell Short<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal#the-options-and-why-they-fell-short" class="hash-link" aria-label="Direct link to The Options and Why They Fell Short" title="Direct link to The Options and Why They Fell Short" translate="no">​</a></h2>
<table><thead><tr><th>Distribution</th><th>Why not</th></tr></thead><tbody><tr><td><strong>kubeadm</strong></td><td>The "official" method — but it requires you to install containerd, a CNI plugin, and etcd separately before you can even initialise the control plane. The control plane alone consumes 2 GB+ at idle. Too heavy, too many moving parts, 40 pages of documentation before hello world.</td></tr><tr><td><strong>minikube / kind</strong></td><td>Single-node, designed for local development and CI runners. Cannot form a real multi-node bare-metal cluster.</td></tr><tr><td><strong>microk8s</strong></td><td>Snap-dependent. Hard-coupled to Ubuntu's package manager, non-trivial overhead from the snap runtime, less portable across distros.</td></tr><tr><td><strong>RKE2</strong></td><td>Rancher's hardened distribution — CIS-compliant by default, excellent for regulated enterprise production. More resource-intensive than k3s and over-engineered for a homelab. Worth revisiting if a second cluster is ever built to enterprise spec.</td></tr><tr><td><strong>k0s</strong></td><td>Comparable in weight to k3s, but the ecosystem was less mature when this project started — fewer community examples, less tooling, less documentation.</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-k3s">Why k3s<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal#why-k3s" class="hash-link" aria-label="Direct link to Why k3s" title="Direct link to Why k3s" translate="no">​</a></h2>
<p>k3s wins on four criteria that actually matter for this hardware:</p>
<p><strong>1. Single binary, everything included.</strong><br>
<!-- -->containerd, CoreDNS, flannel (CNI), kube-proxy, and a local path provisioner are all bundled into one ~70 MB binary. There is nothing to install separately, nothing to version-pin across multiple packages, nothing to break in a gap between component versions.</p>
<p><strong>2. Minimal memory footprint.</strong><br>
<!-- -->The k3s control plane starts at around 300 MB of RAM on a quiet cluster. The MacBook Pro's 8 GB is not a ceiling — it is headroom. Under load, the same node runs Longhorn replicas, system daemonsets, and a k3s agent without being starved.</p>
<p><strong>3. CNCF-certified Kubernetes.</strong><br>
<!-- -->This is the argument that matters for a portfolio. k3s exposes exactly the same APIs as GKE, EKS, and AKS. Every concept learned here — Deployments, Services, PersistentVolumes, RBAC, Admission Webhooks — transfers directly to managed cloud Kubernetes. This is not a homelab toy; it is Kubernetes, stripped of the components that are irrelevant at this scale.</p>
<p><strong>4. Rolling upgrades with zero manual steps.</strong><br>
<!-- -->Once <a href="https://github.com/rancher/system-upgrade-controller" target="_blank" rel="noopener noreferrer" class="">system-upgrade-controller</a> is deployed, upgrading the entire cluster is a single YAML change: update the <code>version:</code> field in the Plan manifest and commit. The controller handles cordon → drain → upgrade → uncordon on each node, server before agents. The cluster was upgraded from v1.33 to v1.36.2+k3s1 this way with zero downtime across five nodes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-installation">The Installation<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal#the-installation" class="hash-link" aria-label="Direct link to The Installation" title="Direct link to The Installation" translate="no">​</a></h2>
<p>The four ThinkPad cluster nodes (set-hog, fast-skunk, fast-heron, star-kitten) were already running Ubuntu 22.04 LTS, provisioned by MAAS. <code>swift-mac</code> had Ubuntu installed manually. From there, the install is three steps.</p>
<p><strong>Step 1 — Control plane on <code>set-hog</code> (10.0.0.2):</strong></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">ssh ubuntu@10.0.0.2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">curl -sfL https://get.k3s.io | sh -</span><br></div></code></pre></div></div>
<p>The script detects the architecture, downloads the single binary, registers a systemd unit, and starts the k3s server. The full API server, scheduler, controller-manager, and embedded etcd are up within 30 seconds.</p>
<p><strong>Step 2 — Retrieve the join token:</strong></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">sudo cat /var/lib/rancher/k3s/server/node-token</span><br></div></code></pre></div></div>
<p><strong>Step 3 — Join each worker (fast-skunk, fast-heron, star-kitten, swift-mac):</strong></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">curl -sfL https://get.k3s.io | \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  K3S_URL=https://10.0.0.2:6443 \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  K3S_TOKEN=&lt;token&gt; \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  sh -</span><br></div></code></pre></div></div>
<p>Four SSH sessions. Four curl commands. The cluster appears in <code>kubectl get nodes</code> within seconds of each join.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">NAME          STATUS   ROLES                  AGE   VERSION</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">set-hog       Ready    control-plane,master   5m    v1.36.2+k3s1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">fast-skunk    Ready    &lt;none&gt;                 4m    v1.36.2+k3s1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">fast-heron    Ready    &lt;none&gt;                 3m    v1.36.2+k3s1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">star-kitten   Ready    &lt;none&gt;                 2m    v1.36.2+k3s1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">swift-mac     Ready    &lt;none&gt;                 1m    v1.36.2+k3s1</span><br></div></code></pre></div></div>
<p>The <code>swift-mac</code> node joined over the Tailscale mesh (the MAAS controller acts as NAT). Apple's SMC does not support Wake-on-AC, so <code>swift-mac</code> does not auto-start after a power failure — every other node does. That is the only infrastructure asymmetry introduced by the hardware.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-analogy">The Analogy<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal#the-analogy" class="hash-link" aria-label="Direct link to The Analogy" title="Direct link to The Analogy" translate="no">​</a></h2>
<p>The relationship between kubeadm and k3s is the same as the relationship between manually installing a Linux server package by package versus using MAAS to provision it. The outcome is identical. The path is radically different. Kubeadm gives you maximum control over every component. k3s gives you a working cluster you can build on immediately.</p>
<p>For a project where the goal is to demonstrate platform engineering skills — not to demonstrate ability to configure etcd from scratch — k3s is the correct choice. The cluster is now in its 75th operational phase. The distribution has never been the bottleneck.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="automating-it-with-ansible">Automating It with Ansible<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal#automating-it-with-ansible" class="hash-link" aria-label="Direct link to Automating It with Ansible" title="Direct link to Automating It with Ansible" translate="no">​</a></h2>
<p>Four SSH sessions is fine for a one-time bootstrap. But after the cluster was up and <code>minicloud-ansible</code> was established as the automation layer, the natural next step was to write a playbook that can reproduce the full install from scratch — so the next cluster rebuild takes one command, not four.</p>
<p>The playbook lives at <code>playbooks/install-k3s.yml</code> in <a href="https://github.com/andrelair-platform/minicloud-ansible" target="_blank" rel="noopener noreferrer" class="">minicloud-ansible</a>. It has three plays:</p>
<p><strong>Play 1 — Control plane (<code>set-hog</code>):</strong><br>
<!-- -->Installs the k3s server, waits for port 6443 to be ready, waits for the node to reach <code>Ready</code> state, then reads the join token into an Ansible variable.</p>
<p><strong>Play 2 — Workers (<code>fast-skunk</code>, <code>fast-heron</code>, <code>star-kitten</code>, <code>swift-mac</code>):</strong><br>
<!-- -->Joins one worker at a time (<code>serial: 1</code>) using the token fetched from <code>hostvars</code>. Each worker must be <code>Ready</code> before the next one starts.</p>
<p><strong>Play 3 — Kubeconfig:</strong><br>
<!-- -->Fetches <code>/etc/rancher/k3s/k3s.yaml</code> from the control plane, patches the server address from <code>127.0.0.1</code> to the real IP, and renames the context to <code>minicloud</code>. Saves the result to <code>/tmp/minicloud.yaml</code> on the Ansible controller.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"># Dry run</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ansible-playbook playbooks/install-k3s.yml --check</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># Full install (pins to current cluster version)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ansible-playbook playbooks/install-k3s.yml</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># Pin a different version</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ansible-playbook playbooks/install-k3s.yml -e k3s_version=v1.37.0+k3s1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># Workers only (control plane already running)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ansible-playbook playbooks/install-k3s.yml --tags workers</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># After the playbook — copy kubeconfig to the controller</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">scp /tmp/minicloud.yaml controller:~/.kube/minicloud.yaml</span><br></div></code></pre></div></div>
<p>The playbook is <strong>idempotent</strong> — it checks for <code>/usr/local/bin/k3s</code> before installing. Re-running on an existing cluster skips the install tasks and only re-validates the <code>Ready</code> state.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-the-install-wasnt-automated-from-day-one">Why the install wasn't automated from day one<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal#why-the-install-wasnt-automated-from-day-one" class="hash-link" aria-label="Direct link to Why the install wasn't automated from day one" title="Direct link to Why the install wasn't automated from day one" translate="no">​</a></h3>
<p>Two reasons. First, the k3s install is a Day-0 operation — it runs once per cluster lifetime. The effort of writing and testing a playbook outweighed the cost of four SSH commands. Second, <code>swift-mac</code> cannot be PXE-booted, so Ubuntu was installed manually via USB regardless. As long as one step is manual, the whole bootstrap sequence is effectively manual for that node.</p>
<p>What was automated from day one was the part that actually repeats: <code>system-upgrade-controller</code> handles every future k3s version bump as a single YAML commit. The Ansible playbook covers the edge case — rebuild from scratch — which is worth automating even if it only happens once or twice in the cluster's lifetime.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-comes-next">What Comes Next<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/k3s-why-lightweight-kubernetes-bare-metal#what-comes-next" class="hash-link" aria-label="Direct link to What Comes Next" title="Direct link to What Comes Next" translate="no">​</a></h2>
<p>With five nodes running k3s, the next layer is networking and load balancing: MetalLB for bare-metal <code>LoadBalancer</code> services, a wildcard ingress controller, and a private CA so every internal service gets real TLS. That is covered in the next post.</p>
<hr>
<p><em>The full runbooks for every phase, including the exact cloud-init templates used for MAAS provisioning, live in the <a class="" href="https://andrelair-platform.github.io/minicloud-platform-docs/platform-roadmap/roadmap-overview">Documentation</a> section.</em></p>]]></content>
        <author>
            <name>Andre Kanmegne</name>
            <uri>https://www.devandre.sbs</uri>
        </author>
        <category label="kubernetes" term="kubernetes"/>
        <category label="k3s" term="k3s"/>
        <category label="bare-metal" term="bare-metal"/>
        <category label="platform-engineering" term="platform-engineering"/>
        <category label="devops" term="devops"/>
        <category label="homelab" term="homelab"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[LLMs on Bare Metal: Quantization, SLMs, and Replacing phi4-mini with Qwen 2.5 7B]]></title>
        <id>https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving</id>
        <link href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving"/>
        <updated>2026-07-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How we think about model size, quantization formats, and inference tuning for a 5-node CPU-only k3s cluster — and why we replaced phi4-mini with Qwen 2.5 7B Q4_K_M.]]></summary>
        <content type="html"><![CDATA[<p>Running LLMs on bare-metal CPU hardware forces you to understand the numbers behind model files. This post documents how we reason about model size, quantization, and inference serving on minicloud — and the concrete change we made: replacing phi4-mini with Qwen 2.5 7B across all three Ollama instances.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-7b-actually-means">What "7B" Actually Means<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#what-7b-actually-means" class="hash-link" aria-label="Direct link to What &quot;7B&quot; Actually Means" title="Direct link to What &quot;7B&quot; Actually Means" translate="no">​</a></h2>
<p>The "7B" in a model name is the number of parameters — floating-point weights stored in the neural network. Each parameter holds learned knowledge from training. More parameters generally means better reasoning, broader knowledge, and fewer hallucinations on complex tasks.</p>
<p>The catch: more parameters means more RAM.</p>
<table><thead><tr><th>Size</th><th>RAM at FP32 (raw)</th><th>Typical use</th></tr></thead><tbody><tr><td>1–3B</td><td>4–12 GB</td><td>Edge, mobile, very constrained hardware</td></tr><tr><td>7B</td><td>28 GB</td><td>The quality/resource sweet spot</td></tr><tr><td>13B</td><td>52 GB</td><td>Good reasoning, feasible on 48 GB GPU</td></tr><tr><td>30–34B</td><td>~120 GB</td><td>Multi-GPU or quantized on 24 GB</td></tr><tr><td>70B</td><td>280 GB</td><td>High-end multi-GPU or cloud</td></tr></tbody></table>
<p>Our cluster has no dedicated GPU — swift-mac has 8 GB RAM, ThinkPad workers have 16–32 GB. At FP32, even a 7B model is out of reach. This is where quantization becomes essential.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="quantization--why-it-changes-everything">Quantization — Why It Changes Everything<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#quantization--why-it-changes-everything" class="hash-link" aria-label="Direct link to Quantization — Why It Changes Everything" title="Direct link to Quantization — Why It Changes Everything" translate="no">​</a></h2>
<p>One FP32 parameter = 4 bytes. Quantization represents the same weight with fewer bits, accepting a small precision loss in exchange for dramatically lower memory usage.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">FP32   → 4 bytes/param  → 7B model = 28 GB   (baseline)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">FP16   → 2 bytes/param  → 7B model = 14 GB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Q8_0   → 1 byte/param   → 7B model =  7 GB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Q4_K_M → ~0.5 bytes/param → 7B model = 4.1 GB  ← most popular</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Q2_K   → ~0.25 bytes/param → 7B model = 2.7 GB  (notable quality loss)</span><br></div></code></pre></div></div>
<p>The quality loss from Q4_K_M vs FP16 is measurable on benchmarks but imperceptible in practical use for chat and RAG. Q2 and Q3 are a different story — avoid them for anything that requires accurate reasoning.</p>
<p><strong>Rule of thumb:</strong> step down one quantization level before stepping down to a smaller model. A 7B Q4 beats a 3B Q8 on almost every task.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-format-zoo">The Format Zoo<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#the-format-zoo" class="hash-link" aria-label="Direct link to The Format Zoo" title="Direct link to The Format Zoo" translate="no">​</a></h3>
<p>You'll encounter four main formats depending on your runtime:</p>
<ul>
<li class=""><strong>GGUF</strong> (llama.cpp / Ollama) — CPU-first format with optional GPU offloading. Suffixes like <code>Q4_K_M</code>, <code>Q5_K_M</code>, <code>Q8_0</code> tell you the quantization level. This is what Ollama uses.</li>
<li class=""><strong>GPTQ</strong> — GPU-oriented post-training quantization. Better than GGUF if you have sufficient VRAM.</li>
<li class=""><strong>AWQ</strong> — Activation-aware Weight Quantization. Better quality than GPTQ at the same size, increasingly standard.</li>
<li class=""><strong>EXL2</strong> — ExLlamaV2 format, very efficient for GPU batch inference.</li>
</ul>
<p>For a CPU-only or CPU-primary cluster: GGUF Q4_K_M is your format.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="slm-candidates--the-real-options-under-8b">SLM Candidates — The Real Options Under 8B<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#slm-candidates--the-real-options-under-8b" class="hash-link" aria-label="Direct link to SLM Candidates — The Real Options Under 8B" title="Direct link to SLM Candidates — The Real Options Under 8B" translate="no">​</a></h2>
<p>Small Language Models (≤7B) have caught up dramatically since 2024. These are the relevant ones for a constrained inference cluster:</p>
<table><thead><tr><th>Model</th><th>Size</th><th>Strengths</th><th>Best for</th></tr></thead><tbody><tr><td>phi4-mini (Microsoft)</td><td>3.8B</td><td>Strong reasoning for its size</td><td>RAG, factual Q&amp;A — we had it running</td></tr><tr><td>Llama 3.2 3B</td><td>3B</td><td>Good instruct, multilingual</td><td>Chat, summarization</td></tr><tr><td>Llama 3.1 8B</td><td>8B</td><td>Quality reference for 7-8B</td><td>General purpose</td></tr><tr><td>Gemma 3 4B</td><td>4B</td><td>Excellent code + instruction following</td><td>Code assistant</td></tr><tr><td>Mistral 7B v0.3</td><td>7B</td><td>Strong French natively, long context</td><td>French-speaking users</td></tr><tr><td>Qwen 2.5 7B</td><td>7B</td><td>Top-ranked 7B on recent benchmarks</td><td>General purpose — our choice</td></tr><tr><td>deepseek-r1 7B (distilled)</td><td>7B</td><td>Chain-of-thought reasoning</td><td>Analytical tasks</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-we-replaced-phi4-mini">Why We Replaced phi4-mini<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#why-we-replaced-phi4-mini" class="hash-link" aria-label="Direct link to Why We Replaced phi4-mini" title="Direct link to Why We Replaced phi4-mini" translate="no">​</a></h2>
<p>phi4-mini isn't a bad model — Microsoft did good work on reasoning at 3.8B. But it has two structural limits:</p>
<p><strong>3.8B is the quality ceiling.</strong> At 3.8B parameters, the model has less "memory" of training patterns. It hallucinates more on precise facts, multi-step reasoning degrades faster, and complex instruction following is less reliable than a 7B.</p>
<p><strong>The market moved.</strong> In 2024–2025, 7B models became what 13B models were in 2023. Qwen 2.5 7B in particular outperforms phi4-mini on nearly every benchmark while fitting in the same RAM constraint once quantized.</p>
<p>The comparison that made the decision clear:</p>
<table><thead><tr><th>Model</th><th>MMLU</th><th>HumanEval</th><th>French</th><th>RAM at Q4_K_M</th></tr></thead><tbody><tr><td>phi4-mini 3.8B</td><td>69%</td><td>62%</td><td>Passable</td><td>2.5 GB</td></tr><tr><td>Mistral 7B v0.3</td><td>64%</td><td>45%</td><td>Native</td><td>4.1 GB</td></tr><tr><td>Llama 3.1 8B</td><td>73%</td><td>72%</td><td>Good</td><td>4.7 GB</td></tr><tr><td><strong>Qwen 2.5 7B</strong></td><td><strong>75%</strong></td><td><strong>83%</strong></td><td><strong>Excellent</strong></td><td><strong>4.5 GB</strong></td></tr></tbody></table>
<p>Qwen 2.5 is natively trained on 29 languages including French. For French-speaking end users interacting with Open WebUI, that's a direct advantage over Llama.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-metrics-actually-matter-for-inference-serving">What Metrics Actually Matter for Inference Serving<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#what-metrics-actually-matter-for-inference-serving" class="hash-link" aria-label="Direct link to What Metrics Actually Matter for Inference Serving" title="Direct link to What Metrics Actually Matter for Inference Serving" translate="no">​</a></h2>
<p>Three metrics to optimize, depending on the use case:</p>
<p><strong>Time To First Token (TTFT)</strong> — latency before the user sees the first word. Critical for interactive chat. Smaller models win here. Groq's LPU hardware achieves TTFT &lt; 200ms even on 8B models.</p>
<p><strong>Tokens per second (throughput)</strong> — generation speed. GPU &gt;&gt; CPU. On CPU, phi4-mini Q4 generates ~15 tok/s on a ThinkPad, Llama 3.1 8B ~6 tok/s.</p>
<p><strong>Concurrency</strong> — how many simultaneous users. Ollama handles one request at a time per instance by default. For multi-user, you need multiple instances (we have three) or a batching server like vLLM.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="decision-matrix">Decision Matrix<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#decision-matrix" class="hash-link" aria-label="Direct link to Decision Matrix" title="Direct link to Decision Matrix" translate="no">​</a></h3>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">User expects response in &lt; 2s?</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Yes → Groq (cloud LPU) or phi4-mini local Q4</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└── No  → Llama 3.1 8B Q4 or Qwen 2.5 7B Q4</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Task requires reasoning (math, analysis)?</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Yes → deepseek-r1 distill or phi4-mini (strong on reasoning)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└── No  → Mistral 7B or Llama 3.2 3B (faster, cheaper)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">French-speaking users?</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Yes → Mistral 7B (native FR) or Llama 3.1 8B (solid multilingual)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└── No  → Qwen 2.5 7B or Gemma 3 4B</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tuning-ollama-for-cpu-inference">Tuning Ollama for CPU Inference<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#tuning-ollama-for-cpu-inference" class="hash-link" aria-label="Direct link to Tuning Ollama for CPU Inference" title="Direct link to Tuning Ollama for CPU Inference" translate="no">​</a></h2>
<p>Beyond the model choice, there are several environment variables that have immediate impact.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="keep-the-model-loaded">Keep the Model Loaded<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#keep-the-model-loaded" class="hash-link" aria-label="Direct link to Keep the Model Loaded" title="Direct link to Keep the Model Loaded" translate="no">​</a></h3>
<p>The most impactful single change: set <code>OLLAMA_KEEP_ALIVE=-1</code> so the model is never unloaded from RAM. Without this, Ollama evicts the model after 5 minutes of inactivity — the next request pays a 3–5 second cold start penalty while the model reloads.</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">env</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OLLAMA_KEEP_ALIVE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"-1"</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="flash-attention">Flash Attention<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#flash-attention" class="hash-link" aria-label="Direct link to Flash Attention" title="Direct link to Flash Attention" translate="no">​</a></h3>
<p>Reduces KV cache memory usage and speeds up the attention computation:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OLLAMA_FLASH_ATTENTION</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"1"</span><br></div></code></pre></div></div>
<p>Gain: ~15–20% less RAM, ~10% faster on longer contexts.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="context-window--the-most-underrated-lever">Context Window — the Most Underrated Lever<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#context-window--the-most-underrated-lever" class="hash-link" aria-label="Direct link to Context Window — the Most Underrated Lever" title="Direct link to Context Window — the Most Underrated Lever" translate="no">​</a></h3>
<p>Ollama defaults to loading the model's maximum context window (128k for Qwen 2.5). The KV cache grows linearly with context length. For standard chat, you don't need 128k tokens.</p>
<p>Forcing <code>num_ctx=4096</code> in the LiteLLM params:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">litellm_params</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">model</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ollama/qwen2.5</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">7b</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">instruct</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">q4_k_m</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">api_base</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> http</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">//ollama.ai.svc.cluster.local</span><span class="token punctuation" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">11434</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">num_ctx</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4096</span><br></div></code></pre></div></div>
<p>Impact: going from <code>num_ctx=32768</code> to <code>num_ctx=4096</code> can <strong>double throughput</strong> on CPU by reducing the amount of memory the model reads per token generation step.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="cpu-threads--align-with-physical-cores">CPU Threads — Align With Physical Cores<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#cpu-threads--align-with-physical-cores" class="hash-link" aria-label="Direct link to CPU Threads — Align With Physical Cores" title="Direct link to CPU Threads — Align With Physical Cores" translate="no">​</a></h3>
<p>Ollama uses all available cores by default, but hyperthreading can hurt inference (two logical threads on the same physical core compete for the same ALU). Set threads to physical core count:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OLLAMA_NUM_THREADS</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"6"</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># physical cores, not logical — avoids hyperthreading overhead</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="parallelism-and-loaded-models">Parallelism and Loaded Models<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#parallelism-and-loaded-models" class="hash-link" aria-label="Direct link to Parallelism and Loaded Models" title="Direct link to Parallelism and Loaded Models" translate="no">​</a></h3>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OLLAMA_NUM_PARALLEL</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"4"</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># max concurrent requests per instance</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OLLAMA_MAX_LOADED_MODELS</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"2"</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># keep at most 2 models in RAM simultaneously</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OLLAMA_KV_CACHE_TYPE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"q8_0"</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># quantize the KV cache itself — saves RAM during long conversations</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary-what-each-tuning-gains">Summary: What Each Tuning Gains<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#summary-what-each-tuning-gains" class="hash-link" aria-label="Direct link to Summary: What Each Tuning Gains" title="Direct link to Summary: What Each Tuning Gains" translate="no">​</a></h3>
<table><thead><tr><th>Optimization</th><th>Estimated gain on ThinkPad CPU</th></tr></thead><tbody><tr><td><code>OLLAMA_KEEP_ALIVE=-1</code></td><td>−3–5s cold start per request</td></tr><tr><td><code>num_ctx: 4096</code> instead of 32k</td><td>×1.8–2× tokens/s</td></tr><tr><td><code>OLLAMA_FLASH_ATTENTION=1</code></td><td>−15–20% RAM, +10% speed</td></tr><tr><td><code>OLLAMA_NUM_THREADS=6</code> (physical cores)</td><td>+5–10% stability under load</td></tr><tr><td>3 LiteLLM load-balanced instances</td><td>×3 simultaneous users</td></tr><tr><td>phi4-mini → Qwen 2.5 7B Q4</td><td>+15–20% quality on benchmarks</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-actually-did-on-minicloud">What We Actually Did on minicloud<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#what-we-actually-did-on-minicloud" class="hash-link" aria-label="Direct link to What We Actually Did on minicloud" title="Direct link to What We Actually Did on minicloud" translate="no">​</a></h2>
<p>All three Ollama instances (<code>ollama</code>, <code>ollama-secondary</code>, <code>ollama-tertiary</code> in the <code>ai</code> namespace) already had all the environment variables set from a previous tuning session. The only missing piece was the model itself.</p>
<p>The model pull from inside a pod was blocked: the default-deny-egress NetworkPolicy and a routing difference between pod-level and node-level IPv4 traffic to Cloudflare R2 caused <code>connection refused</code> on <code>172.64.66.x:443</code>. The node itself could reach the registry fine, but the CNI (flannel) traffic path from pod IPs didn't work the same way.</p>
<p><strong>Workaround used:</strong> temporarily patched <code>hostNetwork: true</code> on all three deployments. This makes the pod use the node's network namespace directly, bypassing CNI. The node can reach registry.ollama.ai — so the pull works.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"># Add hostNetwork temporarily</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl patch deployment/ollama deployment/ollama-secondary deployment/ollama-tertiary \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  -n ai --type=json \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  -p '[{"op":"add","path":"/spec/template/spec/hostNetwork","value":true}]'</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># Pull the model (4.7 GB per instance)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl exec -n ai deployment/ollama -- ollama pull qwen2.5:7b-instruct-q4_k_m</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl exec -n ai deployment/ollama-secondary -- ollama pull qwen2.5:7b-instruct-q4_k_m</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl exec -n ai deployment/ollama-tertiary -- ollama pull qwen2.5:7b-instruct-q4_k_m</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># Remove hostNetwork — pods restart with normal CNI networking</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl patch deployment/ollama deployment/ollama-secondary deployment/ollama-tertiary \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  -n ai --type=json \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  -p '[{"op":"remove","path":"/spec/template/spec/hostNetwork"}]'</span><br></div></code></pre></div></div>
<p>The model files are stored on a PVC, so they persist across pod restarts. After removing <code>hostNetwork</code>, all three instances came back up with <code>qwen2.5:7b-instruct-q4_k_m</code> cached locally — and LiteLLM started routing to it immediately.</p>
<p>The underlying networking issue (pod-to-Cloudflare IPv4 path via flannel) is a known limitation. <strong>Phase 76 will replace flannel with Cilium</strong>, which adds FQDN-based egress policies and Hubble observability — making this kind of debugging trivial next time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next">What's Next<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/llm-slm-quantization-inference-serving#whats-next" class="hash-link" aria-label="Direct link to What's Next" title="Direct link to What's Next" translate="no">​</a></h2>
<p>With Qwen 2.5 7B running on all three instances and LiteLLM routing traffic via least-busy, the AI stack is:</p>
<ul>
<li class=""><strong>phi3-financial pipeline</strong>: Groq <code>llama-3.1-8b-instant</code> as primary (LPU-fast), Qwen 2.5 7B ×3 as local fallback</li>
<li class=""><strong>Open WebUI chat</strong>: Qwen 2.5 7B for general use, deepseek-r1 7B for reasoning tasks</li>
<li class=""><strong>RAG ingest</strong>: <code>nomic-embed-text</code> for embeddings (unchanged)</li>
</ul>
<p>The speculative decoding optimization (a small draft model pre-generates tokens that the main model verifies) is available in Ollama v0.5+ and could push throughput 2–3× further on predictable outputs. That's the next tuning frontier once this setup is stable.</p>]]></content>
        <author>
            <name>Andre Kanmegne</name>
            <uri>https://www.devandre.sbs</uri>
        </author>
        <category label="ai" term="ai"/>
        <category label="llm" term="llm"/>
        <category label="ollama" term="ollama"/>
        <category label="litellm" term="litellm"/>
        <category label="kubernetes" term="kubernetes"/>
        <category label="inference" term="inference"/>
        <category label="quantization" term="quantization"/>
        <category label="mlops" term="mlops"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Automating ConfigMap Reloads: Why We Added Stakater Reloader]]></title>
        <id>https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader</id>
        <link href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader"/>
        <updated>2026-07-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Kubernetes does not automatically restart pods when a mounted ConfigMap changes — especially when using subPath mounts. We added Stakater Reloader to eliminate manual kubectl rollout restart calls across Homer, LiteLLM, SearXNG, and Backstage. Here's the problem, the fix, and a bonus ArgoCD OOMKill investigation we uncovered along the way.
]]></summary>
        <content type="html"><![CDATA[<p>Every time I updated the Homer dashboard config, I had to run <code>kubectl rollout restart deployment/homer -n homer</code> after ArgoCD finished syncing. Same for LiteLLM when routing changed. Same for Backstage after any catalog or proxy update. The pattern was identical every time: push to git, wait for ArgoCD sync, then manually trigger a pod restart.</p>
<p>That is an operational smell. If git is the only write path, the restart should be automatic too.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-kubernetes-doesnt-auto-reload-configmaps">Why Kubernetes Doesn't Auto-Reload ConfigMaps<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader#why-kubernetes-doesnt-auto-reload-configmaps" class="hash-link" aria-label="Direct link to Why Kubernetes Doesn't Auto-Reload ConfigMaps" title="Direct link to Why Kubernetes Doesn't Auto-Reload ConfigMaps" translate="no">​</a></h2>
<p>Kubernetes <em>does</em> update ConfigMap data in-place — but only for volume mounts that use the full directory. When you mount a ConfigMap with <code>subPath</code> (mounting a single file rather than the whole directory), Kubernetes <a href="https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically" target="_blank" rel="noopener noreferrer" class="">intentionally skips the automatic update</a>.</p>
<p>Homer mounts its config with <code>subPath</code>:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">volumeMounts</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">mountPath</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> /www/assets/config.yml</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> config</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">subPath</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> config.yml   </span><span class="token comment" style="color:#999988;font-style:italic"># ← this disables automatic propagation</span><br></div></code></pre></div></div>
<p>This is by design. <code>subPath</code> gives you fine-grained control over where a file lands inside a container, but it breaks the inotify watch that Kubernetes would otherwise use to propagate ConfigMap updates. The file the pod sees is frozen at mount time.</p>
<p>The common workaround — which I had in place — is a manual annotation bump on the pod template:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">template</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">metadata</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">annotations</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">config-checksum</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"v17-star-kitten-polaris"</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># bump this manually after every config change</span><br></div></code></pre></div></div>
<p>Changing the annotation forces a new pod-template-hash, which triggers a rolling restart. But this is noise: it clutters the git history, it's easy to forget, and it has nothing to do with the actual change being made.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-broader-pattern">The Broader Pattern<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader#the-broader-pattern" class="hash-link" aria-label="Direct link to The Broader Pattern" title="Direct link to The Broader Pattern" translate="no">​</a></h2>
<p>It wasn't just Homer. Every ConfigMap-backed deployment in the cluster had the same problem:</p>
<table><thead><tr><th>Deployment</th><th>ConfigMaps</th><th>Required action after change</th></tr></thead><tbody><tr><td><code>homer</code></td><td><code>homer-config</code></td><td>Manual <code>kubectl rollout restart</code></td></tr><tr><td><code>litellm</code></td><td><code>litellm-config</code>, <code>langfuse-prompt-handler</code>, <code>phi3-financial-prompt</code></td><td>Manual <code>kubectl rollout restart</code></td></tr><tr><td><code>searxng</code></td><td><code>searxng-config</code></td><td>Manual <code>kubectl rollout restart</code></td></tr><tr><td><code>backstage</code></td><td><code>backstage-app-config</code>, <code>backstage-session-config</code></td><td>Manual <code>kubectl rollout restart</code></td></tr></tbody></table>
<p>The CLAUDE.md even had a note reminding me to run the restart after every Backstage config push. That note existing is the problem — if a human has to remember to do something after a git push, it will eventually be forgotten.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-fix-stakater-reloader">The Fix: Stakater Reloader<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader#the-fix-stakater-reloader" class="hash-link" aria-label="Direct link to The Fix: Stakater Reloader" title="Direct link to The Fix: Stakater Reloader" translate="no">​</a></h2>
<p><a href="https://github.com/stakater/reloader" target="_blank" rel="noopener noreferrer" class="">Stakater Reloader</a> is a Kubernetes controller that watches ConfigMaps and Secrets, and triggers rolling restarts on any Deployment (or StatefulSet, DaemonSet) that opts in via a single annotation.</p>
<p>The deployment is a single Helm chart in its own namespace:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># apps/reloader.yaml</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">sources</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">repoURL</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> https</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">//stakater.github.io/stakater</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">charts</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">chart</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> reloader</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">targetRevision</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"2.2.14"</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># app v1.4.19</span><br></div></code></pre></div></div>
<p>And opting in is one line on any Deployment metadata:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">metadata</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">annotations</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">reloader.stakater.com/auto</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"true"</span><br></div></code></pre></div></div>
<p>Reloader detects all ConfigMaps and Secrets referenced by the Deployment (via <code>volumes</code>, <code>envFrom</code>, and <code>env.valueFrom</code>) and watches them. When ArgoCD syncs a ConfigMap change, Reloader sees the update within seconds and triggers a rolling restart automatically.</p>
<p>The flow is now fully automated:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">git push → ArgoCD syncs ConfigMap → Reloader detects change → pod rolling restart</span><br></div></code></pre></div></div>
<p>No human step. No annotation bump. No runbook entry reminding you to remember.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="removing-the-manual-workaround">Removing the Manual Workaround<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader#removing-the-manual-workaround" class="hash-link" aria-label="Direct link to Removing the Manual Workaround" title="Direct link to Removing the Manual Workaround" translate="no">​</a></h2>
<p>With Reloader installed, the <code>config-checksum</code> annotation in Homer's deployment was noise:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># before — manual workaround</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">template</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">metadata</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">annotations</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">config-checksum</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"v17-star-kitten-polaris"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># after — clean</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">template</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">metadata</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">labels</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">app</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> homer</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">backstage.io/kubernetes-id</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> homer</span><br></div></code></pre></div></div>
<p>The deployment metadata gets the Reloader annotation instead:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">metadata</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> homer</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">namespace</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> homer</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">annotations</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">reloader.stakater.com/auto</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"true"</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-bonus-investigation-argocd-oomkills">A Bonus Investigation: ArgoCD OOMKills<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader#a-bonus-investigation-argocd-oomkills" class="hash-link" aria-label="Direct link to A Bonus Investigation: ArgoCD OOMKills" title="Direct link to A Bonus Investigation: ArgoCD OOMKills" translate="no">​</a></h2>
<p>While implementing Reloader, I noticed the ArgoCD application controller was in CrashLoopBackOff — 183 restarts over 44 hours:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">argo-cd-argocd-application-controller-0   0/1   CrashLoopBackOff   183 (4m ago)   44h</span><br></div></code></pre></div></div>
<p>This explained why syncs were taking forever and why Reloader itself couldn't complete its initial sync. The exit code was 137 — <code>SIGKILL</code> from the OOM killer:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Last State: Terminated</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Reason:    OOMKilled</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Exit Code: 137</span><br></div></code></pre></div></div>
<p>The controller had a 1Gi memory limit, set when the cluster managed about 10 applications. It now manages ~40, including multi-source Helm applications with large manifests (Harbor, Authentik, kube-prometheus-stack). The reconciliation loop for 40 apps simply exceeded 1Gi.</p>
<p>The fix is in <code>helm-values/argocd-values.yaml</code>:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">controller</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">resources</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">requests</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">cpu</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 100m</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">memory</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 512Mi </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">limits</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">   </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">cpu</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 1000m</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">memory</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 2Gi </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># was 1Gi</span><br></div></code></pre></div></div>
<p>Applied immediately via <code>kubectl patch statefulset</code> without waiting for ArgoCD to self-sync (it couldn't, since the controller was the one crashing). After the patch, the controller came up clean: <code>1/1 Running 0</code>.</p>
<p>The lesson: ArgoCD controller memory scales with the number of applications and the complexity of their manifests. If you're adding many Helm apps, keep an eye on it. A <code>CrashLoopBackOff</code> on the application controller means nothing syncs — it's the most critical pod in the cluster after the API server.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-deployed">What's Deployed<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader#whats-deployed" class="hash-link" aria-label="Direct link to What's Deployed" title="Direct link to What's Deployed" translate="no">​</a></h2>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Namespace:  reloader</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">App:        reloader (ArgoCD)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Chart:      stakater/reloader 2.2.14 (app v1.4.19)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Security:   runAsNonRoot, runAsUser 65534, seccompProfile RuntimeDefault,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            capabilities.drop ALL, no privilege escalation</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Resources:  10m/32Mi request — 100m/128Mi limit</span><br></div></code></pre></div></div>
<p>Deployments that now auto-reload on ConfigMap change:</p>
<ul>
<li class=""><code>homer</code> — dashboard layout</li>
<li class=""><code>litellm</code> — AI Gateway model routing, prompt handler, financial guardrails</li>
<li class=""><code>searxng</code> — search engine settings</li>
<li class=""><code>backstage</code> — catalog locations, proxy endpoints, auth config</li>
</ul>
<p>Any future deployment that mounts a ConfigMap gets the same behaviour with one annotation line. The pattern scales for free.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-gotcha-chart-versioning">One Gotcha: Chart Versioning<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/kubernetes-configmap-auto-reload-stakater-reloader#one-gotcha-chart-versioning" class="hash-link" aria-label="Direct link to One Gotcha: Chart Versioning" title="Direct link to One Gotcha: Chart Versioning" translate="no">​</a></h2>
<p>The Stakater Reloader chart version does <strong>not</strong> match the application version. When I first pinned <code>targetRevision: "1.4.3"</code> (matching the app version format), ArgoCD failed:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">error fetching chart: failed to fetch chart: helm pull --version 1.4.3 --repo https://stakater.github.io/stakater-charts</span><br></div></code></pre></div></div>
<p>The correct version to use is the chart version, found via:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">helm repo add stakater https://stakater.github.io/stakater-charts</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">helm search repo stakater/reloader</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># NAME                  CHART VERSION   APP VERSION</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># stakater/reloader     2.2.14          v1.4.19</span><br></div></code></pre></div></div>
<p>Chart <code>2.2.14</code> = app <code>v1.4.19</code>. Always search before pinning.</p>]]></content>
        <author>
            <name>Andre Kanmegne</name>
            <uri>https://www.devandre.sbs</uri>
        </author>
        <category label="kubernetes" term="kubernetes"/>
        <category label="gitops" term="gitops"/>
        <category label="argocd" term="argocd"/>
        <category label="platform-engineering" term="platform-engineering"/>
        <category label="reloader" term="reloader"/>
        <category label="configmap" term="configmap"/>
        <category label="devops" term="devops"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Platform Engineering on a Budget: Running Production Kubernetes on 5 ThinkPads]]></title>
        <id>https://andrelair-platform.github.io/minicloud-platform-docs/blog/platform-engineering-bare-metal-kubernetes</id>
        <link href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/platform-engineering-bare-metal-kubernetes"/>
        <updated>2026-07-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How I built a production-grade Kubernetes platform on five bare-metal ThinkPad laptops — with MAAS provisioning, GitOps, SSO, observability, and AI services — and what I learned about real platform engineering along the way.
]]></summary>
        <content type="html"><![CDATA[<p>Most cloud platforms hide the infrastructure from you. MAAS provisioning, PXE boot sequences, NIC bonding, storage backends, certificate chains — all of it abstracted behind a few CLI flags or a dashboard. That abstraction is valuable in production, but it can also keep engineers at arm's length from the system they're supposed to understand deeply.</p>
<p>This project started from a simple question: <strong>what does it actually take to build a production-grade Kubernetes platform from scratch?</strong></p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-setup">The Setup<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/platform-engineering-bare-metal-kubernetes#the-setup" class="hash-link" aria-label="Direct link to The Setup" title="Direct link to The Setup" translate="no">​</a></h2>
<p>The hardware is five laptops: four Lenovo ThinkPad X390s acting as cluster nodes and a MacBook Pro 2012 running as a storage worker. A fifth ThinkPad X390 runs as the MAAS controller — handling PXE boot, DHCP, DNS, and Tailscale NAT for the entire rack. Total cost: well under a cloud provider's monthly invoice for equivalent compute.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Controller (MAAS):    ktayl-ThinkPad-X390  — Tailscale endpoint, NAT, kubectl</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Control plane:        set-hog              — k3s master, 10.0.0.2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Workers:              fast-skunk           — 10.0.0.4</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                      fast-heron           — 10.0.0.7</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                      star-kitten          — 10.0.0.8</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                      swift-mac            — 10.0.0.10 (MacBook Pro 2012, Longhorn storage)</span><br></div></code></pre></div></div>
<p>Every node is provisioned by MAAS via PXE: the controller broadcasts a boot offer, nodes pick it up, and Ubuntu 22.04 is deployed with a pre-seeded cloud-init. That means any node can be wiped and re-provisioned in under 10 minutes — the same discipline you'd apply in a real cloud provider's bare-metal service.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-production-grade-actually-means-here">What "Production-Grade" Actually Means Here<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/platform-engineering-bare-metal-kubernetes#what-production-grade-actually-means-here" class="hash-link" aria-label="Direct link to What &quot;Production-Grade&quot; Actually Means Here" title="Direct link to What &quot;Production-Grade&quot; Actually Means Here" translate="no">​</a></h2>
<p>Running a home lab is easy. Running it like a platform team would run production is a different discipline entirely. The constraints I imposed on myself:</p>
<ul>
<li class=""><strong>No manual <code>kubectl apply</code></strong> — all workloads live in Git and sync via ArgoCD (app-of-apps pattern, Kustomize base+overlays, 3-branch CI promotion flow)</li>
<li class=""><strong>Zero-trust networking</strong> — OPA Gatekeeper admission policies in deny mode across all 23 namespaces, NetworkPolicy enforced everywhere, Vault auto-unseal via AWS KMS</li>
<li class=""><strong>Observability from day one</strong> — Prometheus + Grafana + Loki + Tempo deployed before any application workload; node_exporter on the controller itself so disk alerts fire before MinIO crashes</li>
<li class=""><strong>OIDC SSO on every service</strong> — Authentik as the identity provider, OIDC/PKCE protecting ArgoCD, Grafana, Harbor, Backstage, Open WebUI, and Vaultwarden</li>
<li class=""><strong>Automated regression testing</strong> — 62 checks across infra, GitOps, security, storage, AI, and observability that I run after every significant change</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-things-that-surprised-me">The Things That Surprised Me<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/platform-engineering-bare-metal-kubernetes#the-things-that-surprised-me" class="hash-link" aria-label="Direct link to The Things That Surprised Me" title="Direct link to The Things That Surprised Me" translate="no">​</a></h2>
<p><strong>Storage is the hardest part of bare metal.</strong> Longhorn on the MacBook Pro 2012 (swift-mac) required hand-tuning mount propagation, replica counts, and scheduling tolerations. When the node goes down unexpectedly — and it does, because Apple's SMC doesn't support Wake-on-AC — Longhorn's replica recovery needs headroom that you only understand by watching it fail twice.</p>
<p><strong>Certificate chains break in non-obvious ways.</strong> I run a private CA (<code>minicloud-ca.crt</code>) for all internal HTTPS endpoints. The macOS System Keychain trusts it, but <code>/opt/anaconda3/bin/curl</code> uses OpenSSL and ignores it silently. Two hours of debugging a Harbor push failure taught me to always check which <code>curl</code> binary is in <code>$PATH</code>.</p>
<p><strong>GitOps and ArgoCD StatefulSet sync have edge cases.</strong> When a StatefulSet's <code>VolumeClaimTemplate</code> is defined without explicit <code>apiVersion</code>, <code>kind</code>, and <code>volumeMode</code>, ArgoCD v3.4.1 perpetually shows a diff — even after sync. The fix is to write the full spec in the manifest; <code>ignoreDifferences</code> doesn't work for these fields.</p>
<p><strong>Velero backup timeouts need tuning per workload.</strong> The default 4-hour <code>itemOperationTimeout</code> on Velero's kopia plugin wasn't enough for a 4.5 GB ClickHouse PV (Langfuse LLM trace data). Kopia transferred ~2.5 GB and then the backup was cancelled. Bumping to 8 hours fixed it — but the lesson is that backup SLOs depend on data size, not just infrastructure configuration.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-running-today">What's Running Today<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/platform-engineering-bare-metal-kubernetes#whats-running-today" class="hash-link" aria-label="Direct link to What's Running Today" title="Direct link to What's Running Today" translate="no">​</a></h2>
<p>After 35+ phases of incremental build-out, the current platform runs:</p>
<table><thead><tr><th>Service</th><th>Purpose</th></tr></thead><tbody><tr><td>ArgoCD</td><td>GitOps delivery — 8 custom app repos, 23 namespaces</td></tr><tr><td>Harbor</td><td>Private container registry with cosign image signing</td></tr><tr><td>Backstage</td><td>Internal developer portal with catalog, TechDocs, scaffolder</td></tr><tr><td>Authentik</td><td>OIDC/SSO identity provider</td></tr><tr><td>Grafana + Prometheus</td><td>Metrics, dashboards, alerting</td></tr><tr><td>Loki + Tempo</td><td>Log aggregation and distributed tracing</td></tr><tr><td>Vault</td><td>Secrets management with Kubernetes auth</td></tr><tr><td>Longhorn</td><td>Distributed block storage across the cluster</td></tr><tr><td>Open WebUI + Ollama</td><td>Self-hosted LLM chat with local models</td></tr><tr><td>RAG pipeline</td><td>Document ingestion → chunking → embeddings → pgvector</td></tr><tr><td>Velero</td><td>Scheduled backups to MinIO (daily, 7-day TTL)</td></tr><tr><td>Vaultwarden</td><td>Self-hosted password manager with SSO</td></tr><tr><td>Cloudflare Tunnel</td><td>Public HTTPS access without port-forwarding</td></tr></tbody></table>
<p>All publicly reachable at <code>*.devandre.sbs</code> via Cloudflare Tunnel, protected by Authentik SSO.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-for-engineering">Why This Matters for Engineering<a href="https://andrelair-platform.github.io/minicloud-platform-docs/blog/platform-engineering-bare-metal-kubernetes#why-this-matters-for-engineering" class="hash-link" aria-label="Direct link to Why This Matters for Engineering" title="Direct link to Why This Matters for Engineering" translate="no">​</a></h2>
<p>The goal was never to run Kubernetes at home for its own sake. The goal was to develop the kind of platform instinct that only comes from operating infrastructure through failure: disk fills up and MinIO hangs in memory with no alert; a node's SMC doesn't respond to WoL and a cron job has to compensate; a Velero CRD upgrade job never sets <code>status.succeeded</code> because of a k3s Job controller bug.</p>
<p>Those incidents — and the fixes — are what platform engineering actually looks like beneath the abstractions. The next posts in this series will go deeper into specific components: the RAG pipeline architecture, the GitOps promotion workflow, and the OPA Gatekeeper policy set.</p>
<hr>
<p><em>The full runbooks for every phase live in the <a class="" href="https://andrelair-platform.github.io/minicloud-platform-docs/platform-roadmap/roadmap-overview">Documentation</a> section of this site.</em></p>]]></content>
        <author>
            <name>Andre Kanmegne</name>
            <uri>https://www.devandre.sbs</uri>
        </author>
        <category label="kubernetes" term="kubernetes"/>
        <category label="platform-engineering" term="platform-engineering"/>
        <category label="devops" term="devops"/>
        <category label="k3s" term="k3s"/>
        <category label="gitops" term="gitops"/>
        <category label="bare-metal" term="bare-metal"/>
    </entry>
</feed>