<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vishnu Hari Dadhich</title>
    <description>The latest articles on DEV Community by Vishnu Hari Dadhich (@vishnuhd).</description>
    <link>https://dev.to/vishnuhd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1462935%2Fe3c25478-2553-4cbb-92cc-adb4778764e9.jpg</url>
      <title>DEV Community: Vishnu Hari Dadhich</title>
      <link>https://dev.to/vishnuhd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vishnuhd"/>
    <language>en</language>
    <item>
      <title>What the heck is an AI Agent?</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Sat, 12 Sep 2026 18:45:00 +0000</pubDate>
      <link>https://dev.to/vishnuhd/what-the-heck-is-an-ai-agent-lfc</link>
      <guid>https://dev.to/vishnuhd/what-the-heck-is-an-ai-agent-lfc</guid>
      <description>&lt;h2&gt;
  
  
  Not a chatbot with more steps
&lt;/h2&gt;

&lt;p&gt;“Agentic AI” gets used for basically anything with a system prompt now. It isn’t a chatbot that’s more polite about calling tools. The actual difference is structural: an agent runs a loop, a chatbot answers a turn.&lt;/p&gt;

&lt;p&gt;A chatbot takes input, produces output, done. State lives in the conversation history you paste back in. An agent runs its own loop: observe, decide, act, observe again, using its own outputs to decide what to do next, without a human in the middle of every step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example.&lt;/strong&gt; Ask a chatbot “is my API returning 500s right now” and it tells you it can’t check. Ask an agent the same thing and it calls a monitoring tool, reads the response, decides the error rate looks elevated, calls a log-search tool to pull recent 500s, reads those, and comes back with “yes, 12 in the last 10 minutes, all from the /checkout endpoint, here’s the stack trace.” Same model, same question. The difference is the loop in between.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fai-agent-basics%2F01-chatbot-vs-agent.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fai-agent-basics%2F01-chatbot-vs-agent.svg" alt="A chatbot runs once from input through model to output, while an agent loops through observe, decide, act, and observe again." width="1200" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The model doesn’t loop, the harness does
&lt;/h2&gt;

&lt;p&gt;This is the part most explanations skip. The LLM itself is stateless and single-shot, feed it tokens, get tokens back. It has no concept of “keep going until the task is done.” The loop, the tool execution, the deciding-when-to-stop, none of that lives in the model weights.&lt;/p&gt;

&lt;p&gt;All of that lives in the &lt;strong&gt;agent harness&lt;/strong&gt; , the code wrapped around the model that turns single-shot completions into a running agent. The harness is what:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sends the prompt to the model and gets a response back&lt;/li&gt;
&lt;li&gt;Parses that response for a tool call&lt;/li&gt;
&lt;li&gt;Actually executes the tool (runs the shell command, hits the API, reads the file)&lt;/li&gt;
&lt;li&gt;Feeds the result back into context&lt;/li&gt;
&lt;li&gt;Decides whether to call the model again or stop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Swap the model and keep the harness, you get the same agent with different reasoning quality. Swap the harness and keep the model, you get a completely different agent, different tools available, different context management, different stopping conditions. The harness is doing more of the actual engineering work than people give it credit for, the model is just the decision-maker it calls into on each turn.&lt;/p&gt;

&lt;p&gt;Strip the harness out entirely and there’s no agent left, just a model that can produce a tool-call-shaped output with nothing to execute it, feed a result back, or call the model again. In RL terms, the model is the policy, the harness is the environment loop that runs the policy against the world and feeds observations back. A policy with no environment isn’t an agent, it’s a function you can call once.&lt;/p&gt;

&lt;p&gt;This is also why “build your own agent” and “build your own harness” are the same task described two ways. There’s no shortcut where you write an agent and skip the harness, hand-rolling one just means you’re the one writing the loop: send the prompt, parse the response for a tool call, execute it, append the result, decide whether to call the model again. Do that in 40 lines of Python or reach for pi.dev or Claude Code’s, it’s the same role either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example.&lt;/strong&gt; Two different harnesses wrapping the same underlying model can behave nothing alike. One harness might cap the loop at 5 iterations and summarize aggressively to save context. Another might allow 50 iterations, keep full history, and let the model spawn sub-agents. Same model, same weights, same API calls to it, wildly different agent because the harness around it is different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fai-agent-basics%2F02-agent-harness.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fai-agent-basics%2F02-agent-harness.svg" alt="The agent harness cycles from the LLM to tool execution, through context management, and back through a combined loop control and stop decision." width="1200" height="760"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things a harness provides
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Calling the LLM as decision-maker.&lt;/strong&gt; The harness sends the current state to the model and treats its output as a decision about what to do next, not just a completion to display.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A tool/function-calling interface.&lt;/strong&gt; The harness defines what tools exist, executes them when the model calls one, and returns results in a format the model can read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory/state across steps.&lt;/strong&gt; The harness owns the context window, what goes in, what gets summarized or dropped, and any external memory store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loop control.&lt;/strong&gt; The harness decides when to call the model again and when to stop, whether that’s a fixed iteration cap, a “the model said it’s done” signal, or something more custom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example, walking through one iteration.&lt;/strong&gt; User asks the agent to find the total size of log files older than 30 days. Step 1, harness sends the request to the model, model decides it needs to list files, returns a tool call for &lt;code&gt;find /var/log -mtime +30&lt;/code&gt;. Step 2, harness executes that command, gets a list of paths, appends the result to context. Step 3, harness calls the model again, model decides it needs sizes, returns a tool call for &lt;code&gt;du -ch&lt;/code&gt; on those paths. Step 4, harness executes it, appends the total to context. Step 5, harness calls the model once more, model has enough info, returns a final answer with no further tool call, harness detects that and stops the loop. Five round trips through the harness, one user question, no human in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the loop gets expensive
&lt;/h2&gt;

&lt;p&gt;Every loop iteration appends to context: the tool call, the tool result, the model’s next reasoning step. None of that gets discarded between iterations, unless the harness explicitly manages it.&lt;/p&gt;

&lt;p&gt;Tie this back to KV cache: cache grows with every token in context, and context in an agent loop grows every single step, not just per conversation turn. A single-shot inference request builds a cache once and discards it after. An agent loop keeps extending the same cache, iteration after iteration, and the cost compounds because every added token gets re-attended-to on every subsequent forward pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worked example.&lt;/strong&gt; Say a tool call and its result add roughly 300 tokens to context per iteration. A 10-iteration loop adds 3,000 tokens on top of the original prompt, all of it sitting in KV cache. Run the log-file-size agent above against a directory tree with a lot of nested paths and that 300-token estimate climbs fast, since raw &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;du&lt;/code&gt; output isn’t exactly compact.&lt;/p&gt;

&lt;p&gt;Practical implication: a long-running agent has to actively manage context, not just let it grow unbounded. This is a harness responsibility, not a model one, good harnesses summarize old steps, drop stale tool outputs, and truncate before the sequence gets unwieldy. That’s not an optimization, it’s what keeps a loop viable past a handful of iterations regardless of what it’s running on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fai-agent-basics%2F03-context-growth.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fai-agent-basics%2F03-context-growth.svg" alt="Context tokens in the KV cache rise from roughly 500 to 3,500 over ten agent loop iterations." width="1200" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Single-shot vs loop, at the token level
&lt;/h2&gt;

&lt;p&gt;Single-shot: prompt in, tokens out, KV cache built once, discarded after.&lt;/p&gt;

&lt;p&gt;Agent loop: prompt in, tokens out (including a tool call), harness executes the tool outside the model, tool result gets tokenized and appended by the harness, full sequence goes back through the model, cache either recomputed from scratch or extended depending on the serving setup. Repeat.&lt;/p&gt;

&lt;p&gt;The cost isn’t “the model thinks harder.” It’s “the model re-processes a longer sequence every iteration.” That’s the whole reason agent loops cost more to serve than a single chatbot request, and why serving techniques like continuous batching and prefix caching (vLLM does both) matter more for agents than for one-off completions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fai-agent-basics%2F04-agent-token-loop.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fai-agent-basics%2F04-agent-token-loop.svg" alt="At the token level, the prompt enters the model, produces a tool call, the harness executes it, and the tool result loops back to the model." width="1200" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Some harnesses and agents you’ve probably heard of
&lt;/h2&gt;

&lt;p&gt;Frameworks like &lt;a href="https://www.crewai.com/" rel="noopener noreferrer"&gt;CrewAI&lt;/a&gt; and &lt;a href="https://www.langchain.com/langgraph" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; are toolkits for building your own harness, not already-built ones. Since the interesting question is which finished harnesses and agents are worth knowing, here’s a rough split, open source and paid, as of when this was written:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source / self-hostable harnesses&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pi.dev/" rel="noopener noreferrer"&gt;Pi (pi.dev)&lt;/a&gt;&lt;/strong&gt;, a minimal, aggressively extensible terminal coding-agent harness. Deliberately skips features like sub-agents, plan mode, and MCP support out of the box, the pitch is you build those in yourself with extensions rather than accept whatever the harness maker decided. It also makes the model-vs-harness split from earlier concrete: it supports 15+ model providers and lets you switch mid-session, the harness stays constant, the model underneath it doesn’t have to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://hermes-agent.nousresearch.com/" rel="noopener noreferrer"&gt;Hermes Agent&lt;/a&gt;&lt;/strong&gt;, an open-source agent from Nous Research that can run locally or on a server and build reusable skills from previous work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.openhands.dev/" rel="noopener noreferrer"&gt;OpenHands&lt;/a&gt;&lt;/strong&gt; (formerly OpenDevin), an open-source autonomous software engineer harness, the open equivalent of Devin below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://openclaw.ai/" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;&lt;/strong&gt;, a fast-growing self-hosted personal agent harness, notable for running locally with your own model of choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.agpt.co/" rel="noopener noreferrer"&gt;AutoGPT&lt;/a&gt;&lt;/strong&gt;, the original viral agent demo, now a maturer platform with a visual builder and self-hosting support. Still the reference point most people mean when they say “autonomous agent.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Paid / proprietary harnesses and agents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/getting-started" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;&lt;/strong&gt;, Anthropic’s coding agent harness, runs in a terminal, reads and edits across a whole codebase, runs tests, commits changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://cognition.com/blog/introducing-devin" rel="noopener noreferrer"&gt;Devin&lt;/a&gt;&lt;/strong&gt;, a fully autonomous coding agent from Cognition, runs in its own sandboxed cloud environment rather than your terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://openai.com/codex/" rel="noopener noreferrer"&gt;OpenAI Codex&lt;/a&gt; / &lt;a href="https://help.openai.com/en/articles/11752874-chatgpt-agent" rel="noopener noreferrer"&gt;ChatGPT Agent&lt;/a&gt;&lt;/strong&gt;, OpenAI’s equivalents, spanning terminal, cloud, and chat surfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.perplexity.ai/comet" rel="noopener noreferrer"&gt;Perplexity Comet&lt;/a&gt;&lt;/strong&gt;, a browsing agent that navigates and completes tasks inside the browser rather than a terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coding is where agents and harnesses are most mature right now, since code execution gives the loop a fast, checkable signal of whether the last action actually worked. That’s not a coincidence, it’s the same reason the log-file-size example earlier in this post works cleanly: shell commands succeed or fail in an unambiguous way, which is exactly the kind of feedback a harness’s loop needs to decide what to do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Up next
&lt;/h2&gt;

&lt;p&gt;Next post in the local tools series is the practical version of this: standing up a Hermes-style agent, harness, tool calling, loop, the works, on the local setup already covered in this series. This post is the vocabulary you need before that one makes sense.&lt;/p&gt;

</description>
      <category>agenticai</category>
      <category>llm</category>
      <category>agentharness</category>
      <category>infra</category>
    </item>
    <item>
      <title>What Happens When You Ask an LLM a Question</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Fri, 11 Sep 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/vishnuhd/what-happens-when-you-ask-an-llm-a-question-4k4o</link>
      <guid>https://dev.to/vishnuhd/what-happens-when-you-ask-an-llm-a-question-4k4o</guid>
      <description>&lt;p&gt;I have been running models with &lt;a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; and &lt;a href="https://vishctl.dev/posts/vllm-on-wsl2-minikube/" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt;. Then I stopped to ask: what is actually happening behind that API call?&lt;/p&gt;

&lt;p&gt;A learning chat turned into these notes. Let’s follow one request all the way through:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why did my Kubernetes pod restart? The container’s last termination reason is OOMKilled.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is an illustrative example, not a live diagnosis. We will use a hypothetical &lt;strong&gt;2B model&lt;/strong&gt; throughout. Technically, the container restarted within the pod.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. An answer grows one token at a time
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;LLM&lt;/strong&gt; , or large language model, is a neural network trained on language. For the text-generating models here, the basic loop is: use the text so far to predict what comes next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Our pod question + the OOMKilled clue
                  |
                  v
             Tokenizer
          text -&amp;gt; token IDs
                  |
                  v
      Embeddings + position information
                  |
                  v
        Transformer layer 1
        attention + other maths
                  |
                  v
        Transformer layer 2
                  |
                 ...
                  |
                  v
         Scores for next tokens
                  |
                  v
           Choose one token
                  |
                  v
        Append it to the answer
                  |
                  v
           Stop condition?
            / \
          yes no
           | |
           v v
     Final answer Process chosen token
                    through the model
                    using the KV cache
                         |
                         +--&amp;gt; Score and choose again

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;token&lt;/strong&gt; can be a word, part of a word, or punctuation. The tokenizer assigns each piece an ID. An &lt;strong&gt;embedding&lt;/strong&gt; turns that ID into a list of numbers the network can process. The model also needs information about token positions.&lt;/p&gt;

&lt;p&gt;For our prompt, a tokenizer might split &lt;code&gt;OOMKilled&lt;/code&gt; into smaller pieces. Exact splits depend on the tokenizer. Each piece gets an ID, then a numerical vector.&lt;/p&gt;

&lt;p&gt;The answer might grow as &lt;code&gt;The&lt;/code&gt; → &lt;code&gt;container&lt;/code&gt; → &lt;code&gt;was&lt;/code&gt; → &lt;code&gt;killed&lt;/code&gt;. These are illustrative token boundaries. The model chooses from next-token scores using its generation settings, then repeats.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What does a 2B model actually contain?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;About two billion adjustable numbers&lt;/strong&gt; , called parameters. Most are weights used in the network’s calculations. They collectively encode learned patterns, rather than one fact per number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRAINING
Examples about language, code, containers...
                  |
                  v
        Predict the next token &amp;lt;---------+
                  | |
                  v |
       Compare with training target |
                  | |
                  v |
           Calculate the error |
                  | |
                  v |
         Adjust learned numbers ---------+

INFERENCE
Our pod question + learned numbers
                  |
                  v
       Calculations through layers
                  |
                  v
       Generate a restart explanation
       (learned numbers stay fixed)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A neural network is a stack of mathematical operations. A simple neuron combines weighted inputs, adds a bias, and applies an activation function. Many such operations let the network learn complex patterns.&lt;/p&gt;

&lt;p&gt;During training, examples about containers and memory can shape these numbers. Our question then uses those learned patterns to connect &lt;code&gt;OOMKilled&lt;/code&gt; with an out-of-memory event. There is no single “Kubernetes parameter.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training changes the parameters. Ordinary inference uses them.&lt;/strong&gt; Chatting adds context; it does not normally retrain the model. A bigger parameter count alone does not guarantee better answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Transformers connect the relevant pieces
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Transformer&lt;/strong&gt; is the architecture behind many LLMs. Its layers combine attention with other neural-network calculations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attention&lt;/strong&gt; mixes information from tokens in the available context. In a typical text generator, a token can attend to itself and earlier tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Token representations from our prompt
                  |
          +-------+-------+
          | | |
          v v v
          Q K V
       Queries Keys Values
          | | |
          +---+---+ |
              | |
              v |
      Compare Q with K |
              | |
              v |
      Mask future tokens |
      and form weights |
              | |
              +-----+-----+
                    |
                    v
       Weighted mixture of values
                    |
                    v
        Further layer calculations
                    |
                    v
      Later layers -&amp;gt; next-token scores

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our request, &lt;code&gt;OOMKilled&lt;/code&gt; is a useful clue for explaining &lt;code&gt;restart&lt;/code&gt;. Attention helps combine information from those positions while the layers build the response.&lt;/p&gt;

&lt;p&gt;The diagram is an intuition, not a measured attention map. &lt;strong&gt;Q, K, and V are learned numerical representations&lt;/strong&gt; , not literal questions or database entries. Multiple attention heads can capture different relationships. &lt;a href="https://developers.google.com/machine-learning/crash-course/llm/transformers" rel="noopener noreferrer"&gt;Google’s Transformer introduction&lt;/a&gt; explains the architecture further.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why can it reason, and still be wrong?
&lt;/h2&gt;

&lt;p&gt;Learning to predict language can build useful patterns for code, maths, and problem solving. Further training can improve instruction following and reasoning.&lt;/p&gt;

&lt;p&gt;A model can use intermediate steps to work through a problem. Some reasoning models spend additional computation before answering. This can help, but a fluent explanation is not proof.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Question: why did the container restart?
                  |
                  v
       Supplied clue: OOMKilled
                  |
                  v
      Learned relationship: memory
                  |
                  v
      Suggest checking limit and usage
                  |
                  v
       Verify against the real cluster

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an example of a useful explanation, not a trace of the model’s hidden internal computations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A useful answer: “OOMKilled indicates an out-of-memory kill. Check the container’s memory limit and memory usage.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It would be a leap to say &lt;strong&gt;“Your app definitely has a memory leak.”&lt;/strong&gt; Our prompt gives no evidence of a leak. The model also has not inspected the cluster; it only has the information we supplied.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Context is the input. KV cache saves work.
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;context window&lt;/strong&gt; limits how many tokens a request can accommodate, including input and generated output. Instructions, included chat history, and supplied documents all take space.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Our question + OOMKilled clue
              |
              v
           PREFILL
     Process the prompt
       / \
      v v
 Save prompt K/V Score first token
      | |
      v v
  [KV cache] Choose "The"
      | |
      | v
      +---------&amp;gt; DECODE &amp;lt;----------------+
      | Process "The" |
      | using earlier K/V |
      | | |
      | v |
      | Save new K/V |
      | Score and choose |
      | next token |
      | | |
      | v |
      | Stop condition? |
      | / \ |
      | yes no |
      | | | |
      | v +-----------+
      | Finish answer Process next token
      |
      +-- Cache grows as more tokens are processed

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prefill&lt;/strong&gt; processes the prompt and produces the scores for the first output token. &lt;strong&gt;Decode&lt;/strong&gt; continues generation, typically one token per sequence per step.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;KV cache&lt;/strong&gt; stores keys and values from earlier tokens so the model can reuse them. It is temporary attention data, not a permanent memory of you.&lt;/p&gt;

&lt;p&gt;For our request, prefill reads the question and &lt;code&gt;OOMKilled&lt;/code&gt; clue. Decode builds the answer. When generating the next piece after &lt;code&gt;The container&lt;/code&gt;, the model reuses cached keys and values from earlier tokens.&lt;/p&gt;

&lt;p&gt;Pasting 500 lines of pod logs would add input tokens and usually increase cache needs. It would not add parameters to the model.&lt;/p&gt;

&lt;p&gt;Two useful measurements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What I notice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to first token (TTFT)&lt;/td&gt;
&lt;td&gt;Wait until “The” appears, including queueing and prompt processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens per second&lt;/td&gt;
&lt;td&gt;How quickly the rest of the restart explanation appears&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  6. Why does a model need so much memory?
&lt;/h2&gt;

&lt;p&gt;GPUs accelerate the large matrix calculations. But the model also has to fit in memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fllm-basics%2F05-memory.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fllm-basics%2F05-memory.svg" alt="Ideal weight storage for our 2B model is 8 GB at 32 bits, 4 GB at 16 bits, 2 GB at 8 bits, or 1 GB at 4 bits. KV cache and runtime memory are additional."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quantization&lt;/strong&gt; represents numbers using fewer bits. The chart shows ideal weight storage: parameters multiplied by bits, divided by eight. Actual formats add overhead and may keep some weights at higher precision.&lt;/p&gt;

&lt;p&gt;FP16 and BF16 both use 16 bits, with different numerical ranges and precision. INT4 uses 4-bit integers. AWQ and GPTQ are quantization methods.&lt;/p&gt;

&lt;p&gt;For our 2B model, 16-bit weights take about 4 GB; ideal 4-bit storage takes about 1 GB. The question stays the same. We store the learned numbers more compactly, leaving more of my 8 GB GPU for the cache and runtime. Whether it fits depends on the model, format, and context. Quality can fall; speed gains depend on hardware and software.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What does vLLM add?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Our pod question (request A)
            |
            v
     vLLM request queue
            |
            v
 Scheduler chooses work &amp;lt;-------------------+
            | |
            v |
 Batch of scheduled tokens from A, B, C |
            | |
            v |
 GPU runs model using weights + KV cache |
            | |
            v |
 Return generated tokens to each user |
            | |
            v |
 Finished? -- no: schedule more work -------+
     |
    yes
     |
     v
 Release request resources

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;vLLM is serving software that runs a model efficiently.&lt;/strong&gt; Our pod question is &lt;strong&gt;request A&lt;/strong&gt;. Other users send requests B and C. vLLM can process them together while maintaining each request’s own context.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fllm-basics%2F06-serving.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fvishctl.dev%2Fimages%2Fposts%2Fllm-basics%2F06-serving.svg" alt="Continuous batching lets a new request enter when another finishes. PagedAttention maps each request’s KV cache to separate physical blocks."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous batching&lt;/strong&gt; updates the active batch as requests finish and capacity becomes available. &lt;strong&gt;PagedAttention&lt;/strong&gt; manages KV cache in blocks that need not sit together in memory, reducing wasted space. The &lt;a href="https://vllm-project.github.io/2023/06/20/vllm.html" rel="noopener noreferrer"&gt;vLLM team’s explanation&lt;/a&gt; connects these ideas to serving more requests.&lt;/p&gt;

&lt;p&gt;If B finishes while our restart explanation is still generating, D can join when capacity allows. Our request A continues, with its own KV blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. More GPUs, and fewer active experts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Split maths, split layers, or serve separate requests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TENSOR PARALLELISM
One layer's input for our question
             |
       +-----+-----+
       v v
     GPU 1 GPU 2
    part A part B
       | |
       +-----+-----+
             |
       Communicate / combine
             |
         Next layer

PIPELINE PARALLELISM
Our question -&amp;gt; GPU 1 -&amp;gt; GPU 2 -&amp;gt; Output
             early later
             layers layers

DATA PARALLELISM FOR SERVING
              Request routing
               / \
              v v
        Our question Other request
              | |
              v v
        Model copy 1 Model copy 2
              | |
              v v
        Our answer Their answer

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each replica can itself use multiple GPUs. For example, four GPUs could run two replicas, with two GPUs per replica.&lt;/p&gt;

&lt;p&gt;For the same pod question: &lt;strong&gt;TP&lt;/strong&gt; shares each layer’s calculations across GPUs; &lt;strong&gt;PP&lt;/strong&gt; passes the work through groups of layers; &lt;strong&gt;DP&lt;/strong&gt; sends our whole request to one model replica while another serves someone else. These are possible layouts, not a claim that our small model needs multiple GPUs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NCCL&lt;/strong&gt; is NVIDIA’s GPU communication library. It can move and combine data over connections such as PCIe and NVLink. Extra GPUs can also add waiting time, so scaling is not automatically a speedup. &lt;a href="https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/overview.html" rel="noopener noreferrer"&gt;NVIDIA’s overview&lt;/a&gt; describes that communication layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  How GPU communication connects the pieces
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPU 1 partial result GPU 2 partial result
          | |
          +------------+------------+
                       |
              Collective operation
                 (for example,
                NCCL AllReduce)
                       |
          +------------+------------+
          v v
GPU 1 combined result GPU 2 combined result

Communication travels over available connections,
such as NVLink or PCIe, depending on the hardware.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  MoE routes work to selected experts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Representation of a token from our question
                       |
                       v
                  Learned router
                       |
               +-------+-------+
               v v
            Expert 2 Expert 7
               | |
               +-------+-------+
                       |
                       v
             Combine expert outputs
                       |
                       v
              Continue through layers

Other experts are not selected for this token
in this illustrative MoE layer.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MoE&lt;/strong&gt; is an alternative architecture, not a serving switch for our dense 2B model. A mixture-of-experts model answering the same question selects a subset of expert networks for each token at an MoE layer. The routing is learned; experts are not necessarily named subject specialists. For a token in our question, a router might select experts 2 and 7. That does not make either a “Kubernetes expert.” Fewer active parameters reduce computation, but all weights still need storage somewhere. &lt;a href="https://arxiv.org/abs/2401.04088" rel="noopener noreferrer"&gt;Mixtral’s paper&lt;/a&gt; provides a concrete example.&lt;/p&gt;

&lt;p&gt;Memory capacity answers &lt;strong&gt;“Will it fit?”&lt;/strong&gt; Memory bandwidth answers &lt;strong&gt;“How fast can data move?”&lt;/strong&gt; Compute throughput answers &lt;strong&gt;“How fast can the maths run?”&lt;/strong&gt; Any of these, plus GPU communication, can limit performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole conversation flow
&lt;/h2&gt;

&lt;p&gt;The app supplies the conversation history it wants the model to use. The next turn is another request, with an updated context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MODEL SETUP OUR CONVERSATION
Learned parameters Pod question + OOMKilled
       | |
       v v
Choose weight format App assembles context
(optional quantization) instructions + messages
       | |
       v v
Load model on GPU(s) Tokenize and schedule
       | |
       +----------------+-------------------+
                        |
                        v
             Prefill through the model
             Build/reuse available KV cache
                        |
                        v
                  First output token
                        |
                        v
             Decode + extend KV cache
             Repeat until stopping
                        |
                        v
             Restart explanation to user
                        |
                        v
           Follow-up: "What should I check?"
                        |
                        v
            App includes relevant history
            plus this follow-up question
                        |
                        v
                  Next request

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache reuse between requests depends on the engine and matching context. Even without reuse, the app can send the history again and the engine can recompute it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five things I want to remember
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;My reminder&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parameters&lt;/td&gt;
&lt;td&gt;Learned numbers linking patterns such as OOMKilled and memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;td&gt;Pieces of our question and restart explanation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attention&lt;/td&gt;
&lt;td&gt;Combine the restart question with the OOMKilled clue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache&lt;/td&gt;
&lt;td&gt;Reuse earlier calculations as the explanation grows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inference engine&lt;/td&gt;
&lt;td&gt;Schedule our request and run the model&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Our final answer might be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The container was killed because of an out-of-memory event. Check its memory limit and usage to investigate why.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One question, learned numbers, context, repeated calculations, and an answer to verify against the cluster.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>localllm</category>
      <category>llmbasics</category>
      <category>gpu</category>
    </item>
    <item>
      <title>Run vLLM on Kubernetes with Minikube, WSL2 and NVIDIA GPU</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Tue, 08 Sep 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/vishnuhd/run-vllm-on-kubernetes-with-minikube-wsl2-and-nvidia-gpu-2me5</link>
      <guid>https://dev.to/vishnuhd/run-vllm-on-kubernetes-with-minikube-wsl2-and-nvidia-gpu-2me5</guid>
      <description>&lt;p&gt;This is the &lt;a href="https://docs.vllm.ai/" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt; entry in my local AI series. After testing &lt;a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, &lt;a href="https://vishctl.dev/posts/running-llama-cpp-on-32gb-macbook-air/" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;, and &lt;a href="https://vishctl.dev/posts/running-freetoken-on-8gb-laptop-gpu/" rel="noopener noreferrer"&gt;FreeToken&lt;/a&gt;, I wanted to run vLLM as a Kubernetes Deployment on my Windows/WSL2 setup.&lt;/p&gt;

&lt;p&gt;The plan was simple: deploy vLLM, request &lt;code&gt;nvidia.com/gpu: 1&lt;/code&gt;, expose an OpenAI-compatible API endpoint through a Service, and tie it into the Kubernetes workflows I write about regularly.&lt;/p&gt;

&lt;p&gt;Getting a GPU into Kubernetes on WSL2 turned into an investigation. Not because vLLM is hard, but because container runtimes and nested clusters handle GPU passthrough in non-obvious ways. Here is what happened, how the device plugin and node prerequisites work, and how to get a working GPU cluster running with Minikube.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You’ll Build
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows
   │
   ▼
WSL2 Ubuntu
   │
   ▼
Docker Engine + NVIDIA Container Toolkit
   │
   ▼
Minikube
   │
   ▼
Kubernetes
   │
   ├── NVIDIA GPU Operator
   │
   ├── NVIDIA Device Plugin
   │
   └── vLLM
         │
         ▼
      NVIDIA GPU

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the end of this guide, you’ll have vLLM running on Kubernetes with GPU acceleration and serving an OpenAI-compatible API.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Test Rig
&lt;/h2&gt;

&lt;p&gt;My test machine for this run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host Machine:&lt;/strong&gt; Windows 11 Laptop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU:&lt;/strong&gt; NVIDIA GeForce RTX 4070 Laptop GPU (8.0 GiB VRAM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host Memory:&lt;/strong&gt; 32 GB DDR5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WSL2 Environment:&lt;/strong&gt; Ubuntu 24.04 LTS (noble) with systemd enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NVIDIA Drivers:&lt;/strong&gt; Driver version 616.56 / CUDA 13.4 user-mode driver&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes:&lt;/strong&gt; Minikube v1.39.0 provisioning Kubernetes v1.37.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Qwen3.5-0.8B (&lt;code&gt;Qwen/Qwen3.5-0.8B&lt;/code&gt;) served by vLLM v0.28.0&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Troubleshooting vLLM GPU Support on Docker Desktop WSL2 Kubernetes
&lt;/h2&gt;

&lt;p&gt;Docker Desktop offers a one-click Kubernetes cluster in its settings. Turning it on provisions a single-node cluster named &lt;code&gt;desktop-control-plane&lt;/code&gt; running on &lt;code&gt;containerd://2.3.4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the WSL2 host, &lt;code&gt;nvidia-smi&lt;/code&gt; works fine, and running standalone GPU containers (&lt;code&gt;docker run --gpus all&lt;/code&gt;) works without issues. The next step was applying the standard NVIDIA Kubernetes device plugin DaemonSet to enable GPU scheduling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.16.2/deployments/static/nvidia-device-plugin.yml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DaemonSet pod started, but immediately stalled in a retry loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E... Incompatible strategy detected auto
E... If this is a GPU node, did you configure the NVIDIA Container Toolkit?
I... No devices found. Waiting indefinitely.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking the node capacity confirmed the issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe node desktop-control-plane | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s2"&gt;"Capacity:&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;Allocatable:"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq2zuyvjabfoalvpxgdxu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq2zuyvjabfoalvpxgdxu.png" alt="Docker Desktop Kubernetes node missing GPU resources" width="800" height="392"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Checking Docker Desktop’s desktop-control-plane: nvidia-smi finds the RTX 4070, but the node exposes zero GPU capacity.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this tested Docker Desktop Kubernetes setup, GPU resources were not exposed to the Kubernetes node. Despite GPU access working in WSL2 and standalone Docker containers, the Kubernetes node could not expose &lt;code&gt;nvidia.com/gpu&lt;/code&gt; to the device plugin.&lt;/p&gt;


&lt;h2&gt;
  
  
  How Kubernetes Detects NVIDIA GPUs Using the NVIDIA Device Plugin
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/NVIDIA/k8s-device-plugin" rel="noopener noreferrer"&gt;NVIDIA Kubernetes Device Plugin&lt;/a&gt; is a DaemonSet that exposes GPU hardware to the Kubernetes control plane. It does not run inference, and it does not install drivers or container runtimes.&lt;/p&gt;

&lt;p&gt;Under standard &lt;a href="https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/" rel="noopener noreferrer"&gt;Kubernetes GPU scheduling&lt;/a&gt;, pods request GPU resources by specifying limits for extended resources. The device plugin integrates with Kubelet through the Kubernetes Device Plugin API over gRPC, using a Unix domain socket at &lt;code&gt;/var/lib/kubelet/device-plugins/kubelet.sock&lt;/code&gt;. Its responsibility comes down to three functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discovery:&lt;/strong&gt; The plugin queries the host system using NVML (NVIDIA Management Library) to check how many physical GPUs are present and verify their health status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registration:&lt;/strong&gt; It registers with Kubelet and advertises the discovered GPUs as an extended allocatable resource named &lt;code&gt;nvidia.com/gpu&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocation:&lt;/strong&gt; When a pod requesting &lt;code&gt;nvidia.com/gpu: 1&lt;/code&gt; gets scheduled to the node, Kubelet calls the plugin’s &lt;code&gt;Allocate&lt;/code&gt; gRPC endpoint. The plugin picks a healthy GPU and sends back the device IDs and environment variables (&lt;code&gt;NVIDIA_VISIBLE_DEVICES=&amp;lt;UUID&amp;gt;&lt;/code&gt;) to Kubelet. Kubelet then passes those variables to the container runtime so the container gets access to &lt;code&gt;/dev/nvidia*&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Worker Node Prerequisites
&lt;/h3&gt;

&lt;p&gt;Because the device plugin only handles discovery and Kubelet registration, it assumes the worker node already has a functional GPU stack. For worker nodes in any Kubernetes cluster, three layers must be in place before the device plugin can run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Host Kernel Drivers:&lt;/strong&gt; The node OS must have the NVIDIA kernel modules loaded (&lt;code&gt;nvidia.ko&lt;/code&gt;, &lt;code&gt;nvidia-uvm.ko&lt;/code&gt;) and device nodes created in &lt;code&gt;/dev&lt;/code&gt;. Running &lt;code&gt;nvidia-smi&lt;/code&gt; on the host must return clean output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NVIDIA Container Toolkit:&lt;/strong&gt; Packages such as &lt;code&gt;libnvidia-container&lt;/code&gt; and &lt;code&gt;nvidia-container-toolkit&lt;/code&gt; must be installed on the host. Refer to the &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html" rel="noopener noreferrer"&gt;NVIDIA Container Toolkit install guide&lt;/a&gt; for distribution setup. This toolkit includes the OCI prestart hook that inspects container environment variables and mounts host GPU driver libraries into the target container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime Configuration:&lt;/strong&gt; The node container runtime (containerd or Docker) must have the NVIDIA runtime configured in its daemon settings (such as &lt;code&gt;/etc/containerd/config.toml&lt;/code&gt; or &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;) and set up to handle CDI (Container Device Interface) or NVIDIA runtime hooks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of those three pieces is missing or unconfigured, the device plugin pod will fail during startup or report zero allocatable GPUs.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Docker Desktop Kubernetes Cannot Detect My NVIDIA GPU
&lt;/h2&gt;

&lt;p&gt;Based on the runtime architecture observed in this setup, the issue comes down to how Docker Desktop isolates its cluster node.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;desktop-control-plane&lt;/code&gt; runs inside Docker Desktop’s private utility VM (&lt;code&gt;docker-desktop&lt;/code&gt;), isolated using &lt;strong&gt;&lt;code&gt;sysbox-runc&lt;/code&gt;&lt;/strong&gt; rather than the standard OCI runtime &lt;code&gt;runc&lt;/code&gt;. Sysbox provides nested container virtualization to spin up systemd, kubelet, and containerd within an unprivileged container environment.&lt;/p&gt;

&lt;p&gt;In this setup, that isolation layer prevents GPU passthrough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing OCI Passthrough:&lt;/strong&gt; The node container was not launched with NVIDIA GPU passthrough flags (&lt;code&gt;--gpus all&lt;/code&gt;). Inside the sandbox, containerd cannot access NVIDIA device nodes (&lt;code&gt;/dev/nvidia*&lt;/code&gt;) or the WSL2 DirectX driver mapping (&lt;code&gt;/usr/lib/wsl/lib&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locked-Down Lifecycle:&lt;/strong&gt; You cannot configure the NVIDIA Container Toolkit inside the node’s containerd because Docker Desktop internally supervises the container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daemon Defaults Do Not Propagate:&lt;/strong&gt; Setting &lt;code&gt;nvidia&lt;/code&gt; as the default OCI runtime in &lt;code&gt;docker-desktop&lt;/code&gt; WSL does not alter the &lt;code&gt;sysbox-runc&lt;/code&gt; runtime used by the Kubernetes node container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker has an open roadmap issue requesting native GPU passthrough for Docker Desktop Kubernetes. It remains an architectural boundary in the current setup.&lt;/p&gt;

&lt;p&gt;Because of these issues, I pivoted to running Minikube with WSL2 using native Docker.&lt;/p&gt;


&lt;h2&gt;
  
  
  Installing Docker CE and Containerd Inside WSL2
&lt;/h2&gt;

&lt;p&gt;To satisfy those node prerequisites and bypass Docker Desktop’s VM isolation, we install the native Docker Community Edition engine directly inside the Ubuntu 24.04 WSL2 environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add Docker's official GPG key&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/apt/keyrings
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc

&lt;span class="c"&gt;# Set up the repository&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Install Docker CE and containerd&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokcmg2ckk1i6mbl5jzxt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokcmg2ckk1i6mbl5jzxt.png" alt="Installing Docker CE and containerd in Ubuntu WSL2" width="800" height="430"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Configuring the official Docker CE repository and installing containerd and the Docker daemon inside Ubuntu WSL2.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Verify that both the Docker daemon and containerd services are running cleanly under systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status docker
systemctl status containerd

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u8nn53js84xxz2juxg0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u8nn53js84xxz2juxg0.png" alt="Verifying systemd status for Docker and containerd" width="800" height="430"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Confirming active (running) status for both docker.service and containerd.service under WSL2 systemd.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before moving to Kubernetes, sanity check that native Docker has full GPU access through the WSL2 NVIDIA container runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker container run &lt;span class="nt"&gt;--gpus&lt;/span&gt; all &lt;span class="nt"&gt;--rm&lt;/span&gt; nvidia/cuda:13.3.1-base-ubuntu26.04 nvidia-smi &lt;span class="nt"&gt;-L&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3pbxrc8suik3dqbg76m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3pbxrc8suik3dqbg76m.png" alt="Docker container GPU passthrough test" width="799" height="285"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Direct GPU passthrough test: Docker successfully identifies GPU 0 as the NVIDIA GeForce RTX 4070 Laptop GPU.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Run Minikube with NVIDIA GPU Support on WSL2
&lt;/h2&gt;

&lt;p&gt;Install the latest Minikube Debian package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; minikube_latest_amd64.deb

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx2bjcv0dg9axvqrjwijh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx2bjcv0dg9axvqrjwijh.png" alt="Installing Minikube package in WSL2" width="800" height="430"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Installing Minikube v1.39.0 via Debian package.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Following the official &lt;a href="https://minikube.sigs.k8s.io/docs/tutorials/nvidia/" rel="noopener noreferrer"&gt;Minikube GPU documentation&lt;/a&gt;, starting Minikube with GPU acceleration requires specific driver and runtime parameters. Now start Minikube. Two flags matter here and both are easy to miss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube start &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--driver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--container-runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12g &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extra-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apiserver.service-account-issuer&lt;span class="o"&gt;=&lt;/span&gt;https://kubernetes.default.svc &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extra-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apiserver.service-account-signing-key-file&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/minikube/certs/sa.key

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe6emtlj9cpwcfj1m46tm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe6emtlj9cpwcfj1m46tm.png" alt="Starting Minikube with GPU support and Docker runtime" width="800" height="232"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Minikube starting Kubernetes v1.37.0 on Docker, automatically detecting and enabling the nvidia-device-plugin addon.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT] &lt;strong&gt;Why &lt;code&gt;--container-runtime=docker&lt;/code&gt; is required:&lt;/strong&gt; By default, even when using &lt;code&gt;--driver=docker&lt;/code&gt;, Minikube configures containerd as its internal in-node runtime. However, Minikube’s &lt;code&gt;--gpus=all&lt;/code&gt; flag currently hooks into Docker’s OCI runtime wrapper. If you omit &lt;code&gt;--container-runtime=docker&lt;/code&gt;, Minikube fails during preflight checks with an invalid flag combination error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice the startup output: Minikube automatically detects the GPU and enables the &lt;code&gt;nvidia-device-plugin&lt;/code&gt; addon for you.&lt;/p&gt;


&lt;h2&gt;
  
  
  Verifying the Cluster and GPU Allocation
&lt;/h2&gt;

&lt;p&gt;Install &lt;code&gt;kubectl&lt;/code&gt; to talk to the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LO&lt;/span&gt; &lt;span class="s2"&gt;"https://dl.k8s.io/release/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; https://dl.k8s.io/release/stable.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/bin/linux/amd64/kubectl"&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; root &lt;span class="nt"&gt;-g&lt;/span&gt; root &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 kubectl /usr/local/bin/kubectl
kubectl version &lt;span class="nt"&gt;--client&lt;/span&gt;
kubectl get nodes
kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdcptshufztcc626sezqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdcptshufztcc626sezqd.png" alt="Kubectl verification and nvidia-device-plugin DaemonSet" width="800" height="265"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Node minikube is Ready (v1.37.0) and the nvidia-device-plugin-daemonset is 1/1 Running in kube-system.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, inspect the node’s schedulable capacity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe node minikube | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A7&lt;/span&gt; &lt;span class="s2"&gt;"Capacity:&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;Allocatable:"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Firfhegs3g88qpd7bmklw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Firfhegs3g88qpd7bmklw.png" alt="Minikube node capacity showing GPU resource" width="800" height="162"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Cluster verification: nvidia.com/gpu: 1 is now officially registered in Capacity and Allocatable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A quick test pod confirms the GPU is reachable from inside Kubernetes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;cat &amp;lt;&amp;lt;EOF | kubectl apply -f -&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpu-test&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cuda-test&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia/cuda:13.3.1-base-ubuntu26.04&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nvidia-smi"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-L"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;nvidia.com/gpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the test pod logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs gpu-test

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i840ukynab79p6li5j9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i840ukynab79p6li5j9.png" alt="CUDA test pod running on Minikube" width="800" height="430"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Test pod running nvidia-smi inside the Kubernetes cluster and successfully accessing the RTX 4070.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Real GPU scheduling and &lt;code&gt;nvidia.com/gpu&lt;/code&gt; resource accounting, without Docker Desktop getting in the way.&lt;/p&gt;


&lt;h2&gt;
  
  
  Deploy vLLM on Kubernetes with NVIDIA GPU
&lt;/h2&gt;

&lt;p&gt;Model choice for 8 GB VRAM: &lt;strong&gt;Qwen3.5-0.8B&lt;/strong&gt; (&lt;code&gt;Qwen/Qwen3.5-0.8B&lt;/code&gt;). Small enough to leave real headroom for vLLM’s KV cache, which is the main reason to use vLLM over llama.cpp or Ollama in the first place, and it is a capable model at that size. As detailed in the &lt;a href="https://docs.vllm.ai/" rel="noopener noreferrer"&gt;vLLM documentation&lt;/a&gt;, the server exposes an OpenAI-compatible API endpoint over HTTP.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Shared Memory (&lt;code&gt;/dev/shm&lt;/code&gt;) Gotcha
&lt;/h3&gt;

&lt;p&gt;When running vLLM via the Docker CLI, passing &lt;code&gt;--ipc=host&lt;/code&gt; lets workers exchange tensors and state across processes using host shared memory. In Kubernetes, pods do not share the host IPC namespace by default, and Kubernetes provisions &lt;code&gt;/dev/shm&lt;/code&gt; as a minimal 64 MB tmpfs mount.&lt;/p&gt;

&lt;p&gt;Some PyTorch and vLLM multiprocessing workloads can require significantly more shared memory than Kubernetes’ default &lt;code&gt;/dev/shm&lt;/code&gt; (which defaults to a minimal 64 MB tmpfs mount). If insufficient shared memory is available, initialization or worker processes may fail. The Kubernetes solution is mounting an &lt;code&gt;emptyDir&lt;/code&gt; volume backed by host RAM (&lt;code&gt;medium: Memory&lt;/code&gt;) with a dedicated &lt;code&gt;sizeLimit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vllm-qwen-k8s.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm-qwen&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm-qwen&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm-qwen&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm-qwen&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm/vllm-openai:v0.28.0&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--model=Qwen/Qwen3.5-0.8B"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--gpu-memory-utilization=0.7"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--max-model-len=8192"&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;nvidia.com/gpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hf-cache&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/root/.cache/huggingface&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dshm&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/shm&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hf-cache&lt;/span&gt;
          &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dshm&lt;/span&gt;
          &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;medium&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Memory&lt;/span&gt;
            &lt;span class="na"&gt;sizeLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2Gi&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm-qwen&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm-qwen&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two practical notes on this manifest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image version pinning:&lt;/strong&gt; I pin the image version (&lt;code&gt;v0.28.0&lt;/code&gt;) here so the deployment remains reproducible. You can update the image tag after validating compatibility with your CUDA and model version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hugging Face cache:&lt;/strong&gt; &lt;code&gt;emptyDir&lt;/code&gt; is used here for simplicity. The Hugging Face model cache is lost when the pod is recreated. For repeated testing or larger models, consider using a PersistentVolumeClaim (PVC) or a host-mounted path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apply the manifest and watch the deployment roll out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; vllm-qwen-k8s.yaml
kubectl get pods,svc

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8x6vocfcy4vq4jlqy8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8x6vocfcy4vq4jlqy8v.png" alt="vLLM deployment and service running" width="799" height="209"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Applying vllm-qwen-k8s.yaml and confirming the vLLM pod reaches 1/1 Running status alongside the ClusterIP service.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Stream the pod logs to inspect the vLLM initialization sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs vllm-qwen-5c8bbc786f-gdpmj &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr05518ctj7zm3hm7lj8a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr05518ctj7zm3hm7lj8a.png" alt="vLLM pod engine initialization logs" width="800" height="476"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;vLLM v0.28.0 engine initialization: resolving Qwen3_5ForConditionalGeneration, setting max model length to 8192, and warming up CUDA graphs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once PyTorch compilation and CUDA graph capture finish, the ASGI application completes startup and begins listening on port 8000:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F61otl8bchs76th1078qj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F61otl8bchs76th1078qj.png" alt="vLLM application startup complete and request logs" width="800" height="430"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;vLLM application startup complete: exposing OpenAI-compatible endpoints (/v1/chat/completions) with live throughput metrics.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Testing Inference and Hardware Telemetry
&lt;/h2&gt;

&lt;p&gt;Forward port 8000 from the cluster service to the local machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward svc/vllm-qwen 8000:8000

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now issue an OpenAI-standard chat completion request using &lt;code&gt;curl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "Qwen/Qwen3.5-0.8B",
    "messages": [{"role": "user", "content": "Say hello in one sentence."}]
  }'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhshrcxf56w72htk64p0z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhshrcxf56w72htk64p0z.png" alt="Calling vLLM chat completions API with curl" width="800" height="329"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Inference test: 48 tokens generated cleanly through the OpenAI-compatible vLLM API running in Minikube.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The response returned without issues, with server logs showing generation throughput averaging 23.5 tokens/s.&lt;/p&gt;

&lt;p&gt;Finally, checking Windows Task Manager confirms the hardware utilization on the host:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4d6y70anp7x8ol7xirq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4d6y70anp7x8ol7xirq.png" alt="Windows Task Manager GPU memory usage" width="800" height="476"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Windows Task Manager telemetry: 4.4 GB of 8.0 GB dedicated VRAM allocated to the WSL2 vLLM pod at 45°C.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dedicated GPU memory held at &lt;strong&gt;4.4 / 8.0 GB&lt;/strong&gt; (in line with the &lt;code&gt;--gpu-memory-utilization 0.7&lt;/code&gt; setting plus desktop display overhead), with the GPU temperature at 45°C.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beyond Local Dev: Why Production Clusters Use the NVIDIA GPU Operator
&lt;/h2&gt;

&lt;p&gt;For a single-node laptop or a Minikube sandbox, managing drivers on the host and running a standalone device plugin DaemonSet gets the job done.&lt;/p&gt;

&lt;p&gt;In production Kubernetes clusters, manual node configuration does not scale. When you operate multi-node clusters across cloud providers or bare metal, nodes get provisioned dynamically by autoscalers, Linux kernel patch levels drift, and different GPU architectures (such as A100, H100, or L40S) coexist in the same cluster.&lt;/p&gt;

&lt;p&gt;This is why production setups deploy the &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/" rel="noopener noreferrer"&gt;NVIDIA GPU Operator&lt;/a&gt; instead of managing the device plugin directly.&lt;/p&gt;

&lt;p&gt;The GPU Operator automates the deployment and lifecycle management of the NVIDIA software stack required for GPU-enabled Kubernetes nodes. Driven by the &lt;code&gt;ClusterPolicy&lt;/code&gt; Custom Resource Definition, it manages several components depending on your cluster configuration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Driver Management:&lt;/strong&gt; Depending on the operating environment, the Operator can compile and load the NVIDIA driver container matched to the node kernel, or use pre-installed host drivers (common in managed Kubernetes environments like GKE, EKS, or AKS).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container Toolkit and CDI Configuration:&lt;/strong&gt; Depending on the Operator version and runtime configuration, GPU injection can be configured through direct NVIDIA runtime integration or CDI-based device injection (Container Device Interface), which modern releases enable by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device Plugin Supervision:&lt;/strong&gt; It deploys and manages the lifecycle of the NVIDIA Kubernetes Device Plugin DaemonSet, keeping it in sync with driver and runtime availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node Feature Discovery (GFD):&lt;/strong&gt; It automatically detects physical GPU capabilities and labels each node (such as &lt;code&gt;nvidia.com/gpu.product: NVIDIA-A100-SXM4-80GB&lt;/code&gt; or &lt;code&gt;nvidia.com/gpu.family: ampere&lt;/code&gt;). Workloads can then target specific GPU models using standard Kubernetes &lt;code&gt;nodeSelector&lt;/code&gt; or affinity rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster Metrics with DCGM Exporter:&lt;/strong&gt; It runs the Data Center GPU Manager (DCGM) exporter to collect metrics (GPU compute utilization, VRAM usage, temperature, power draw, and memory bandwidth) and expose them to Prometheus for cluster monitoring and alerting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIG Management:&lt;/strong&gt; On enterprise GPUs that support Multi-Instance GPU (MIG), the Operator can dynamically partition a single physical GPU into isolated hardware instances, allowing smaller workloads to share an A100 or H100 without memory interference.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In short: the device plugin provides basic GPU scheduling for pods. The GPU Operator manages the operational lifecycle, monitoring, and driver stack required to run GPU infrastructure reliably in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Docker Desktop Kubernetes isolates nodes without GPU passthrough:&lt;/strong&gt; In tested setups, &lt;code&gt;desktop-control-plane&lt;/code&gt; runs in a nested &lt;code&gt;sysbox-runc&lt;/code&gt; sandbox without GPU device passthrough flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minikube on native Docker CE in WSL2 works reliably:&lt;/strong&gt; Launching Minikube with &lt;code&gt;--driver=docker --container-runtime=docker --gpus=all&lt;/code&gt; automatically activates the NVIDIA device plugin addon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understand device plugin vs node prerequisites:&lt;/strong&gt; The device plugin only manages discovery and Kubelet allocation. Host drivers, Container Toolkit, and runtime configuration must already exist on the worker node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always size &lt;code&gt;/dev/shm&lt;/code&gt; in Kubernetes:&lt;/strong&gt; Multiprocessing in PyTorch and vLLM quickly exceeds the default 64 MB tmpfs mount. Mount an &lt;code&gt;emptyDir&lt;/code&gt; memory volume backed by host RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget VRAM carefully:&lt;/strong&gt; Setting &lt;code&gt;--gpu-memory-utilization 0.7&lt;/code&gt; on a compact model like Qwen3.5-0.8B preserves headroom for the KV cache on an 8 GB GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy the NVIDIA GPU Operator in production:&lt;/strong&gt; Production multi-node clusters rely on the GPU Operator to manage drivers, CDI device injection, node feature discovery, and DCGM monitoring.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Thanks for reading! If you run into issues setting up GPU passthrough on your machine, have questions, or have suggestions for other local AI setups to explore, leave a comment below.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>localllm</category>
      <category>vllm</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Running a 35B MoE Model on an 8 GB Laptop GPU: Testing FreeToken</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Mon, 07 Sep 2026 06:30:00 +0000</pubDate>
      <link>https://dev.to/vishnuhd/running-a-35b-moe-model-on-an-8-gb-laptop-gpu-testing-freetoken-37ib</link>
      <guid>https://dev.to/vishnuhd/running-a-35b-moe-model-on-an-8-gb-laptop-gpu-testing-freetoken-37ib</guid>
      <description>&lt;p&gt;Running large language models locally usually comes down to a hard hardware boundary: video RAM. If a model does not fit into your GPU’s VRAM, performance usually falls off a cliff as soon as standard runtimes fall back to system memory over the PCIe bus.&lt;/p&gt;

&lt;p&gt;Mixture of Experts (MoE) architectures offer an attractive theoretical escape hatch. While the total parameter count can be large (30B to 70B+), only a sparse subset of expert layers activates for any given token. However, standard local runtimes still require loading the entire weight footprint into memory, which puts 30B+ models out of reach for everyday consumer laptops with 8 GB of VRAM.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://github.com/FlashML-org/FreeToken" rel="noopener noreferrer"&gt;FreeToken&lt;/a&gt; by FlashML, backed by their research paper (&lt;a href="https://arxiv.org/abs/2608.16157" rel="noopener noreferrer"&gt;arXiv:2608.16157&lt;/a&gt;). FreeToken is an edge inference runtime designed specifically to run frontier MoE models on consumer hardware by dynamically managing a hierarchical cache between GPU VRAM and host RAM.&lt;/p&gt;

&lt;p&gt;I recently downloaded FreeToken to put their claims to the test on my modest laptop GPU. Here is what the setup looked like, how FreeToken handles memory under the hood, and the real-world generation numbers I observed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Test Rig
&lt;/h3&gt;

&lt;p&gt;My test machine is a portable laptop, not a high-end multi-GPU workstation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; 13th Gen Intel Core i9-13900H (14 cores, 20 threads)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU:&lt;/strong&gt; NVIDIA GeForce RTX 4070 Laptop GPU (8.0 GiB VRAM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host RAM:&lt;/strong&gt; 32 GB DDR5 (31.7 GiB usable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operating System:&lt;/strong&gt; Windows 11&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; FreeToken Desktop (v0.2.0-beta.17)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd367gulytijkx9md6xzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd367gulytijkx9md6xzf.png" alt="FreeToken website download page" width="800" height="450"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;The FreeToken landing page highlights bringing frontier models to consumer edge hardware.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On paper, an 8 GB VRAM budget makes running a 35-billion parameter model look impossible. In traditional setups, 8 GB VRAM limits you to 7B or 8B parameter models in 4-bit quantizations (such as Q4_K_M). Attempting to load a 35B model typically triggers out-of-memory errors or slows inference to a crawl.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing FreeToken and Exploring the Library
&lt;/h3&gt;

&lt;p&gt;The FreeToken desktop app is available to download directly from the &lt;a href="https://www.flashml.ai/" rel="noopener noreferrer"&gt;FlashML website&lt;/a&gt;. It provides a self-contained installer that is quick to set up on Windows. When you launch it, the interface automatically detects your hardware specs, available VRAM, and system RAM, and presents a curated library of models optimized for edge offloading.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fweqhvybt44jqoncqe1n6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fweqhvybt44jqoncqe1n6.png" alt="FreeToken hardware detection and model library" width="800" height="450"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;FreeToken accurately detects the RTX 4070 Laptop GPU (8 GB) and 32 GB RAM, recommending compatible models.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For this test, I selected &lt;code&gt;Qwen3.6-35B-A3B NVFP4&lt;/code&gt; (&lt;code&gt;nvidia/Qwen3.6-35B-A3B-NVFP4&lt;/code&gt;). This is a 35-billion parameter MoE model using NVIDIA’s 4-bit floating point (NVFP4) format, with an initial download size of 21.9 GiB.&lt;/p&gt;

&lt;p&gt;Before loading the model, the console dashboard showed clean baseline resource usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU VRAM:&lt;/strong&gt; 0.4 / 8.0 GiB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host RAM:&lt;/strong&gt; 11.0 / 31.7 GiB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU Temp:&lt;/strong&gt; 42°C at idle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pnltwzc8rvgl2y6xk5c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pnltwzc8rvgl2y6xk5c.png" alt="System resources at idle before model loading" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Idle console telemetry: 0.4 GiB VRAM used, ready for model weights.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Weight Conversion: The FTW Format
&lt;/h3&gt;

&lt;p&gt;Once the 21.9 GiB download finished, FreeToken flagged the model as needing conversion before it could be launched.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls063lnop1brrmbf4xy2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls063lnop1brrmbf4xy2.png" alt="Download complete prompt indicating conversion is required" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Raw model download complete (21.9 GiB), prompting for weight conversion.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FreeToken converts raw Hugging Face weights into its proprietary format called &lt;strong&gt;FTW&lt;/strong&gt; (FreeToken Weight format). During this process, the engine repacks and organizes the tensors into memory-mapped structures optimized for rapid streaming between host RAM and GPU memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff76vrgikjx5gvfft7i8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff76vrgikjx5gvfft7i8r.png" alt="Converting raw weights into FTW format" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Repacking raw weights into FTW format for fast memory streaming.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The conversion took just over a minute on the i9-13900H and NVMe storage, producing a compact 19.5 GiB weight package.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9oklwq5pihn6268a17qu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9oklwq5pihn6268a17qu.png" alt="Conversion complete showing 19.5 GiB repacked size" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Conversion complete: 19.5 GiB ready for execution.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading the Model and Memory Allocation
&lt;/h3&gt;

&lt;p&gt;Starting the model triggers the weight allocation phase. Instead of attempting to cram the entire 19.5 GiB into the 8 GB VRAM, FreeToken partitions the workload across both memory tiers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdrg79g6n0nq9icbkvzzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdrg79g6n0nq9icbkvzzf.png" alt="Loading model weights into VRAM" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Starting model and populating active weights into VRAM.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once loaded, the memory profile was eye-opening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VRAM Usage:&lt;/strong&gt; 6.8 GiB / 8.0 GiB (85% utilization, leaving a healthy buffer for OS display compositing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host RAM Usage:&lt;/strong&gt; 30.2 GiB / 31.7 GiB (95% utilization)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU Status:&lt;/strong&gt; Running at 45°C, drawing only 7W at idle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl33oy02bvhpdnl58jzih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl33oy02bvhpdnl58jzih.png" alt="Model running with VRAM and RAM utilization" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Model running: 6.8 GiB in VRAM and 30.2 GiB in system RAM.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cache Architecture Under the Hood
&lt;/h3&gt;

&lt;p&gt;The console view provides detailed insight into how FreeToken manages this memory footprint:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;MoE Expert Cache:&lt;/strong&gt; FreeToken allocated 916 active expert slots in VRAM out of a total pool of 10,240 slots, consuming 1.51 GiB of VRAM. As different experts are needed during generation, they are dynamically paged in and out from the host RAM cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KV Cache:&lt;/strong&gt; 8K tokens of context were allocated in VRAM, consuming just 0.16 GiB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mamba State Slots:&lt;/strong&gt; 24 state slots were reserved, using 1.44 GiB of VRAM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frji5uu6378ll6y9a14r0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frji5uu6378ll6y9a14r0.png" alt="Cache architecture telemetry in console" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Detailed cache breakdown: MoE expert cache, KV cache, and Mamba state slots in VRAM.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This hierarchical design is the core reason the model can run without crashing. Inactive experts reside in system RAM (filling ~30 GB of system memory), while the active attention layers, KV cache, and currently triggered experts reside in the 8 GB VRAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World Performance &amp;amp; Generation Speed
&lt;/h3&gt;

&lt;p&gt;To evaluate real-world performance, I tested a conversational prompt with reasoning enabled:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; “hi, how are you ?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model engaged its full thinking process, taking 1 minute and 9 seconds to plan and structure its response, and then generated 393 tokens at &lt;strong&gt;4.3 tokens per second&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthmli302y7d89y924fx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthmli302y7d89y924fx7.png" alt="Chat inference test output and token speed" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Chat generation benchmark: 393 tokens generated at 4.3 tok/s with full chain-of-thought thinking.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While 4.3 tokens per second is not real-time voice conversational speed, it is remarkably steady for running a &lt;strong&gt;35-billion parameter model on an 8 GB laptop GPU&lt;/strong&gt;. For tasks like code review, background agent execution, document analysis, or local drafting, 4.3 tok/s is completely practical.&lt;/p&gt;

&lt;p&gt;The model detail sheet confirms the architecture and licensing details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base Repository:&lt;/strong&gt; &lt;code&gt;nvidia/Qwen3.6-35B-A3B-NVFP4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Length:&lt;/strong&gt; 262,144 tokens (256K context window)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache 2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v7bo8erbvsu9in0zl9v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v7bo8erbvsu9in0zl9v.png" alt="Model detail card on FreeToken" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Model details: 262K context window, NVFP4 quantization, and Apache 2.0 license.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-in Agentic Tooling &amp;amp; Local Endpoints
&lt;/h3&gt;

&lt;p&gt;FreeToken also includes built-in support for developer tools and coding agents.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Apps&lt;/strong&gt; tab exposes both OpenAI-compatible and Anthropic-compatible local HTTP endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI Endpoint:&lt;/strong&gt; &lt;code&gt;http://127.0.0.1:1919/v1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic Messages Endpoint:&lt;/strong&gt; &lt;code&gt;http://127.0.0.1:1919/v1/messages&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5crllaxnalcz9c15qqk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5crllaxnalcz9c15qqk.png" alt="FreeToken Apps tab showing local endpoints and integrations" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Local OpenAI and Anthropic compatible endpoints with one-click configurations for Claude Code, Codex, opencode, and more.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FreeToken provides quick configuration presets for popular coding assistants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code:&lt;/strong&gt; Configurable via local Anthropic endpoint routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex / opencode / openclaw:&lt;/strong&gt; One-click environment setups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hermes / DeepSeek Harness:&lt;/strong&gt; Direct command-line integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Having drop-in compatibility with both OpenAI and Anthropic API schemas means you can point tools like Claude Code directly to your localhost port without running a reverse proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where FreeToken Fits
&lt;/h3&gt;

&lt;p&gt;FreeToken is a relatively young product, but its approach solves a real constraint for local inference.&lt;/p&gt;

&lt;h4&gt;
  
  
  Strengths
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bypasses the VRAM limit:&lt;/strong&gt; Running a 35B MoE model on an 8 GB laptop GPU without manual layer-by-layer offload tuning works out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hierarchical caching:&lt;/strong&gt; The dynamic MoE expert cache keeps VRAM usage around 6.8 GiB, preventing driver resets and CUDA out-of-memory crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local developer endpoints:&lt;/strong&gt; Dual support for OpenAI and Anthropic endpoints makes integration with modern coding agents straightforward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple setup on Windows:&lt;/strong&gt; No manual compilation, no CUDA toolkit troubleshooting, and a clean desktop GUI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Limitations
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System RAM is the real requirement:&lt;/strong&gt; While VRAM requirements drop significantly, your host RAM must be large enough to hold the repacked weights. On my machine, the model took 30.2 GB of system RAM. If your laptop only has 16 GB of RAM, you will not be able to run 35B models this way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initial conversion step:&lt;/strong&gt; Each new model requires a one-time repacking step into the FTW format before the first run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation throughput:&lt;/strong&gt; At roughly 4.3 tokens per second, it is well suited for asynchronous workloads, coding agents, and complex reasoning queries, but not for instant conversational back-and-forth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wrap-up
&lt;/h3&gt;

&lt;p&gt;If you have a laptop with 32 GB of system RAM and a modest 8 GB NVIDIA GPU, FreeToken is worth checking out. It makes 35B-class MoE models runnable on consumer hardware that would otherwise choke on them, keeping your data completely local.&lt;/p&gt;

&lt;p&gt;You can download the desktop app directly from &lt;a href="https://www.flashml.ai/" rel="noopener noreferrer"&gt;FlashML&lt;/a&gt;, check out the project on &lt;a href="https://github.com/FlashML-org/FreeToken" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and read their research paper on &lt;a href="https://arxiv.org/abs/2608.16157" rel="noopener noreferrer"&gt;arXiv:2608.16157&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>localllm</category>
      <category>freetoken</category>
      <category>gpu</category>
    </item>
    <item>
      <title>Kubernetes is Adding a K to YAML: Why KYAML Makes Sense</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/vishnuhd/kubernetes-is-adding-a-k-to-yaml-why-kyaml-makes-sense-25lf</link>
      <guid>https://dev.to/vishnuhd/kubernetes-is-adding-a-k-to-yaml-why-kyaml-makes-sense-25lf</guid>
      <description>&lt;p&gt;Kubernetes 1.37 (“Garhwal”) just made KYAML Stable. YAML with a K bolted on, and it’s a genuinely useful feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;YAML has two well known landmines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type coercion.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NO&lt;/span&gt; &lt;span class="c1"&gt;# becomes boolean false&lt;/span&gt;
&lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt; &lt;span class="c1"&gt;# becomes boolean true&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.10&lt;/span&gt; &lt;span class="c1"&gt;# becomes float 3.1&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those values were meant to be strings. YAML quietly turned them into something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whitespace as structure.&lt;/strong&gt; Indentation defines meaning in normal YAML. Get one space wrong and the file still parses. It just means something else now, silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt; &lt;span class="c1"&gt;# one space off, this attaches to the wrong container&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No error. No warning. Just wrong.&lt;/p&gt;

&lt;p&gt;Helm makes this worse, since you’re injecting indentation from outside the YAML context and hoping it lines up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What KYAML actually changes
&lt;/h2&gt;

&lt;p&gt;Nothing about the language. It’s YAML’s existing “flow style,” just enforced. Every KYAML file is valid YAML. Old tooling, old parsers, old &lt;code&gt;kubectl&lt;/code&gt; versions all read it fine.&lt;/p&gt;

&lt;p&gt;Three rules: objects use &lt;code&gt;{ }&lt;/code&gt;, arrays use &lt;code&gt;[]&lt;/code&gt;, strings are always quoted. Numbers and booleans stay bare, since they’re actually meant to be those types.&lt;/p&gt;

&lt;p&gt;One example, everything at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# before&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;automountServiceAccountToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REGION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NO&lt;/span&gt;


&lt;span class="c1"&gt;# after (KYAML)&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apps/v1"&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deployment"&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;automountServiceAccountToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
        &lt;span class="pi"&gt;{&lt;/span&gt;
          &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
          &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nginx"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
          &lt;span class="nv"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;8080&lt;/span&gt; &lt;span class="pi"&gt;}],&lt;/span&gt;
          &lt;span class="nv"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NO"&lt;/span&gt; &lt;span class="pi"&gt;}],&lt;/span&gt;
        &lt;span class="pi"&gt;},&lt;/span&gt;
      &lt;span class="pi"&gt;],&lt;/span&gt;
    &lt;span class="pi"&gt;},&lt;/span&gt;
  &lt;span class="pi"&gt;},&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything in one shot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{ }&lt;/code&gt; for objects (&lt;code&gt;metadata&lt;/code&gt;, &lt;code&gt;spec&lt;/code&gt;, each container)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[]&lt;/code&gt; for arrays (&lt;code&gt;containers&lt;/code&gt;, &lt;code&gt;ports&lt;/code&gt;, &lt;code&gt;env&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;strings always quoted (&lt;code&gt;"api"&lt;/code&gt;, &lt;code&gt;"nginx"&lt;/code&gt;, &lt;code&gt;"REGION"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;replicas: 2&lt;/code&gt; and &lt;code&gt;containerPort: 8080&lt;/code&gt; stay bare, they’re real numbers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;automountServiceAccountToken: false&lt;/code&gt; and &lt;code&gt;hostNetwork: true&lt;/code&gt; stay bare, they’re real booleans&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;value: "NO"&lt;/code&gt; is quoted, because it’s a string that would otherwise get read as &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;KYAML doesn’t remove types, it just stops guessing which one you meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;Since structure comes from &lt;code&gt;{ }&lt;/code&gt; and &lt;code&gt;[]&lt;/code&gt;, not from spaces, indentation stops being load bearing. Squash a KYAML file onto one line and it means exactly the same thing. That kills the Helm whitespace problem outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get deployment api &lt;span class="nt"&gt;-o&lt;/span&gt; kyaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convert existing files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/google/yamlfmt/cmd/yamlfmt@latest
yamlfmt &lt;span class="nt"&gt;--kyaml&lt;/span&gt; manifest.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also ships as a pre-commit hook and a Docker image for CI.&lt;/p&gt;

&lt;p&gt;Nothing forces the switch. Plain YAML keeps working. Mixed repos, some converted, some not, are fine.&lt;/p&gt;

&lt;p&gt;My plan if I touch this on a real repo: one directory first, verify round-trip parsing, check Helm and Kustomize don’t reformat it differently, then expand once it’s boring.&lt;/p&gt;

&lt;p&gt;Not required. Cheap upgrade. Worth exploring.&lt;/p&gt;

&lt;p&gt;For a broader breakdown of what else landed in this release, check out my notes on &lt;a href="https://vishctl.dev/posts/kubernetes-v1-37-stability/" rel="noopener noreferrer"&gt;Kubernetes v1.37: An Operator’s Look at Garhwal&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>yaml</category>
      <category>devops</category>
    </item>
    <item>
      <title>Running llama.cpp on a 32 GB MacBook Air: A Direct Comparison with Ollama</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Thu, 03 Sep 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/vishnuhd/running-llamacpp-on-a-32-gb-macbook-air-a-direct-comparison-with-ollama-4mc9</link>
      <guid>https://dev.to/vishnuhd/running-llamacpp-on-a-32-gb-macbook-air-a-direct-comparison-with-ollama-4mc9</guid>
      <description>&lt;p&gt;In the &lt;a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;, I ran Ornith 1.5 9B on my 32 GB MacBook Air using Ollama and recorded baseline token-generation speeds on short prompts. Ollama is great for getting up and running quickly, but under the hood, its inference engine is built on &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this post, we go one level down: building and running llama.cpp directly, offloading inference to Apple Silicon’s Metal GPU, and comparing performance numbers side by side with Ollama on the exact same model and quantization level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Build llama.cpp from Source
&lt;/h3&gt;

&lt;p&gt;Building llama.cpp from source on macOS is fast and straightforward. On Apple Silicon, CMake automatically enables Metal support (&lt;code&gt;GGML_METAL=ON&lt;/code&gt;) and compiles GPU compute kernels tailored for Apple’s unified memory architecture.&lt;/p&gt;

&lt;p&gt;Clone the repository and compile the release binaries using all available CPU cores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ggml-org/llama.cpp
&lt;span class="nb"&gt;cd &lt;/span&gt;llama.cpp
cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-DCMAKE_BUILD_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Release
cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;--config&lt;/span&gt; Release &lt;span class="nt"&gt;-j&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sysctl &lt;span class="nt"&gt;-n&lt;/span&gt; hw.logicalcpu&lt;span class="si"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the build finishes, the binaries live in &lt;code&gt;build/bin&lt;/code&gt;. Add them to your current session’s &lt;code&gt;$PATH&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/build/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make this permanent across terminal sessions, append it to your shell configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"export PATH=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/build/bin:&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;PATH&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm that the build succeeded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-server &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Serve the Model (Out-of-the-Box Baseline)
&lt;/h3&gt;

&lt;p&gt;llama.cpp can download GGUF models directly from Hugging Face Hub using the &lt;code&gt;-hf&lt;/code&gt; flag, caching weights under &lt;code&gt;~/.cache/huggingface/hub&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-server &lt;span class="nt"&gt;-hf&lt;/span&gt; ornith-ai/Ornith-1.5-9B-GGUF &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what the flags do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-hf ornith-ai/Ornith-1.5-9B-GGUF&lt;/code&gt;: Resolves the model on Hugging Face and downloads the default &lt;code&gt;Q4_K_M&lt;/code&gt; quant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-ngl 99&lt;/code&gt;: Offloads all 99 model layers to the GPU (Metal on Apple Silicon unified memory).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--port 8080&lt;/code&gt;: Binds the OpenAI-compatible HTTP server to port 8080.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbgigy0kulpp4ctuhaf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbgigy0kulpp4ctuhaf3.png" alt="Starting llama-server and downloading Ornith 1.5 9B from Hugging Face" width="800" height="356"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Starting llama-server with automatic Hugging Face download and full Metal GPU offload.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;During startup, llama.cpp downloads both the multimodal vision projector (&lt;code&gt;mmproj-Ornith-1.5-9B-BF16.gguf&lt;/code&gt;) and the main model weights (&lt;code&gt;Ornith-1.5-9B-Q4_K_M.gguf&lt;/code&gt;), offloads the layers into Metal, initializes inference slots, and listens on port 8080.&lt;/p&gt;

&lt;p&gt;We test this default setup with a short greeting prompt through the OpenAI-compatible API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "ornith-1.5-9b",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj38yy89oe3k9gzt9q5sf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj38yy89oe3k9gzt9q5sf.png" alt="Calling the llama-server chat completions API" width="800" height="150"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;First chat completion request via curl, returning detailed server-side timing breakdowns.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The response returns a complete timing breakdown in the &lt;code&gt;timings&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt eval time = 496.21 ms / 12 tokens (24.18 tokens per second)
eval time = 5527.17 ms / 96 tokens (17.19 tokens per second)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to Ollama’s numbers on the same model and quant from the &lt;a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/" rel="noopener noreferrer"&gt;earlier post&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Ollama (Default)&lt;/th&gt;
&lt;th&gt;llama.cpp (Default, no &lt;code&gt;-fa&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt Eval Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37.13 tokens/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;24.18 tokens/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generation Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16.68 tokens/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.19 tokens/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Generation speed is practically neck-and-neck (~17 tokens/s). However, prompt evaluation (prefill latency) was noticeably slower on vanilla llama.cpp out of the box (24.18 tokens/s vs. Ollama’s 37.13 tokens/s).&lt;/p&gt;

&lt;p&gt;Ollama enables FlashAttention and sensible batch sizes by default, while raw llama.cpp keeps conservative baseline settings unless configured explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Verifying the Quants
&lt;/h3&gt;

&lt;p&gt;Before comparing benchmark numbers, it is critical to confirm that both engines are executing the exact same quantization format. Comparing different quants (e.g. Q4_K_M vs Q8_0) would invalidate any performance conclusions.&lt;/p&gt;

&lt;p&gt;Check the quant llama.cpp loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/v1/models | jq


&lt;span class="s2"&gt;"ftype"&lt;/span&gt;: &lt;span class="s2"&gt;"Q4_K - Medium"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the quant Ollama is using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:11434/api/show &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "ornith-1.5:9b"}'&lt;/span&gt; | jq &lt;span class="s1"&gt;'.details.quantization_level'&lt;/span&gt;


&lt;span class="s2"&gt;"Q4_K_M"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both runtimes are confirmed to be executing &lt;code&gt;Q4_K_M&lt;/code&gt;. Precision and model size are strictly identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Leveling the Playing Field with Flash Attention &amp;amp; Batch Tuning
&lt;/h3&gt;

&lt;p&gt;To give llama.cpp parity with Ollama’s runtime optimizations, we restart &lt;code&gt;llama-server&lt;/code&gt; with FlashAttention enabled and explicit batch sizes configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-server &lt;span class="nt"&gt;-hf&lt;/span&gt; ornith-ai/Ornith-1.5-9B-GGUF &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;-fa&lt;/span&gt; on &lt;span class="nt"&gt;-b&lt;/span&gt; 2048 &lt;span class="nt"&gt;-ub&lt;/span&gt; 512

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what these tuning flags configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-fa on&lt;/code&gt; (or &lt;code&gt;--flash-attn on&lt;/code&gt;): Enables FlashAttention kernels for Apple Silicon Metal. This significantly accelerates prompt evaluation (prefill) and reduces memory bandwidth overhead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-b 2048&lt;/code&gt; (&lt;code&gt;--batch-size&lt;/code&gt;): Sets the logical batch size for prompt evaluation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-ub 512&lt;/code&gt; (&lt;code&gt;--ubatch-size&lt;/code&gt;): Sets the physical micro-batch size dispatched to Metal compute passes, keeping the GPU pipelines fully saturated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Side-by-Side Benchmark on Longer Generation
&lt;/h3&gt;

&lt;p&gt;Both engines were given the same prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;"give a 25 line story"&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A longer generation produces several hundred tokens, giving a much more reliable measurement of steady-state generation speed than short one-liners.&lt;/p&gt;

&lt;p&gt;Here are the side-by-side results on the 32 GB MacBook Air:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Ollama (&lt;code&gt;ornith-1.5:9b&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;llama.cpp (&lt;code&gt;-fa on -b 2048 -ub 512&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt Eval Count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;17 tokens&lt;/td&gt;
&lt;td&gt;18 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt Eval Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;46.01 tokens/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48.68 tokens/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generation Count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;496 tokens&lt;/td&gt;
&lt;td&gt;436 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generation Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16.42 tokens/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.07 tokens/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30.58 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25.85 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With FlashAttention and batch tuning enabled, llama.cpp matches or slightly edges out Ollama across both prompt evaluation ( &lt;strong&gt;48.68 vs. 46.01 tokens/s&lt;/strong&gt; ) and token generation speed ( &lt;strong&gt;17.07 vs. 16.42 tokens/s&lt;/strong&gt; ).&lt;/p&gt;

&lt;h4&gt;
  
  
  Ollama Command &amp;amp; Stats
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run ornith-1.5:9b &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; give a 25 line story
Thinking...
...done thinking.

&lt;span class="o"&gt;[&lt;/span&gt;story output omitted]

total duration: 30.582007541s
load duration: 2.821208ms
prompt &lt;span class="nb"&gt;eval &lt;/span&gt;count: 17 token&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
prompt &lt;span class="nb"&gt;eval &lt;/span&gt;duration: 369.481ms
prompt &lt;span class="nb"&gt;eval &lt;/span&gt;rate: 46.01 tokens/s
&lt;span class="nb"&gt;eval &lt;/span&gt;count: 496 token&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;eval &lt;/span&gt;duration: 30.204706s
&lt;span class="nb"&gt;eval &lt;/span&gt;rate: 16.42 tokens/s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  llama.cpp Server Startup &amp;amp; Timing Logs
&lt;/h4&gt;

&lt;p&gt;Server startup and live slot execution log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;➜ ~ llama-server -hf ornith-ai/Ornith-1.5-9B-GGUF --port 8080 -ngl 99 -fa on -b 2048 -ub 512
0.00.729.018 I cmn common_param: common_params_print_info: verbosity = 3 (adjust with the `-lv N` CLI arg)
0.00.729.664 W srv llama_server: -----------------
0.00.729.667 W srv llama_server: CORS is set to allow all origins ('*') and no API key is set
0.00.729.668 W srv llama_server: this can be a security risk (cross-origin attacks)
0.00.729.668 W srv llama_server: more info: https://github.com/ggml-org/llama.cpp/pull/25655
0.00.729.669 W srv llama_server: -----------------
0.00.731.435 I srv load_model: loading model 'ornith-ai/Ornith-1.5-9B-GGUF'
0.01.177.317 W model has unused tensor blk.32.attn_norm.weight (size = 16384 bytes) -- ignoring
0.01.177.324 W model has unused tensor blk.32.post_attention_norm.weight (size = 16384 bytes) -- ignoring
0.01.177.329 W model has unused tensor blk.32.attn_q.weight (size = 18874368 bytes) -- ignoring
0.01.177.331 W model has unused tensor blk.32.attn_k.weight (size = 2359296 bytes) -- ignoring
0.01.177.333 W model has unused tensor blk.32.attn_v.weight (size = 3440640 bytes) -- ignoring
0.01.177.338 W model has unused tensor blk.32.attn_output.weight (size = 9437184 bytes) -- ignoring
0.01.177.340 W model has unused tensor blk.32.attn_q_norm.weight (size = 1024 bytes) -- ignoring
0.01.177.341 W model has unused tensor blk.32.attn_k_norm.weight (size = 1024 bytes) -- ignoring
0.01.177.343 W model has unused tensor blk.32.ffn_gate.weight (size = 28311552 bytes) -- ignoring
0.01.177.345 W model has unused tensor blk.32.ffn_down.weight (size = 41287680 bytes) -- ignoring
0.01.177.347 W model has unused tensor blk.32.ffn_up.weight (size = 28311552 bytes) -- ignoring
0.01.177.350 W model has unused tensor blk.32.nextn.eh_proj.weight (size = 18874368 bytes) -- ignoring
0.01.177.353 W model has unused tensor blk.32.nextn.enorm.weight (size = 16384 bytes) -- ignoring
0.01.177.354 W model has unused tensor blk.32.nextn.hnorm.weight (size = 16384 bytes) -- ignoring
0.01.177.359 W model has unused tensor blk.32.nextn.shared_head_norm.weight (size = 16384 bytes) -- ignoring
0.01.799.972 I cmn init: llama threadpool init, n_threads = 4
0.01.959.375 W load_hparams: Qwen-VL models require at minimum 1024 image tokens to function correctly on grounding tasks
0.01.959.377 W load_hparams: if you encounter problems with accuracy, try adding --image-min-tokens 1024
0.01.959.377 W load_hparams: more info: https://github.com/ggml-org/llama.cpp/issues/16842

0.02.172.437 I srv load_model: loaded multimodal model, '/Users/vishnuhd/.cache/huggingface/hub/models--ornith-ai--Ornith-1.5-9B-GGUF/snapshots/abdd624b12ebf020b767fff532ff44fe552b28c3/mmproj-Ornith-1.5-9B-BF16.gguf'
0.02.399.797 I srv load_model: initializing, n_slots = 4, n_ctx_slot = 262144, kv_unified = 'true'
0.02.402.615 I srv init: chat template supports preserving reasoning, consider enabling it via --reasoning-preserve
0.02.402.621 I srv llama_server: model loaded
0.02.402.623 I srv llama_server: listening on http://127.0.0.1:8080
0.02.402.623 W srv llama_server: NOTICE: server default port will be changed to :9931 in a future release
0.02.402.623 W srv llama_server: ref: https://github.com/ggml-org/llama.cpp/pull/26508

0.36.386.802 I slot get_availabl: id 3 | task -1 | selected slot by LRU, t_last = -1
0.36.386.826 I slot launch_slot_: id 3 | task 0 | processing task, is_child = 0
0.42.586.497 I slot print_timing: id 3 | task 0 | n_gen = 100, tg = 16.98 t/s, tg_3s = 17.15 t/s
0.45.633.605 I slot print_timing: id 3 | task 0 | n_gen = 152, tg = 17.01 t/s, tg_3s = 17.07 t/s
0.48.679.586 I slot print_timing: id 3 | task 0 | n_gen = 204, tg = 17.03 t/s, tg_3s = 17.07 t/s
0.51.718.658 I slot print_timing: id 3 | task 0 | n_gen = 256, tg = 17.04 t/s, tg_3s = 17.11 t/s
0.54.746.036 I slot print_timing: id 3 | task 0 | n_gen = 308, tg = 17.07 t/s, tg_3s = 17.18 t/s
0.57.778.672 I slot print_timing: id 3 | task 0 | n_gen = 360, tg = 17.08 t/s, tg_3s = 17.15 t/s
1.00.817.109 I slot print_timing: id 3 | task 0 | n_gen = 412, tg = 17.08 t/s, tg_3s = 17.11 t/s
1.02.233.148 I slot print_timing: id 3 | task 0 | prompt eval time = 369.74 ms / 18 tokens ( 20.54 ms per token, 48.68 tokens per second)
1.02.233.155 I slot print_timing: id 3 | task 0 | eval time = 25476.17 ms / 436 tokens ( 58.57 ms per token, 17.07 tokens per second)
1.02.233.157 I slot print_timing: id 3 | task 0 | total time = 25845.92 ms / 454 tokens
1.02.233.159 I slot print_timing: id 3 | task 0 | graphs reused = 434
1.02.233.187 I slot release: id 3 | task 0 | stop processing: n_tokens = 453, truncated = 0

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the raw curl request (with story output trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;curl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;http://localhost:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="err"&gt;/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/chat/completions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ornith-1.5-9b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Give me a 25 line story"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[story text omitted]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reasoning_content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[thinking omitted]"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1788423730&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ornith-ai/Ornith-1.5-9B-GGUF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"system_fingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b10712-daef7b687"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chat.completion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;436&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;454&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens_details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cached_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chatcmpl-dLoeYkd6rvuFieOvUSr9dBxB798twvZi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cache_n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;369.742&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_per_token_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;20.54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_per_second"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;48.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"predicted_n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;436&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"predicted_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;25476.174&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"predicted_per_token_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;58.57&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"predicted_per_second"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;17.07&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ollama is not faster than llama.cpp&lt;/strong&gt; : Under the hood, Ollama is llama.cpp. Its initial out-of-the-box advantage in prompt evaluation comes entirely from default runtime tuning (FlashAttention and batch sizes), not a secret runtime or different quantization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FlashAttention is essential on Apple Silicon&lt;/strong&gt; : Enabling &lt;code&gt;-fa on&lt;/code&gt; doubled prompt evaluation speed from &lt;strong&gt;24.18 tokens/s to 48.68 tokens/s&lt;/strong&gt; , drastically cutting time-to-first-token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent throughput&lt;/strong&gt; : At 9B parameters with Q4_K_M quantization, Apple Silicon unified memory sustains a rock-solid &lt;strong&gt;~17.1 tokens/sec&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why run llama.cpp directly?&lt;/strong&gt; : Ollama provides exceptional developer ergonomics for local apps and testing. But direct llama.cpp gives you full control over context allocation, slot limits, KV cache quantization (&lt;code&gt;-ctk&lt;/code&gt;, &lt;code&gt;-ctv&lt;/code&gt;), and immediate access to upstream features and bugfixes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the next post, we will explore KV cache quantization and memory profiling to see how far we can stretch long-context windows on a 32 GB machine.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>localllm</category>
      <category>llamacpp</category>
      <category>ollama</category>
    </item>
    <item>
      <title>Running Ollama on a 32 GB MacBook Air: A Practical First Setup</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Thu, 03 Sep 2026 01:30:00 +0000</pubDate>
      <link>https://dev.to/vishnuhd/running-ollama-on-a-32-gb-macbook-air-a-practical-first-setup-2bc0</link>
      <guid>https://dev.to/vishnuhd/running-ollama-on-a-32-gb-macbook-air-a-practical-first-setup-2bc0</guid>
      <description>&lt;p&gt;I have a 32 GB MacBook Air. It is not a workstation GPU box, but its unified memory makes it a surprisingly capable machine for local models, provided I choose models that fit and keep expectations sensible.&lt;/p&gt;

&lt;p&gt;This is the first post in a small, practical series about running models locally. I am starting with Ollama because it gets a model running quickly without building a runtime from source or hand-managing dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Ollama
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; manages model downloads, exposes a straightforward CLI, and starts a local HTTP API. On Apple silicon, it supports the Apple GPU; Ollama’s current macOS requirement is Sonoma (14) or newer. The app stores models and configuration under &lt;code&gt;~/.ollama&lt;/code&gt;. &lt;a href="https://docs.ollama.com/macos" rel="noopener noreferrer"&gt;The macOS documentation&lt;/a&gt; is the useful reference for install, storage, and logs.&lt;/p&gt;

&lt;p&gt;Under the hood, Ollama packages model management and an API around inference backends including &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;. That distinction is useful: Ollama is the convenient front door; llama.cpp is a lower-level route I can use later when I want to compare runtimes directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install Ollama
&lt;/h3&gt;

&lt;p&gt;Download the macOS app from &lt;a href="https://ollama.com/download" rel="noopener noreferrer"&gt;ollama.com/download&lt;/a&gt;, mount the DMG, and drag Ollama to &lt;code&gt;/Applications&lt;/code&gt;. Start it once. If the CLI is not already available, the app will offer to add it to your path.&lt;/p&gt;

&lt;p&gt;Confirm that both the CLI and the local server are available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama &lt;span class="nt"&gt;--version&lt;/span&gt;
ollama list
ollama ps

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a new installation, &lt;code&gt;ollama list&lt;/code&gt; and &lt;code&gt;ollama ps&lt;/code&gt; should be empty. The first reports downloaded models; the second reports models currently loaded into memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Pull a Model
&lt;/h3&gt;

&lt;p&gt;For this machine, I started with Ornith 1.5 9B:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull ornith-1.5:9b

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels deliberately familiar if you work with containers: &lt;code&gt;ollama pull&lt;/code&gt; downloads the model and its layers, while &lt;code&gt;ollama run&lt;/code&gt; starts an interactive session. The current Ollama build of &lt;code&gt;ornith-1.5:9b&lt;/code&gt; is 6.6 GB with a 256K context window, which is a comfortable starting point on a 32 GB laptop. &lt;a href="https://ollama.com/library/ornith-1.5" rel="noopener noreferrer"&gt;Ollama’s model page&lt;/a&gt; lists the available tags; the 35B download is 23 GB, so I would not make that the default on an Air.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqj1zv6qp2ef0qtdjzb9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqj1zv6qp2ef0qtdjzb9.png" alt="Pulling Ornith 1.5 9B, then confirming the local model" width="800" height="204"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Pulling Ornith 1.5 9B, then confirming it is available locally.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After the pull completes, confirm it is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama list

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Run It Interactively
&lt;/h3&gt;

&lt;p&gt;Start a chat session with the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run ornith-1.5:9b

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a question that resembles the work you actually do. I tested a simple greeting first, then moved on to infrastructure questions. Exit the interactive prompt with &lt;code&gt;/exit&lt;/code&gt; or &lt;code&gt;Ctrl-D&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu2w838wlqel8rgnlhxfl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu2w838wlqel8rgnlhxfl.png" alt="An interactive Ornith 1.5 9B session in the terminal" width="799" height="172"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;A first interactive conversation with the locally running model.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For an initial sanity check, the model was responsive and produced a natural answer. That is useful confirmation that the model loads and runs locally, but it is not a benchmark. A real comparison needs the same prompt, context length, generation settings, and output length.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Inspect Performance with &lt;code&gt;--verbose&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Ollama’s &lt;code&gt;--verbose&lt;/code&gt; flag is a quick way to see timings after every response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run ornith-1.5:9b &lt;span class="nt"&gt;--verbose&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On this MacBook Air, my short greeting test produced 114 output tokens at &lt;strong&gt;16.68 tokens/sec&lt;/strong&gt; , with a total duration of &lt;strong&gt;7.22 seconds&lt;/strong&gt;. The model’s thinking trace was visible before its answer.&lt;/p&gt;

&lt;p&gt;I ran the same kind of test with Gemma 4 E4B:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run gemma4:e4b &lt;span class="nt"&gt;--verbose&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That run produced 228 output tokens at &lt;strong&gt;27.62 tokens/sec&lt;/strong&gt; , with a total duration of &lt;strong&gt;8.49 seconds&lt;/strong&gt;. It is faster in this small test, but it also generated a different and longer response. These figures are useful as a personal baseline, not as an apples-to-apples model ranking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ornith 1.5 9B vs. Gemma 4 E4B
&lt;/h3&gt;

&lt;p&gt;Both models fit well on a 32 GB MacBook Air, but they are aimed at slightly different trade-offs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What I observed&lt;/th&gt;
&lt;th&gt;Practical fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ornith-1.5:9b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6.6 GB download; 16.68 tokens/sec in my short verbose run&lt;/td&gt;
&lt;td&gt;A capable 9B-class, text-and-image model with plenty of memory headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma4:e4b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9.6 GB download; 27.62 tokens/sec in my different short verbose run&lt;/td&gt;
&lt;td&gt;An efficient edge model for local chat, reasoning, coding, and multimodal work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;E&lt;/code&gt; in Gemma 4 E4B means &lt;strong&gt;effective&lt;/strong&gt; parameters. Ollama describes E4B as a 4.5B-effective-parameter edge model (8B including embeddings), with a 128K context window and text, image, and audio support. It is designed to do useful local work without the memory cost of the larger Gemma 4 workstation models. &lt;a href="https://ollama.com/library/gemma4" rel="noopener noreferrer"&gt;The Gemma 4 library page&lt;/a&gt; has the current tags, sizes, and capabilities.&lt;/p&gt;

&lt;p&gt;In practice, I would start with &lt;code&gt;ornith-1.5:9b&lt;/code&gt; if I want the smaller download and a roomy 256K context window, or &lt;code&gt;gemma4:e4b&lt;/code&gt; if I want the efficient Gemma 4 feature set. Neither of these two quick runs says which model is universally better; use the prompts you care about and record the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Call the Local API
&lt;/h3&gt;

&lt;p&gt;The terminal chat is only the first test. Ollama exposes an API locally at &lt;code&gt;http://localhost:11434/api&lt;/code&gt;, so the model can be part of a script or an application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;curl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;http://localhost:&lt;/span&gt;&lt;span class="mi"&gt;11434&lt;/span&gt;&lt;span class="err"&gt;/api/chat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ornith-1.5:9b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello!"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fye0n6w8m0c0qv4e2j0yi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fye0n6w8m0c0qv4e2j0yi.png" alt="Calling the local Ollama chat API with curl" width="799" height="136"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Calling the same model through Ollama’s local chat API.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The response includes the answer and useful timing fields. Keep this endpoint local by default. If I later expose it to another device, I will put authentication and a proper reverse proxy in front of it. I will not publish port 11434 directly. &lt;a href="https://docs.ollama.com/api/introduction" rel="noopener noreferrer"&gt;Ollama’s API documentation&lt;/a&gt; covers the local base URL and client libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Try the App UI
&lt;/h3&gt;

&lt;p&gt;The CLI is great for testing and scripts, but the Ollama app also gives me a simple chat interface. Here, Ornith is selected in the model picker and used for a weather question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5gizfz91sgs3ur8kfz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5gizfz91sgs3ur8kfz0.png" alt="The Ollama app with Ornith 1.5 9B selected" width="800" height="503"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;The Ollama app with Ornith 1.5 9B selected for a web-assisted prompt.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The app can also give a model access to web tools. In this example, Ornith searched for current Singapore weather before answering. That extends the model with fresh online information when the tool is enabled, but it does not replace the model’s built-in training knowledge. Treat retrieved results as sources to verify, especially for technical or time-sensitive answers.&lt;/p&gt;

&lt;p&gt;The UI is useful when I want to compare prompts casually. The local API is the path I will use when I want to integrate models into tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commands Worth Remembering
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download without entering an interactive chat&lt;/span&gt;
ollama pull ornith-1.5:9b

&lt;span class="c"&gt;# Start an interactive chat&lt;/span&gt;
ollama run ornith-1.5:9b

&lt;span class="c"&gt;# Show downloaded models and their disk usage&lt;/span&gt;
ollama list

&lt;span class="c"&gt;# Show models currently loaded by the runner&lt;/span&gt;
ollama ps

&lt;span class="c"&gt;# Remove a model I no longer need&lt;/span&gt;
ollama &lt;span class="nb"&gt;rm &lt;/span&gt;ornith-1.5:9b

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Models are not small. Treat &lt;code&gt;ollama pull&lt;/code&gt; the same way you would a sizeable &lt;code&gt;docker pull&lt;/code&gt;: check disk space before collecting a pile of models “just in case.”&lt;/p&gt;

&lt;h3&gt;
  
  
  What This MacBook Air Is Good At
&lt;/h3&gt;

&lt;p&gt;An 8B to 9B class model is a good fit for private note summarisation, explaining logs, drafting YAML, lightweight coding help, and experimenting with local integrations. This is not where I expect a 70B-class model to be effortless or where I would host production inference.&lt;/p&gt;

&lt;p&gt;There are many ways to run models locally, but Ollama is an excellent Apple-silicon starting point. It gets the plumbing out of the way so I can spend time evaluating the models themselves.&lt;/p&gt;

&lt;p&gt;Next, I will run llama.cpp directly on the same machine, then see whether a vLLM setup is worth comparing. Stay tuned.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>localllm</category>
      <category>ollama</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>Kubernetes v1.37: An Operator's Look at Garhwal</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Wed, 02 Sep 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/vishnuhd/kubernetes-v137-an-operators-look-at-garhwal-a53</link>
      <guid>https://dev.to/vishnuhd/kubernetes-v137-an-operators-look-at-garhwal-a53</guid>
      <description>&lt;p&gt;Kubernetes v1.37 landed recently under the release theme &lt;strong&gt;Garhwal&lt;/strong&gt; , named after the Himalayan region of Uttarakhand, India. The release packages 67 enhancements across alpha, beta, and stable.&lt;/p&gt;

&lt;p&gt;The upstream changelog is massive, but as someone who spends most of the day managing control planes, dealing with storage, and watching cloud bills, only a handful of changes stand out as immediately relevant to day-to-day work.&lt;/p&gt;

&lt;p&gt;Here are the features I am keeping an eye on, along with practical examples for how they work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. HPA Can Finally Scale to Zero (&lt;code&gt;minReplicas: 0&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;For years, one of the most frustrating limitations in vanilla Kubernetes autoscaling was that &lt;code&gt;HorizontalPodAutoscaler&lt;/code&gt; could not scale below one replica. If you had a queue consumer waiting on messages in SQS or RabbitMQ, or a GPU worker waiting on batch jobs, you had to keep at least one pod running around the clock, or pull in third-party tools like KEDA just to do basic scale-to-zero.&lt;/p&gt;

&lt;p&gt;In v1.37, &lt;strong&gt;HPA scale to zero&lt;/strong&gt; graduated to Beta and is enabled by default. You can now configure &lt;code&gt;minReplicas: 0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HorizontalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;queue-worker-hpa&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;queue-worker&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;External&lt;/span&gt;
      &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws_sqs_approximate_number_of_messages_visible&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Value&lt;/span&gt;
          &lt;span class="na"&gt;averageValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the queue is empty, the controller scales the deployment down to 0 pods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get hpa queue-worker-hpa
&lt;span class="c"&gt;# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE&lt;/span&gt;
&lt;span class="c"&gt;# queue-worker-hpa Deployment/queue-worker 0/30 (avg) 0 10 0 18m&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, the HPA controller adds a &lt;code&gt;ScaledToZero&lt;/code&gt; condition so you can easily distinguish an automated scale-down from a manual shutdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;currentReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;desiredReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ScaledToZero&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;True"&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ScaledToZero&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pods&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scaled&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;down&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;based&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;metrics"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; This only works with object or external metrics. It does not work with CPU or memory metrics, because a deployment with zero running pods produces no CPU or memory data to trigger an upscale. For event-driven workers and GPU workloads, this saves real money on cloud bills.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Finding Orphaned PVCs Just Got Much Easier
&lt;/h3&gt;

&lt;p&gt;PersistentVolumeClaims have a habit of outliving the workloads that created them. Someone deletes a Deployment or StatefulSet, but the PVC stays behind in the namespace. Weeks or months later, dozens of unused EBS volumes or cloud disks are quietly burning infrastructure budget.&lt;/p&gt;

&lt;p&gt;Until now, Kubernetes gave you no native way to know when a PVC was last used without writing custom scripts to inspect every pod spec in the cluster.&lt;/p&gt;

&lt;p&gt;Kubernetes v1.37 addresses this by graduating &lt;strong&gt;PVC last-used tracking&lt;/strong&gt; to Beta (enabled by default). The PVC protection controller now adds an &lt;code&gt;Unused&lt;/code&gt; condition to &lt;code&gt;status.conditions&lt;/code&gt; on PersistentVolumeClaims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;abandoned-redis-data&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bound&lt;/span&gt;
  &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unused&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;True"&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NoPodsUsingPVC&lt;/span&gt;
      &lt;span class="na"&gt;lastTransitionTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-08-15T09:30:00Z"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;When the last pod referencing a PVC terminates, &lt;code&gt;Status&lt;/code&gt; flips to &lt;code&gt;"True"&lt;/code&gt; with reason &lt;code&gt;NoPodsUsingPVC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;lastTransitionTime&lt;/code&gt; tells you the exact timestamp since the volume became idle.&lt;/li&gt;
&lt;li&gt;As soon as a new pod attaches, the condition flips back to &lt;code&gt;False&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes does not delete anything automatically (which is good; you do not want the control plane guessing whether data is safe to purge). But this gives you a clean field to query across your cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all PVCs across all namespaces that are currently unused, with their idle timestamp&lt;/span&gt;
kubectl get pvc &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'NAMESPACE:.metadata.namespace,\
NAME:.metadata.name,\
CAPACITY:.status.capacity.storage,\
UNUSED:.status.conditions[?(@.type=="Unused")].status,\
IDLE_SINCE:.status.conditions[?(@.type=="Unused")].lastTransitionTime'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With one command, you can spot storage that has been abandoned for 30+ days and clean it up safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. ClusterTrustBundles Go GA: Retire Your CA Sync Cronjobs
&lt;/h3&gt;

&lt;p&gt;If you manage enterprise clusters with internal PKI, private container registries, or corporate proxies, you have almost certainly solved the root certificate problem with a workaround. Most teams either run a custom controller that clones a secret into every namespace as a &lt;code&gt;ConfigMap&lt;/code&gt;, or bake internal certificates into container base images.&lt;/p&gt;

&lt;p&gt;In v1.37, &lt;strong&gt;ClusterTrustBundles&lt;/strong&gt; and &lt;strong&gt;Pod Certificates&lt;/strong&gt; officially graduated to Stable (GA) under &lt;code&gt;certificates.k8s.io&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;ClusterTrustBundle&lt;/code&gt; is a cluster-scoped object that holds trusted X.509 certificate anchors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;certificates.k8s.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterTrustBundle&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;corporate-root-ca&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;signerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;corp.internal/pki"&lt;/span&gt;
  &lt;span class="na"&gt;trustBundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;-----BEGIN CERTIFICATE-----&lt;/span&gt;
    &lt;span class="s"&gt;MIICjTCCAjSgAwIBAgIUTkX...&lt;/span&gt;
    &lt;span class="s"&gt;-----END CERTIFICATE-----&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workloads in any namespace can project this bundle directly into their filesystem via standard projected volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-registry.internal/api:v1&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Point common TLS libraries to the projected CA bundle&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SSL_CERT_FILE&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssl/certs/corporate-root-ca.crt&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trusted-certs&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssl/certs/corporate-root-ca.crt&lt;/span&gt;
              &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;corporate-root-ca.crt&lt;/span&gt;
              &lt;span class="na"&gt;readOnly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trusted-certs&lt;/span&gt;
          &lt;span class="na"&gt;projected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;clusterTrustBundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;corporate-root-ca&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kubelet handles mounting and rotation natively. That is one less custom controller or sync cronjob to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Control Plane Breathing Room: etcd RangeStream &amp;amp; Concurrent Decode
&lt;/h3&gt;

&lt;p&gt;When large clusters experience network hiccups or controller restarts, hundreds of controllers can hit the API server with list requests at the exact same moment. Historically, etcd built full responses in memory before sending them, and the API server processed watch events serially on a single goroutine. During recovery storms, control plane memory would spike, occasionally sending nodes into Out-Of-Memory (OOM) loops.&lt;/p&gt;

&lt;p&gt;In v1.37, two features land on by default to fix this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;etcd RangeStream (Beta):&lt;/strong&gt; Instead of buffering massive key-value sets in memory, etcd streams responses back in chunks. This turns what used to be a memory-heavy dump into a smooth stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent Watch Object Decode (Beta):&lt;/strong&gt; The API server now decodes incoming watch events across a pool of worker goroutines rather than processing them one by one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you interact with etcd 3.7+ directly, you can test the new chunked streaming RPC via &lt;code&gt;etcdctl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stream keys in chunks rather than buffering everything into a single gRPC response&lt;/span&gt;
etcdctl get /registry/pods/ &lt;span class="nt"&gt;--prefix&lt;/span&gt; &lt;span class="nt"&gt;--stream&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In upstream benchmarks across 150,000 pods, these two improvements combined cut cache initialization time by more than 50%. For operators of large clusters or CRD-heavy environments, this means significantly less control plane jitter during rolling restarts.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;code&gt;kubectl get -o kyaml&lt;/code&gt; Goes Stable
&lt;/h3&gt;

&lt;p&gt;YAML has plenty of parsing quirks (such as unquoted country codes like &lt;code&gt;NO&lt;/code&gt; being interpreted as boolean &lt;code&gt;false&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;In v1.37, &lt;strong&gt;KYAML&lt;/strong&gt; reaches Stable. KYAML is a safer, unambiguous subset of YAML tailored specifically for Kubernetes. You do not need to change any of your existing manifests or pipelines; KYAML is strictly backwards compatible with standard YAML.&lt;/p&gt;

&lt;p&gt;You can now run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Output clean, normalized YAML without parser ambiguity&lt;/span&gt;
kubectl get deployment queue-worker &lt;span class="nt"&gt;-o&lt;/span&gt; kyaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparing the output against regular YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Standard YAML output often strips quotes from country codes or booleans:&lt;/span&gt;
&lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NO&lt;/span&gt; &lt;span class="c1"&gt;# Parsed by some YAML 1.1 loaders as boolean false!&lt;/span&gt;

&lt;span class="c1"&gt;# KYAML output strictly normalizes and quotes scalar types:&lt;/span&gt;
&lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NO"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It formats cleanly, avoids parser edge cases, and provides predictable output for gitops diffs and scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. SELinuxMount Reaches GA
&lt;/h3&gt;

&lt;p&gt;If your nodes run SELinux (common in Red Hat, Rocky, or Fedora environments), volume mounts historically required recursive file relabeling on every pod start, which could cause painfully slow startup times on large volumes.&lt;/p&gt;

&lt;p&gt;In v1.37, &lt;strong&gt;SELinuxMount&lt;/strong&gt; is now Stable and enabled by default for CSI drivers that support it. Volumes are mounted with a single mount context rather than recursively walking the directory tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; A volume mount can only carry one SELinux context. If you run multiple pods with different SELinux labels that share the same volume on the same node, those pods may now fail to mount.&lt;/p&gt;

&lt;p&gt;If your workloads rely on shared volumes with mixed labels, you can preserve the legacy recursive relabeling behavior in your Pod spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-data-consumer&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;seLinuxChangePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Recursive&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine&lt;/span&gt;
      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-storage&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-storage&lt;/span&gt;
      &lt;span class="na"&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;claimName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-data-pvc&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Where This Leaves Us
&lt;/h3&gt;

&lt;p&gt;Do not rush to upgrade production this weekend. If you use a managed provider (EKS, GKE, Rancher), wait for &lt;code&gt;.1&lt;/code&gt; or &lt;code&gt;.2&lt;/code&gt; patch releases. If you run physical data centers like me and manage the control plane yourself, be even more conservative: test locally, promote through lower environments, and comfortably stay on n-1 or n-2.&lt;/p&gt;

&lt;p&gt;The real reason to track v1.37 today is preventing fresh technical debt. If your team was about to build custom controllers to sync CA certs or write scripts to hunt dead storage, tell them to hold off. Upstream is solving it natively.&lt;/p&gt;

&lt;p&gt;Let the early patches bake, keep your clusters healthy, and start planning which legacy workarounds you get to delete.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cloudnative</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Multi-region YugabyteDB deployment on AWS EKS with Istio</title>
      <dc:creator>Vishnu Hari Dadhich</dc:creator>
      <pubDate>Thu, 02 May 2024 06:34:52 +0000</pubDate>
      <link>https://dev.to/vishnuhd/multi-region-yugabytedb-deployment-on-aws-eks-with-istio-2ng5</link>
      <guid>https://dev.to/vishnuhd/multi-region-yugabytedb-deployment-on-aws-eks-with-istio-2ng5</guid>
      <description>&lt;p&gt;In today’s distributed cloud landscape, deploying applications across multiple regions and clusters is crucial for scalability, reliability, and performance. This blog post will guide you through setting up a multi-region, multi-cluster YugabyteDB deployment on AWS EKS with Istio service mesh.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHY YUGABYTEDB?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.yugabyte.com/" rel="noopener noreferrer"&gt;YugabyteDB&lt;/a&gt; is a transactional database that brings together four must-have needs of cloud native apps – namely SQL as a flexible query language, low-latency performance, continuous availability, and globally-distributed scalability. Other databases do not serve all 4 of these needs simultaneously.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monolithic SQL databases offer SQL and low-latency reads, but neither have the ability to tolerate failures, nor can they scale writes across multiple nodes, zones, regions, and clouds.&lt;/li&gt;
&lt;li&gt;Distributed NoSQL databases offer read performance, high availability, and write scalability, but give up on SQL features such as relational data modelling and ACID transactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  WHY AWS EKS AND ISTIO?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/eks/" rel="noopener noreferrer"&gt;AWS EKS&lt;/a&gt; provides a managed Kubernetes service, simplifying cluster management and deployment. &lt;a href="https://istio.io/" rel="noopener noreferrer"&gt;Istio&lt;/a&gt;, an open-source service mesh, enables traffic management, security, and observability across microservices.&lt;/p&gt;

&lt;p&gt;Combining Yugabyte with AWS EKS and Istio creates a robust, scalable, and secure cloud-native architecture that spans across multiple regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  DEPLOYMENT OVERVIEW
&lt;/h2&gt;

&lt;p&gt;Our deployment consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three AWS regions (Singapore, Mumbai and Hyderabad).&lt;/li&gt;
&lt;li&gt;One EKS cluster in each region.&lt;/li&gt;
&lt;li&gt;One YugabyteDB master and one YugabyteDB tserver are deployed in each EKS cluster.&lt;/li&gt;
&lt;li&gt;Istio is deployed in each cluster with an east-west gateway to provide a multi-cluster service mesh.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk54aoeavtasdf0u4myag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk54aoeavtasdf0u4myag.png" alt=" " width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  DEPLOYMENT STEPS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pre-requisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AWS account with at least three regions enabled&lt;/li&gt;
&lt;li&gt;AWS user with access to create VPC and EKS using eksctl
eksctl, aws cli, kubectl and git installed on your local system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clone the following repo to follow along with this blog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/vishnuhd/yugabyte-multiregion-aws-eks-istio.git
cd yugabyte-multiregion-aws-eks-istio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy AWS EKS clusters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Deploy EKS clusters in three different regions (namely Singapore, Mumbai and Hyderabad) using eksctl:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Creating EKS cluster in ${region}...\n"
    eksctl create cluster -f ${region}/cluster-config.yaml
    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Rename the kube contexts for the simplicity of this demo:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl config rename-context 'yb-mumbai.ap-south-1.eksctl.io' mumbai
kubectl config rename-context 'yb-singapore.ap-southeast-1.eksctl.io' singapore
kubectl config rename-context 'yb-hyderabad.ap-south-2.eksctl.io' hyderabad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; By default, EKS does not provide EBS permissions, follow this article to enable EKS PVC dynamic provisioning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup Istio
&lt;/h3&gt;

&lt;p&gt;When configuring a production deployment of Istio, key considerations include whether the mesh will be in single or multiple clusters, Istio control plane setup for high availability, and the choice between a single multicluster service mesh or federated multi-mesh deployment. These factors represent independent dimensions of configuration for Istio deployment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://istio.io/latest/docs/ops/deployment/deployment-models/" rel="noopener noreferrer"&gt;This&lt;/a&gt; guide describes the various options and considerations when configuring your Istio deployment. For this demo, we are gonna &lt;a href="https://istio.io/latest/docs/setup/install/multicluster/multi-primary_multi-network/" rel="noopener noreferrer"&gt;Install Multi-Primary on different networks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Download Istio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -L https://istio.io/downloadIstio | sh -
cd istio-1.21.0
export PATH=$PWD/bin:$PATH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Plug in CA Certificates for Istio
&lt;/h3&gt;

&lt;p&gt;In a multi-cluster environment, we would want to set up one root CA and use the root CA to issue intermediate certificates to the Istio CAs that run in each cluster. This would ensure the services behind it can only be accessed by services with a trusted mTLS certificate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a cert directory :
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p istio-1.21.0/certs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Generate the root CA certificate and key:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd istio-1.21.0/certs
make -f ../tools/certs/Makefile.selfsigned.mk root-ca
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For each cluster, generate an intermediate certificate and key for the Istio CA:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd istio-1.21.0/certs

{
  for region in mumbai singapore hyderabad; do
    echo -e "Generating certs for cluster - ${region}...\n"
    make -f ../tools/certs/Makefile.selfsigned.mk yb-${region}-cacerts
    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In each cluster, create a secret called &lt;code&gt;cacerts&lt;/code&gt; including all the input files &lt;code&gt;ca-cert.pem&lt;/code&gt;, &lt;code&gt;ca-key.pem&lt;/code&gt;, &lt;code&gt;root-cert.pem&lt;/code&gt; and &lt;code&gt;cert-chain.pem&lt;/code&gt; :
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Creating namespace and secret for cluster - ${region}...\n"

    kubectl --context ${region} create namespace istio-system
    kubectl --context ${region} create secret generic cacerts -n istio-system \
          --from-file=istio-1.21.0/certs/yb-${region}/ca-cert.pem \
          --from-file=istio-1.21.0/certs/yb-${region}/ca-key.pem \
          --from-file=istio-1.21.0/certs/yb-${region}/root-cert.pem \
          --from-file=istio-1.21.0/certs/yb-${region}/cert-chain.pem

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this step completed, we are now prepared to install Istio on every cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Istio
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Install Istio using istioctl:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Installing istio for cluster - ${region}...\n"

    istioctl install --context ${region} -f ./${region}/istio.yaml

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Install a gateway in each cluster, that is dedicated to &lt;a href="https://en.wikipedia.org/wiki/East-west_traffic" rel="noopener noreferrer"&gt;east-west&lt;/a&gt; traffic. By default, this gateway will be public on the Internet. Production systems may require additional access restrictions (e.g. via firewall rules) to prevent external attacks.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Installing the east-west gateway for cluster - ${region}...\n"

    ./istio-1.21.0/samples/multicluster/gen-eastwest-gateway.sh \
        --mesh mesh1 --cluster yb-${region} --network network-${region} | \
        istioctl --context ${region} install -y -f -

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Since the clusters are on separate networks, we need to expose all services (&lt;code&gt;*.local&lt;/code&gt;) on the east-west gateway in all three clusters. While this gateway is public on the Internet, services behind it can only be accessed by services with a trusted mTLS certificate and workload ID, just as if they were on the same network.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Exposing the services for cluster - ${region}...\n"

    kubectl --context ${region} apply -n istio-system -f \
        ./istio-1.21.0/samples/multicluster/expose-services.yaml

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Install a remote secret in each cluster that provides access to the other cluster’s Kube API server.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region1 in mumbai singapore hyderabad; do
    for region2 in mumbai singapore hyderabad; do
      if [[ "${region1}" == "${region2}" ]]; then continue; fi
      echo -e "Create remote secret of ${region1} in ${region2}...\n"

      istioctl create-remote-secret \
        --context ${region1} \
        --name=yb-${region1} | \
        kubectl apply -f - --context ${region2}

      echo -e "-------------\n"
    done
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install YugabyteDB
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;To install YugabyteDB using helm charts, add the chart repo:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm repo add yugabytedb https://charts.yugabyte.com
helm repo update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a YugabyteDB namespace (&lt;code&gt;yb-demo&lt;/code&gt;) in each cluster and enable Istio injection:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Creating namespace for cluster - ${region}...\n"

    kubectl --context ${region} create namespace yb-demo
    kubectl label --context ${region} namespace yb-demo istio-injection=enabled

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Install YugabyteDB:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Installing YugabyteDB in cluster - ${region}...\n"

    helm upgrade --install ${region} yugabytedb/yugabyte \
        --version 2.19.3 \
        --namespace yb-demo \
        -f ${region}/overrides.yaml \
        --kube-context ${region} --wait

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will install YugabyteDB in each EKS cluster with 1 master and 1 tserver connected with each other through the Istio service mesh. At this point, it is important to understand each parameter being set in the &lt;a href="https://github.com/vishnuhd/yugabyte-multiregion-aws-eks-istio/blob/main/singapore/overrides.yaml" rel="noopener noreferrer"&gt;overrides.yaml&lt;/a&gt; file. Each master and tserver pod needs to know all the master addresses to replicate data and elect leaders.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In addition to the Istio setup, an extra step is required, which involves creating identical Kubernetes services in all clusters to enable DNS service discovery. More information can be found &lt;a href="https://istio.io/latest/docs/ops/deployment/deployment-models/#dns-with-multiple-clusters" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Therefore, we need to replicate the yugabyte-master and yugabyte-tserver services present in the Mumbai region to both the Singapore and Hyderabad regions, and vice versa.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region1 in mumbai singapore hyderabad; do
    for region2 in mumbai singapore hyderabad; do
      if [[ "${region1}" == "${region2}" ]]; then continue; fi
      echo -e "Creating services of ${region2} in ${region1}...\n"

      kubectl --context ${region1} apply -f ${region1}/services-${region2}.yaml -n yb-demo

      echo -e "-------------\n"
    done
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Check the YugabyteDB pods and services, all of them should be up and running:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Checking YugabyteDB pods and svcs for cluster - ${region}...\n"

    kubectl --context ${region} get pods,svc -A

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Finally, we need to configure global data distribution, for Yugabyte to handle the data distribution properly across regions:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl --context mumbai exec -n yb-demo mumbai-yugabyte-yb-master-0 -- bash \
-c "/home/yugabyte/master/bin/yb-admin --master_addresses mumbai-yugabyte-yb-master-0.yb-demo.svc.cluster.local,hyderabad-yugabyte-yb-master-0.yb-demo.svc.cluster.local,singapore-yugabyte-yb-master-0.yb-demo.svc.cluster.local modify_placement_info aws.ap-south-1.ap-south-1a,aws.ap-south-2.ap-south-2a,aws.ap-southeast-1.ap-southeast-1a 3"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Voila, your YugabyteDB multi-regional setup is now complete!&lt;/p&gt;

&lt;h2&gt;
  
  
  Access the YugabyteDB UI
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Find the yb-master-ui service in yb-demo namespace for any cluster and open it in the browser along with the port 7000:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcq5k6du2a13ov8i0qokk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcq5k6du2a13ov8i0qokk.png" alt=" " width="799" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we can see the masters are spread across regions, with Hyderabad one as the leader.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explore the tablet servers:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6dniin0za79c9iwz469w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6dniin0za79c9iwz469w.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similarly, we can see the tablet servers being distributed across multiple regions, each of them able to hand synchronous reads and writes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run a sample Yugabyte application in any of the clusters:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl run yb-sample-apps \
    -it --rm \
    --image yugabytedb/yb-sample-apps \
    --namespace yb-demo \
    --context singapore \
    --command -- sh

java -jar yb-sample-apps.jar java-client-sql \
    --workload SqlInserts \
    --nodes yb-tserver-common.yb-demo.svc.cluster.local:5433 \
    --num_threads_write 1 \
    --num_threads_read 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we are targeting yb-tserver-common service for reads and writes, which will choose any of the tserver in any of the regions randomly. This also helps in load-balancing the traffic across regions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We can also see the tables created by this sample app:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjvq2q4b1z8pqxcbuuu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjvq2q4b1z8pqxcbuuu7.png" alt=" " width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  DISASTER RECOVERY
&lt;/h2&gt;

&lt;p&gt;Disasters can happen anytime, YugabyteDB provides us with a feature called Replication Factor (RF). Configurations usually include a Replication Factor (RF) of 3. In this setup, a write to the leader requires an acknowledgement from one follower before being committed, as the leader and one follower together constitute the majority. In the event of a failure, operational replicas in the Raft group can support consistent reads and writes, while those that have become separated from the Raft consensus quorum cannot make progress. We will now see this feature of Yugabyte in action.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current leaders for master and tservers:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycv2kzitwrb3zx3jijwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycv2kzitwrb3zx3jijwj.png" alt=" " width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghhd1jlktxwcwnkhfgw6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghhd1jlktxwcwnkhfgw6.png" alt=" " width="720" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently, the master pod in the Hyderabad region is the LEADER, while most of the transactions are handled by the tablet server in the Mumbai region.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Let’s make the Hyderabad region go down:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl scale sts hyderabad-yugabyte-yb-master-0 --replicas 0 -n yb-demo --context hyderabad
kubectl scale sts hyderabad-yugabyte-yb-tserver-0 --replicas 0 -n yb-demo --context hyderabad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As soon as we decrease the pod replicas to 0 for both master and tserver, we can see the errors in the Yugabyte master UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcgwiz5wu0pnhi2aftzsy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcgwiz5wu0pnhi2aftzsy.png" alt=" " width="720" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tlrki89ovvb2mr6eh9x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tlrki89ovvb2mr6eh9x.png" alt=" " width="720" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we can see that the master server from the Mumbai region is elected as a new Leader and all transactions are still operating even if one whole region is down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yugabyte also keeps track of the under-replicated tables, so that they can be replicated to the region as soon as it comes back online:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95zwq5jg9ygaezmr218h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95zwq5jg9ygaezmr218h.png" alt=" " width="720" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the region comes online again, the data is replicated back to the Hyderabad region:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3vxn3jjrhbwg6si6ot92.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3vxn3jjrhbwg6si6ot92.png" alt=" " width="720" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxgge9hlllzsv94vkqs9j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxgge9hlllzsv94vkqs9j.png" alt=" " width="720" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywcigexaok8uzcyo94j0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywcigexaok8uzcyo94j0.png" alt=" " width="720" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Awesome, now you have a multi-regional fault-tolerant YugabyteDB setup on AWS EKS clusters using Istio as a service mesh.&lt;/p&gt;

&lt;h2&gt;
  
  
  BONUS: SETUP KIALI FOR OBSERVABILITY
&lt;/h2&gt;

&lt;p&gt;The Istio download package comes by default with Kiali and Prometheus, let’s set it up to have a nice view of all the services in the mesh.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install Kiali and Prometheus:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Checking Kiali and Prometheus for cluster - ${region}...\n"

    kubectl apply -f istio-1.21.0/samples/addons/prometheus.yaml --context ${region}
    kubectl apply -f istio-1.21.0/samples/addons/kiali.yaml --context ${region}

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Open Kiali dashboard:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;istioctl dashboard kiali --context singapore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh05al0h097vqpocfxi7t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh05al0h097vqpocfxi7t.png" alt=" " width="720" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The above Kiali graph shows the various clusters and services from the POV of EKS cluster in the Singapore region.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLEANING UP THE RESOURCES
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Uninstall YugabyteDB:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Un-installing YugabyteDB in cluster - ${region}...\n"

    helm uninstall ${region} --namespace yb-demo --kube-context ${region}
    kubectl delete pvc --namespace yb-demo \
      --selector component=yugabytedb,release=${region} \
      --context ${region}

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Delete additional YB services:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region1 in mumbai singapore hyderabad; do
    for region2 in mumbai singapore hyderabad; do
      if [[ "${region1}" == "${region2}" ]]; then continue; fi
      echo -e "Deleting services of ${region2} in ${region1}...\n"

      kubectl --context ${region1} delete -f ${region1}/services-${region2}.yaml -n yb-demo

      echo -e "-------------\n"
    done
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Un-install Kiali and Prometheus:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Checking Kiali and Prometheus for cluster - ${region}...\n"

    kubectl delete -f istio-1.21.0/samples/addons/prometheus.yaml --context ${region}
    kubectl delete -f istio-1.21.0/samples/addons/kiali.yaml --context ${region}

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Uninstall Istio:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  for region in mumbai singapore hyderabad; do
    echo -e "Un-installing Istio in cluster - ${region}...\n"

    istioctl uninstall --purge -y --context ${region}

    echo -e "-------------\n"
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Delete EKS clusters:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eksctl delete cluster yb-mumbai --region ap-south-1
eksctl delete cluster yb-singapore --region ap-southeast-1
eksctl delete cluster yb-hyderabad --region ap-south-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CONCLUSION
&lt;/h2&gt;

&lt;p&gt;A multi-region, multi-cluster YugabyteDB deployment on AWS EKS with Istio provides a highly available, scalable, and secure architecture for distributed applications. By leveraging YugabyteDB deployed on multiple AWS regions and EKS clusters, this setup ensures redundancy and failover capabilities, minimizing downtime and ensuring business continuity. Istio’s service mesh capabilities provide advanced traffic management, security, and observability features, allowing for fine-grained control and monitoring of the application traffic. This setup is ideal for organizations requiring a robust and resilient infrastructure for their critical applications.&lt;/p&gt;

&lt;p&gt;The original tech blog is &lt;a href="https://dvops.wordpress.com/2024/04/27/multi-region-yugabytedb-deployment-on-aws-eks-with-istio/" rel="noopener noreferrer"&gt;here&lt;/a&gt;, please follow/subscribe to get notifications directly in your inbox when new content goes live. You can also find me on LinkedIn @ &lt;a href="https://www.linkedin.com/in/vishnuhd/" rel="noopener noreferrer"&gt;in/vishnuhd&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>istio</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>yugabytedb</category>
    </item>
  </channel>
</rss>
